詳解Golang中零拷貝的原理以及實踐
零拷貝原理
零拷貝技術(shù)的原理本質(zhì)上就是減少數(shù)據(jù)的拷貝次數(shù),因為當調(diào)用傳統(tǒng)read write方法讀取文件內(nèi)容并返回給客戶端的時候,會經(jīng)過四次拷貝。我用golang代碼舉例如下
func?main()?{??
???http.HandleFunc("/tradition",?func(writer?http.ResponseWriter,?request?*http.Request)?{??
??????f,?_?:=?os.Open("./testmmap.txt")??
??????buf?:=?make([]byte,?1024)??
??????//?內(nèi)核拷貝到buf
??????n,?_?:=?f.Read(buf)??
??????//?buf拷貝到內(nèi)核
??????writer.Write(buf[:n])??
???})??
???http.ListenAndServe(":8080",?http.DefaultServeMux)??
}如上面代碼所示,如果我們需要將本地testmmap.txt文件的內(nèi)容讀出來返回給客戶端。
testmmap.txt里只有一個hello的單詞,當服務啟動以后訪問接口便會返回hello。
(base) ? codelearning git:(master) ? cat testmmap.txt
hello
(base) ? codelearning git:(master) ? curl localhost:8080/tradition
hello
整個過程需要經(jīng)過read和write兩次系統(tǒng)調(diào)用,而每次read和write的調(diào)用將面臨用戶態(tài)和內(nèi)核態(tài)緩沖區(qū)之間數(shù)據(jù)的拷貝。

整個拷貝過程如上圖所示,磁盤和內(nèi)核間的數(shù)據(jù)傳遞可以通過DMA技術(shù)讓cpu不參與其中,但是內(nèi)核態(tài)和用戶態(tài)間的數(shù)據(jù)拷貝則需要經(jīng)過cpu參與,涉及到了兩次系統(tǒng)調(diào)用,和4次數(shù)據(jù)拷貝。
mmap+write
基于上述傳統(tǒng)文件的訪問方式,我們可以用mmap技術(shù)進行優(yōu)化,mmap可以讓用戶緩沖區(qū)buf的地址和文件磁盤地址建立映射,這樣訪問用戶緩沖區(qū)buf的數(shù)據(jù)就等效于訪問磁盤文件上的數(shù)據(jù)。
用mmap優(yōu)化后的文件訪問代碼如下:
??
http.HandleFunc("/mmap",?func(writer?http.ResponseWriter,?request?*http.Request)?{??
???f,?_?:=?os.Open("./testmmap.txt")??
???data,?err?:=?syscall.Mmap(int(f.Fd()),?0,?5,?syscall.PROT_READ,?syscall.MAP_SHARED)??
???if?err?!=?nil?{??
??????panic(err)??
???}??
???writer.Write(data)??
})可以看到mmap返回了一個data的字節(jié)數(shù)組,這個字節(jié)數(shù)組的內(nèi)容就是映射了文件內(nèi)容,之后將字節(jié)數(shù)組寫入到響應體里。
syscall.Mmap(int(f.Fd()), 0, 5, syscall.PROT_READ, syscall.MAP_SHARED)
這里再解釋下mmap涉及的參數(shù)含義:
其中第一個參數(shù)代表要映射的文件描述符。
接著是映射的范圍是從0個字節(jié)到第5個字節(jié)。
第四個參數(shù) 代表映射的后的內(nèi)存區(qū)域是只讀的,類似的參數(shù)還有 syscall.PROT_WRITE表示內(nèi)存區(qū)域可以被寫入,syscall.PROT_NONE表示內(nèi)存區(qū)域不可訪問。
第五個參數(shù)表示 映射的內(nèi)存區(qū)域可以被多個進程共享,這樣一個進程修改了這個內(nèi)存區(qū)域的數(shù)據(jù),對其他進程是可見的,并且修改后的內(nèi)容會自動被操作系統(tǒng)同步到磁盤文件里。
類似的參數(shù)還有syscall.MAP_PRIVATE表示內(nèi)存區(qū)域是私有的,不可被其他進程訪問,聲明為私有后,每個進程擁有單獨的一份內(nèi)存映射拷貝,并且對此內(nèi)存區(qū)域進行修改不會被同步到磁盤文件。
注意整個過程,我們是沒有將文件內(nèi)容讀取到用戶空間的任何緩沖區(qū)的。我們僅僅是在write系統(tǒng)調(diào)用時,告訴了內(nèi)核一個地址(即字節(jié)數(shù)組的地址),而這個地址被mmap映射成了文件的地址。示意圖如下:

整個過程是用戶進程告訴內(nèi)核需要拷貝的數(shù)據(jù)數(shù)據(jù)的地址,然后內(nèi)核拷貝數(shù)據(jù)。
sendfile
基于上述mmap+write方式進行優(yōu)化后的文件內(nèi)容訪問減少了一次拷貝過程,不過系統(tǒng)調(diào)用還是兩次。如果用sendfile的話可以將系統(tǒng)調(diào)用減少到一次。
func?Sendfile(outfd?int,?infd?int,?offset?*int64,?count?int)?(written?int,?err?error)?
Sendfile的系統(tǒng)調(diào)用可以將目的文件描述符和源文件描述符傳遞進去,剩下的拷貝過程就交給內(nèi)核了。示意圖如下:

但是sendfile對源文件描述符有要求,普通的文件可以,如果源文件描述符是socket則不能用sendfile了。
splice
splice系統(tǒng)調(diào)用則是為了解決源文件描述符和目的文件描述符都是socket的情況而產(chǎn)生的。splice系統(tǒng)調(diào)用的原理是通過管道讓數(shù)據(jù)在源socket和目的socket之間進行傳輸。示意圖如下:

splice的系統(tǒng)調(diào)用方法如下:
func?Splice(rfd?int,?roff?*int64,?wfd?int,?woff?*int64,?len?int,?flags?int)?(n?int64,?err?error)?
注意splice系統(tǒng)調(diào)用需要保證傳入的文件描述符,rfd或者wfd至少一個是管道的文件描述符。創(chuàng)建管道也是一個系統(tǒng)調(diào)用,如下:
func?Pipe2(p?[]int,?flags?int)?error?
再回到通過splice系統(tǒng)調(diào)用的情況,可以看到要調(diào)用兩次splice系統(tǒng)調(diào)用,才能完成socket間的數(shù)據(jù)傳遞,因為splice系統(tǒng)調(diào)用會根據(jù)源文件描述符或目的文件描述符是管道的情況做不同的動作。
第一次系統(tǒng)調(diào)用,目的文件描述符是管道,那么內(nèi)核則會將管道和源文件描述符綁定在一起,注意此時是不會進行數(shù)據(jù)拷貝的。
第二次splice系統(tǒng)調(diào)用,源文件描述符是管道,那么內(nèi)核才會將管道內(nèi)的數(shù)據(jù)拷貝到目的文件描述符,由于在前一次,管道已經(jīng)和源文件描述符進行了綁定,所以這次的splice系統(tǒng)調(diào)用,實際上會將源文件描述符的數(shù)據(jù)拷貝到目的文件描述符。
整個過程,拋開DMA技術(shù)拷貝的次數(shù),一共只有一次數(shù)據(jù)拷貝的過程。
零拷貝在golang中的實踐
講完了零拷貝涉及的技術(shù),我們來看看golang是如何運用這些技術(shù)的。拿一個比較常用的方法舉例,io.Copy, 其底層調(diào)用了copyBuffer方法,copyBuffer會判斷copy的目的接口Writer是否實現(xiàn)了ReaderFrom 接口,如果實現(xiàn)了則直接調(diào)用ReaderFrom 從src讀取數(shù)據(jù)。
func?Copy(dst?Writer,?src?Reader)?(written?int64,?err?error)?{??
???return?copyBuffer(dst,?src,?nil)??
}
func?copyBuffer(dst?Writer,?src?Reader,?buf?[]byte)?(written?int64,?err?error)?{??
???//?If?the?reader?has?a?WriteTo?method,?use?it?to?do?the?copy.??
???//?Avoids?an?allocation?and?a?copy.???if?wt,?ok?:=?src.(WriterTo);?ok?{??
??????return?wt.WriteTo(dst)??
???}??
???//?Similarly,?if?the?writer?has?a?ReadFrom?method,?use?it?to?do?the?copy.??
???if?rt,?ok?:=?dst.(ReaderFrom);?ok?{??
??????return?rt.ReadFrom(src)??
???}??
???//?進行傳統(tǒng)的文件讀取,代碼較長,暫時省略了。
???.......
???return?written,?err??
}net.TcpConn實現(xiàn)了ReadFrom 接口,拿net.TcpConn舉例,看看它的實現(xiàn)。
func?(c?*TCPConn)?readFrom(r?io.Reader)?(int64,?error)?{??
???if?n,?err,?handled?:=?splice(c.fd,?r);?handled?{??
??????return?n,?err??
???}??
???if?n,?err,?handled?:=?sendFile(c.fd,?r);?handled?{??
??????return?n,?err??
???}??
???return?genericReadFrom(c,?r)??
}最終net.TcpConn 會調(diào)用readFrom方法從來源io.Reader讀取數(shù)據(jù),而readFrom讀取數(shù)據(jù)用到的技術(shù)則是剛剛所講的零拷貝技術(shù),這里用到了splice和sendFile系統(tǒng)調(diào)用,如果來源io.Reader是一個tcp連接或者時unix 連接則會調(diào)用splice進行數(shù)據(jù)拷貝,否則就會調(diào)用sendFile進行數(shù)據(jù)拷貝,具體細節(jié)我就不在這里展開了。
總之,你可以看到,其實我們平時用到的方法就用到了零拷貝技術(shù),這些經(jīng)常說的底層原理離我們并不遙遠,學習,永遠懷著一顆謙卑的心。
到此這篇關(guān)于詳解Golang中零拷貝的原理以及實踐的文章就介紹到這了,更多相關(guān)Golang零拷貝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang基于websocket實現(xiàn)的簡易聊天室程序
這篇文章主要介紹了golang基于websocket實現(xiàn)的簡易聊天室,分析了websocket的下載、安裝及使用實現(xiàn)聊天室功能的相關(guān)技巧,需要的朋友可以參考下2016-07-07
golang結(jié)構(gòu)化日志slog的用法簡介
日志是任何軟件的重要組成部分,Go?提供了一個內(nèi)置日志包(slog),在本文中,小編將簡單介紹一下slog包的功能以及如何在?Go?應用程序中使用它,感興趣的可以了解下2023-09-09
Go語言中slice作為參數(shù)傳遞時遇到的一些“坑”
這篇文章主要給大家介紹了關(guān)于Go語言中slice作為參數(shù)傳遞時遇到的一些“坑”,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2018-03-03

