詳解Golang中零拷貝的原理以及實(shí)踐
零拷貝原理
零拷貝技術(shù)的原理本質(zhì)上就是減少數(shù)據(jù)的拷貝次數(shù),因?yàn)楫?dāng)調(diào)用傳統(tǒng)read write方法讀取文件內(nèi)容并返回給客戶端的時(shí)候,會(huì)經(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里只有一個(gè)hello的單詞,當(dāng)服務(wù)啟動(dòng)以后訪問接口便會(huì)返回hello。
(base) ? codelearning git:(master) ? cat testmmap.txt
hello
(base) ? codelearning git:(master) ? curl localhost:8080/tradition
hello
整個(gè)過程需要經(jīng)過read和write兩次系統(tǒng)調(diào)用,而每次read和write的調(diào)用將面臨用戶態(tài)和內(nèi)核態(tài)緩沖區(qū)之間數(shù)據(jù)的拷貝。
整個(gè)拷貝過程如上圖所示,磁盤和內(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ù)進(jìn)行優(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返回了一個(gè)data的字節(jié)數(shù)組,這個(gè)字節(jié)數(shù)組的內(nèi)容就是映射了文件內(nèi)容,之后將字節(jié)數(shù)組寫入到響應(yīng)體里。
syscall.Mmap(int(f.Fd()), 0, 5, syscall.PROT_READ, syscall.MAP_SHARED)
這里再解釋下mmap涉及的參數(shù)含義:
其中第一個(gè)參數(shù)代表要映射的文件描述符。
接著是映射的范圍是從0個(gè)字節(jié)到第5個(gè)字節(jié)。
第四個(gè)參數(shù) 代表映射的后的內(nèi)存區(qū)域是只讀的,類似的參數(shù)還有 syscall.PROT_WRITE表示內(nèi)存區(qū)域可以被寫入,syscall.PROT_NONE表示內(nèi)存區(qū)域不可訪問。
第五個(gè)參數(shù)表示 映射的內(nèi)存區(qū)域可以被多個(gè)進(jìn)程共享,這樣一個(gè)進(jìn)程修改了這個(gè)內(nèi)存區(qū)域的數(shù)據(jù),對(duì)其他進(jìn)程是可見的,并且修改后的內(nèi)容會(huì)自動(dòng)被操作系統(tǒng)同步到磁盤文件里。
類似的參數(shù)還有syscall.MAP_PRIVATE表示內(nèi)存區(qū)域是私有的,不可被其他進(jìn)程訪問,聲明為私有后,每個(gè)進(jìn)程擁有單獨(dú)的一份內(nèi)存映射拷貝,并且對(duì)此內(nèi)存區(qū)域進(jìn)行修改不會(huì)被同步到磁盤文件。
注意整個(gè)過程,我們是沒有將文件內(nèi)容讀取到用戶空間的任何緩沖區(qū)的。我們僅僅是在write系統(tǒng)調(diào)用時(shí),告訴了內(nèi)核一個(gè)地址(即字節(jié)數(shù)組的地址),而這個(gè)地址被mmap映射成了文件的地址。示意圖如下:
整個(gè)過程是用戶進(jìn)程告訴內(nèi)核需要拷貝的數(shù)據(jù)數(shù)據(jù)的地址,然后內(nèi)核拷貝數(shù)據(jù)。
sendfile
基于上述mmap+write方式進(jìn)行優(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)用可以將目的文件描述符和源文件描述符傳遞進(jìn)去,剩下的拷貝過程就交給內(nèi)核了。示意圖如下:
但是sendfile對(duì)源文件描述符有要求,普通的文件可以,如果源文件描述符是socket則不能用sendfile了。
splice
splice系統(tǒng)調(diào)用則是為了解決源文件描述符和目的文件描述符都是socket的情況而產(chǎn)生的。splice系統(tǒng)調(diào)用的原理是通過管道讓數(shù)據(jù)在源socket和目的socket之間進(jìn)行傳輸。示意圖如下:
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至少一個(gè)是管道的文件描述符。創(chuàng)建管道也是一個(gè)系統(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ù)傳遞,因?yàn)閟plice系統(tǒng)調(diào)用會(huì)根據(jù)源文件描述符或目的文件描述符是管道的情況做不同的動(dòng)作。
第一次系統(tǒng)調(diào)用,目的文件描述符是管道,那么內(nèi)核則會(huì)將管道和源文件描述符綁定在一起,注意此時(shí)是不會(huì)進(jìn)行數(shù)據(jù)拷貝的。
第二次splice系統(tǒng)調(diào)用,源文件描述符是管道,那么內(nèi)核才會(huì)將管道內(nèi)的數(shù)據(jù)拷貝到目的文件描述符,由于在前一次,管道已經(jīng)和源文件描述符進(jìn)行了綁定,所以這次的splice系統(tǒng)調(diào)用,實(shí)際上會(huì)將源文件描述符的數(shù)據(jù)拷貝到目的文件描述符。
整個(gè)過程,拋開DMA技術(shù)拷貝的次數(shù),一共只有一次數(shù)據(jù)拷貝的過程。
零拷貝在golang中的實(shí)踐
講完了零拷貝涉及的技術(shù),我們來看看golang是如何運(yùn)用這些技術(shù)的。拿一個(gè)比較常用的方法舉例,io.Copy, 其底層調(diào)用了copyBuffer方法,copyBuffer會(huì)判斷copy的目的接口Writer是否實(shí)現(xiàn)了ReaderFrom 接口,如果實(shí)現(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)?? ???}?? ???//?進(jìn)行傳統(tǒng)的文件讀取,代碼較長(zhǎng),暫時(shí)省略了。 ???....... ???return?written,?err?? }
net.TcpConn實(shí)現(xiàn)了ReadFrom 接口,拿net.TcpConn舉例,看看它的實(shí)現(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 會(huì)調(diào)用readFrom方法從來源io.Reader讀取數(shù)據(jù),而readFrom讀取數(shù)據(jù)用到的技術(shù)則是剛剛所講的零拷貝技術(shù),這里用到了splice和sendFile系統(tǒng)調(diào)用,如果來源io.Reader是一個(gè)tcp連接或者時(shí)unix 連接則會(huì)調(diào)用splice進(jìn)行數(shù)據(jù)拷貝,否則就會(huì)調(diào)用sendFile進(jìn)行數(shù)據(jù)拷貝,具體細(xì)節(jié)我就不在這里展開了。
總之,你可以看到,其實(shí)我們平時(shí)用到的方法就用到了零拷貝技術(shù),這些經(jīng)常說的底層原理離我們并不遙遠(yuǎn),學(xué)習(xí),永遠(yuǎn)懷著一顆謙卑的心。
到此這篇關(guān)于詳解Golang中零拷貝的原理以及實(shí)踐的文章就介紹到這了,更多相關(guān)Golang零拷貝內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang基于websocket實(shí)現(xiàn)的簡(jiǎn)易聊天室程序
這篇文章主要介紹了golang基于websocket實(shí)現(xiàn)的簡(jiǎn)易聊天室,分析了websocket的下載、安裝及使用實(shí)現(xiàn)聊天室功能的相關(guān)技巧,需要的朋友可以參考下2016-07-07golang結(jié)構(gòu)化日志slog的用法簡(jiǎn)介
日志是任何軟件的重要組成部分,Go?提供了一個(gè)內(nèi)置日志包(slog),在本文中,小編將簡(jiǎn)單介紹一下slog包的功能以及如何在?Go?應(yīng)用程序中使用它,感興趣的可以了解下2023-09-09golang構(gòu)建工具M(jìn)akefile使用詳解
這篇文章主要為大家介紹了golang構(gòu)建工具M(jìn)akefile的使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07Go語言中slice作為參數(shù)傳遞時(shí)遇到的一些“坑”
這篇文章主要給大家介紹了關(guān)于Go語言中slice作為參數(shù)傳遞時(shí)遇到的一些“坑”,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-03-03使用Go語言實(shí)現(xiàn)找出兩個(gè)大文件中相同的記錄
這篇文章主要為大家詳細(xì)介紹了使用Go語言實(shí)現(xiàn)找出兩個(gè)大文件中相同的記錄的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10