Go語(yǔ)言中零拷貝的原理與實(shí)現(xiàn)詳解
傳統(tǒng)讀寫(xiě)模式
傳統(tǒng)讀寫(xiě)模式流程圖
- 第一次數(shù)據(jù)拷貝: 用戶進(jìn)程發(fā)起 read() 系統(tǒng)調(diào)用,當(dāng)前上下文從用戶態(tài)切換至內(nèi)核態(tài),DMA(Direct Memory Access) 引擎從文件中讀取數(shù)據(jù),并存儲(chǔ)到內(nèi)核態(tài)緩沖區(qū) (DMA 拷貝)
- 第二次數(shù)據(jù)拷貝: 將數(shù)據(jù)從內(nèi)核態(tài)緩沖區(qū)拷貝到用戶態(tài)緩沖區(qū) (CPU 拷貝),然后返回給用戶進(jìn)程,拷貝數(shù)據(jù)時(shí)會(huì)發(fā)生一次上下文切換 (從內(nèi)核態(tài)切換到用戶態(tài))
- 第三次數(shù)據(jù)拷貝: 用戶進(jìn)程發(fā)起 write() 系統(tǒng)調(diào)用,當(dāng)前上下文從用戶態(tài)切換至內(nèi)核態(tài),數(shù)據(jù)從用戶態(tài)緩沖區(qū)被拷貝到 Socket 緩沖區(qū) (CPU 拷貝)
- 第四次數(shù)據(jù)拷貝: write() 系統(tǒng)調(diào)用結(jié)束返回到用戶進(jìn)程,當(dāng)前上下文從內(nèi)核態(tài)切換至用戶態(tài),第四次數(shù)據(jù)拷貝為異步執(zhí)行,從 Socket 緩沖區(qū)拷貝到網(wǎng)卡 (DMA 拷貝)
transferTo
transferTo() 和 send() 類似,也是一個(gè)系統(tǒng)調(diào)用,用于在文件之間高效地傳輸數(shù)據(jù)。
transferTo 在操作系統(tǒng)層面實(shí)現(xiàn)了零拷貝技術(shù),允許將數(shù)據(jù)直接從一個(gè)文件傳輸?shù)搅硪粋€(gè)文件,而無(wú)需通過(guò)用戶空間進(jìn)行中轉(zhuǎn)。
transferTo 流程圖
- 第一次數(shù)據(jù)拷貝: 用戶進(jìn)程發(fā)起 transferTo() 調(diào)用,將文件數(shù)據(jù)拷貝到一個(gè) Read buffer(內(nèi)核態(tài))中,當(dāng)前上下文從用戶態(tài)切換至內(nèi)核態(tài)
- 第二次數(shù)據(jù)拷貝: 內(nèi)核將 Read buffer 中的數(shù)據(jù)拷貝到 Socket 緩沖區(qū)
- 第三次數(shù)據(jù)拷貝: 數(shù)據(jù)從 Socket 緩沖區(qū)拷貝到網(wǎng)卡,當(dāng)前上下文從內(nèi)核態(tài)切換至用戶態(tài)
相比較于傳統(tǒng)的讀寫(xiě)模式, transferTo 把上下文的切換次數(shù)從 4 次減少到 2 次,同時(shí)把數(shù)據(jù)拷貝的次數(shù)從 4 次降低到了 3 次, 雖然已經(jīng)前進(jìn)了一大步,但是作為過(guò)渡階段,transferTo 距離零拷貝還有一些距離。
零拷貝
零拷貝是相對(duì)于用戶態(tài)來(lái)講的,數(shù)據(jù)在用戶態(tài)不發(fā)生任何拷貝。
sendfile + DMA
sendfile() 是作用于兩個(gè)文件描述符之間的數(shù)據(jù)拷貝的系統(tǒng)調(diào)用,這個(gè)拷貝操作是直接在內(nèi)核中進(jìn)行的,沒(méi)有用戶態(tài)到內(nèi)核態(tài)的數(shù)據(jù)拷貝和上下文切換帶來(lái)的開(kāi)銷,所以稱為零拷貝技術(shù)。
Linux2.4 內(nèi)核對(duì) sendfile 系統(tǒng)調(diào)用做了改進(jìn):
sendfile 改進(jìn)
- 用戶進(jìn)程發(fā)起 sendfile() 系統(tǒng)調(diào)用,當(dāng)前上下文從用戶態(tài)切換至內(nèi)核態(tài),DMA 將數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)
- 向 Socket 緩沖區(qū)中發(fā)送當(dāng)前數(shù)據(jù)在內(nèi)核緩沖區(qū)的地址和偏移量?jī)蓚€(gè)值
- 根據(jù) Socket 緩沖區(qū)的地址和偏移量,直接將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到網(wǎng)卡,當(dāng)前上下文從內(nèi)核態(tài)切換至用戶態(tài)
零拷貝流程圖
相比較于傳統(tǒng)的讀寫(xiě)模式, sendfile + DMA 把上下文的切換次數(shù)從 4 次減少到 2 次,同時(shí)把數(shù)據(jù)拷貝的次數(shù)從 4 次降低到了 2 次 (2 次均為 DMA 拷貝),完全消除了數(shù)據(jù)從用戶態(tài)和內(nèi)核態(tài)之間拷貝數(shù)據(jù)帶來(lái)的開(kāi)銷。
sendfile + DMA 雖然已經(jīng)足夠高效,但是依然存在兩個(gè)不足之處:
- 方案本身需要引入新的硬件支持
- 輸入文件描述符僅支持文件類型
splice
針對(duì) sendfile + DMA 方案存在的不足,Linux 引入了 splice() 系統(tǒng)調(diào)用, splice() 不需要硬件支持,能夠?qū)崿F(xiàn)在任意的兩個(gè)文件描述符時(shí)之間傳輸數(shù)據(jù)。
splice() 是基于管道緩沖區(qū)機(jī)制實(shí)現(xiàn)的,所以兩個(gè)參數(shù)文件描述符必須有一個(gè)是管道設(shè)備。在實(shí)際開(kāi)發(fā)中,splice() 作為實(shí)現(xiàn)零拷貝的首選,因此 sendfile() 的內(nèi)部實(shí)現(xiàn)也替換為了 splice()。
Go 語(yǔ)言中的零拷貝
現(xiàn)在有了前文的理論基礎(chǔ)后,我們來(lái)看下在 Go 語(yǔ)言中標(biāo)準(zhǔn)庫(kù)的零拷貝方法原型和應(yīng)用方法,筆者的 Go 版本為 go1.19 linux/amd64
。
sendfile
sendfile 的方法原型為 syscall.Sendfile,文件路徑為 syscall/syscall_unix.go。
func?Sendfile(outfd?int,?infd?int,?offset?*int64,?count?int)?(written?int,?err?error)
一個(gè)簡(jiǎn)單的使用示例:
package?main import?( ?"fmt" ?"os" ?"syscall" ) func?main()?{ ?//?設(shè)置源文件 ?src,?err?:=?os.Open("/tmp/source.txt") ?if?err?!=?nil?{ ??panic(err) ?} ?defer?src.Close() ?//?設(shè)置目標(biāo)文件 ?target,?err?:=?os.Create("/tmp/target.txt") ?if?err?!=?nil?{ ??panic(err) ?} ?defer?target.Close() ?//?獲取源文件的文件描述符 ?srcFd?:=?int(src.Fd()) ?//?獲取目標(biāo)文件的文件描述符 ?targetFd?:=?int(target.Fd()) ?//?使用?Sendfile?實(shí)現(xiàn)零拷貝?(拷貝?10?個(gè)字節(jié)) ?//?如果因?yàn)樽址幋a導(dǎo)致的字符截?cái)鄦?wèn)題?(如中文亂碼問(wèn)題),?結(jié)果自動(dòng)保留到截?cái)嗲暗淖詈笸暾止?jié) ?//?例如文件內(nèi)容為?“星期三四五六七”,count?參數(shù)為?4,?那么只會(huì)拷貝第一個(gè)字?(一個(gè)漢字?3?個(gè)字節(jié)) ?//?但是需要注意的是,方法的返回值?written?不受影響?(和?count?參數(shù)保持一致) ?//?所以實(shí)際開(kāi)發(fā)中,第三個(gè)參數(shù)?offset?必須設(shè)置正確,否則就可能引起亂碼或數(shù)據(jù)丟失問(wèn)題 ?n,?err?:=?syscall.Sendfile(targetFd,?srcFd,?nil,?4) ?if?err?!=?nil?{ ??fmt.Println(err) ??return ?} ?fmt.Printf("寫(xiě)入字節(jié)數(shù):?%d",?n) }
splice
splice 的方法原型為 syscall.Splice,文件路徑為 syscall/zsyscall_linux_amd64.go。
func?Splice(rfd?int,?roff?*int64,?wfd?int,?woff?*int64,?len?int,?flags?int)?(n?int64,?err?error)
一個(gè)簡(jiǎn)單的使用示例:
package?main import?( ?"fmt" ?"os" ?"syscall" ) func?main()?{ ?//?設(shè)置源文件 ?src,?err?:=?os.Open("/tmp/source.txt") ?if?err?!=?nil?{ ??panic(err) ?} ?defer?src.Close() ?//?設(shè)置目標(biāo)文件 ?target,?err?:=?os.Create("/tmp/target.txt") ?if?err?!=?nil?{ ??panic(err) ?} ?defer?target.Close() ?//?創(chuàng)建管道文件 ?//?作為兩個(gè)文件傳輸數(shù)據(jù)的中介 ?pipeReader,?pipeWriter,?err?:=?os.Pipe() ?if?err?!=?nil?{ ??panic(err) ?} ?defer?pipeReader.Close() ?defer?pipeWriter.Close() ?//?設(shè)置文件讀寫(xiě)模式 ?//?筆者在標(biāo)準(zhǔn)庫(kù)中沒(méi)有找到對(duì)應(yīng)的常量說(shuō)明 ?//?讀者可以參考這個(gè)文檔: ?//???https://pkg.go.dev/golang.org/x/sys/unix#pkg-constants ?//???SPLICE_F_NONBLOCK?=?0x2 ?spliceNonBlock?:=?0x02 ?//?使用?Splice?將數(shù)據(jù)從源文件描述符移動(dòng)到管道?writer ?_,?err?=?syscall.Splice(int(src.Fd()),?nil,?int(pipeWriter.Fd()),?nil,?1024,?spliceNonBlock) ?if?err?!=?nil?{ ??panic(err) ?} ?//?使用?Splice?將數(shù)據(jù)從管道?reader?移動(dòng)到目標(biāo)文件描述符 ?n,?err?:=?syscall.Splice(int(pipeReader.Fd()),?nil,?int(target.Fd()),?nil,?1024,?spliceNonBlock) ?if?err?!=?nil?{ ??panic(err) ?} ?fmt.Printf("寫(xiě)入字節(jié)數(shù):?%d",?n) }
以上就是Go語(yǔ)言中零拷貝的原理與實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于Go零拷貝的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go語(yǔ)言實(shí)現(xiàn)順序存儲(chǔ)的棧
這篇文章主要介紹了go語(yǔ)言實(shí)現(xiàn)順序存儲(chǔ)的棧,實(shí)例分析了Go語(yǔ)言實(shí)現(xiàn)順序存儲(chǔ)的棧的原理與各種常見(jiàn)的操作技巧,需要的朋友可以參考下2015-03-03Golang 實(shí)現(xiàn)分片讀取http超大文件流和并發(fā)控制
這篇文章主要介紹了Golang 實(shí)現(xiàn)分片讀取http超大文件流和并發(fā)控制,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12Go語(yǔ)言使用select{}阻塞main函數(shù)介紹
這篇文章主要介紹了Go語(yǔ)言使用select{}阻塞main函數(shù)介紹,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04Golang連接池的幾種實(shí)現(xiàn)案例小結(jié)
這篇文章主要介紹了Golang連接池的幾種實(shí)現(xiàn)案例小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03Golang實(shí)現(xiàn)Md5校驗(yàn)的示例代碼
本文主要介紹了Golang實(shí)現(xiàn)Md5校驗(yàn)的示例代碼,要求接收方需要文件的md5值,和接收到的文件做比對(duì),以免文件不完整,但引起bug,下面就一起來(lái)解決一下2024-08-08Go語(yǔ)言七篇入門(mén)教程三函數(shù)方法及接口
這篇文章主要為大家介紹了Go語(yǔ)言的函數(shù)方法及接口的示例詳解,本文是Go語(yǔ)言七篇入門(mén)系列文章,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11Golang應(yīng)用執(zhí)行Shell命令實(shí)戰(zhàn)
本文主要介紹了Golang應(yīng)用執(zhí)行Shell命令實(shí)戰(zhàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03使用Go module和GoLand初始化一個(gè)Go項(xiàng)目的方法
這篇文章主要介紹了使用Go module和GoLand初始化一個(gè)Go項(xiàng)目,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12