Go語言中零拷貝的原理與實(shí)現(xiàn)詳解
傳統(tǒng)讀寫模式

傳統(tǒng)讀寫模式流程圖
- 第一次數(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è)文件,而無需通過用戶空間進(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)的讀寫模式, transferTo 把上下文的切換次數(shù)從 4 次減少到 2 次,同時(shí)把數(shù)據(jù)拷貝的次數(shù)從 4 次降低到了 3 次, 雖然已經(jīng)前進(jìn)了一大步,但是作為過渡階段,transferTo 距離零拷貝還有一些距離。
零拷貝
零拷貝是相對(duì)于用戶態(tài)來講的,數(shù)據(jù)在用戶態(tài)不發(fā)生任何拷貝。
sendfile + DMA
sendfile() 是作用于兩個(gè)文件描述符之間的數(shù)據(jù)拷貝的系統(tǒng)調(diào)用,這個(gè)拷貝操作是直接在內(nèi)核中進(jìn)行的,沒有用戶態(tài)到內(nèi)核態(tài)的數(shù)據(jù)拷貝和上下文切換帶來的開銷,所以稱為零拷貝技術(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)的讀寫模式, sendfile + DMA 把上下文的切換次數(shù)從 4 次減少到 2 次,同時(shí)把數(shù)據(jù)拷貝的次數(shù)從 4 次降低到了 2 次 (2 次均為 DMA 拷貝),完全消除了數(shù)據(jù)從用戶態(tài)和內(nèi)核態(tài)之間拷貝數(shù)據(jù)帶來的開銷。
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í)際開發(fā)中,splice() 作為實(shí)現(xiàn)零拷貝的首選,因此 sendfile() 的內(nèi)部實(shí)現(xiàn)也替換為了 splice()。
Go 語言中的零拷貝
現(xiàn)在有了前文的理論基礎(chǔ)后,我們來看下在 Go 語言中標(biāo)準(zhǔn)庫的零拷貝方法原型和應(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)鄦栴}?(如中文亂碼問題),?結(jié)果自動(dòng)保留到截?cái)嗲暗淖詈笸暾止?jié)
?//?例如文件內(nèi)容為?“星期三四五六七”,count?參數(shù)為?4,?那么只會(huì)拷貝第一個(gè)字?(一個(gè)漢字?3?個(gè)字節(jié))
?//?但是需要注意的是,方法的返回值?written?不受影響?(和?count?參數(shù)保持一致)
?//?所以實(shí)際開發(fā)中,第三個(gè)參數(shù)?offset?必須設(shè)置正確,否則就可能引起亂碼或數(shù)據(jù)丟失問題
?n,?err?:=?syscall.Sendfile(targetFd,?srcFd,?nil,?4)
?if?err?!=?nil?{
??fmt.Println(err)
??return
?}
?fmt.Printf("寫入字節(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è)置文件讀寫模式
?//?筆者在標(biāo)準(zhǔn)庫中沒有找到對(duì)應(yīng)的常量說明
?//?讀者可以參考這個(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("寫入字節(jié)數(shù):?%d",?n)
}以上就是Go語言中零拷貝的原理與實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于Go零拷貝的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang 實(shí)現(xiàn)分片讀取http超大文件流和并發(fā)控制
這篇文章主要介紹了Golang 實(shí)現(xiàn)分片讀取http超大文件流和并發(fā)控制,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Golang連接池的幾種實(shí)現(xiàn)案例小結(jié)
這篇文章主要介紹了Golang連接池的幾種實(shí)現(xiàn)案例小結(jié),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
Golang實(shí)現(xiàn)Md5校驗(yàn)的示例代碼
本文主要介紹了Golang實(shí)現(xiàn)Md5校驗(yàn)的示例代碼,要求接收方需要文件的md5值,和接收到的文件做比對(duì),以免文件不完整,但引起bug,下面就一起來解決一下2024-08-08
Golang應(yīng)用執(zhí)行Shell命令實(shí)戰(zhàn)
本文主要介紹了Golang應(yīng)用執(zhí)行Shell命令實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(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

