Go語言中零拷貝的原理與實現(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ù),并存儲到內(nèi)核態(tài)緩沖區(qū) (DMA 拷貝)
- 第二次數(shù)據(jù)拷貝: 將數(shù)據(jù)從內(nèi)核態(tài)緩沖區(qū)拷貝到用戶態(tài)緩沖區(qū) (CPU 拷貝),然后返回給用戶進(jìn)程,拷貝數(shù)據(jù)時會發(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() 類似,也是一個系統(tǒng)調(diào)用,用于在文件之間高效地傳輸數(shù)據(jù)。
transferTo 在操作系統(tǒng)層面實現(xiàn)了零拷貝技術(shù),允許將數(shù)據(jù)直接從一個文件傳輸?shù)搅硪粋€文件,而無需通過用戶空間進(jìn)行中轉(zhuǎn)。
transferTo 流程圖
- 第一次數(shù)據(jù)拷貝: 用戶進(jìn)程發(fā)起 transferTo() 調(diào)用,將文件數(shù)據(jù)拷貝到一個 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ù)據(jù)拷貝的次數(shù)從 4 次降低到了 3 次, 雖然已經(jīng)前進(jìn)了一大步,但是作為過渡階段,transferTo 距離零拷貝還有一些距離。
零拷貝
零拷貝是相對于用戶態(tài)來講的,數(shù)據(jù)在用戶態(tài)不發(fā)生任何拷貝。
sendfile + DMA
sendfile() 是作用于兩個文件描述符之間的數(shù)據(jù)拷貝的系統(tǒng)調(diào)用,這個拷貝操作是直接在內(nèi)核中進(jìn)行的,沒有用戶態(tài)到內(nèi)核態(tài)的數(shù)據(jù)拷貝和上下文切換帶來的開銷,所以稱為零拷貝技術(shù)。
Linux2.4 內(nèi)核對 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ù) 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ù)據(jù)拷貝的次數(shù)從 4 次降低到了 2 次 (2 次均為 DMA 拷貝),完全消除了數(shù)據(jù)從用戶態(tài)和內(nèi)核態(tài)之間拷貝數(shù)據(jù)帶來的開銷。
sendfile + DMA 雖然已經(jīng)足夠高效,但是依然存在兩個不足之處:
- 方案本身需要引入新的硬件支持
- 輸入文件描述符僅支持文件類型
splice
針對 sendfile + DMA 方案存在的不足,Linux 引入了 splice() 系統(tǒng)調(diào)用, splice() 不需要硬件支持,能夠?qū)崿F(xiàn)在任意的兩個文件描述符時之間傳輸數(shù)據(jù)。
splice() 是基于管道緩沖區(qū)機(jī)制實現(xiàn)的,所以兩個參數(shù)文件描述符必須有一個是管道設(shè)備。在實際開發(fā)中,splice() 作為實現(xiàn)零拷貝的首選,因此 sendfile() 的內(nèi)部實現(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)
一個簡單的使用示例:
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?實現(xiàn)零拷貝?(拷貝?10?個字節(jié)) ?//?如果因為字符編碼導(dǎo)致的字符截斷問題?(如中文亂碼問題),?結(jié)果自動保留到截斷前的最后完整字節(jié) ?//?例如文件內(nèi)容為?“星期三四五六七”,count?參數(shù)為?4,?那么只會拷貝第一個字?(一個漢字?3?個字節(jié)) ?//?但是需要注意的是,方法的返回值?written?不受影響?(和?count?參數(shù)保持一致) ?//?所以實際開發(fā)中,第三個參數(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)
一個簡單的使用示例:
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)建管道文件 ?//?作為兩個文件傳輸數(shù)據(jù)的中介 ?pipeReader,?pipeWriter,?err?:=?os.Pipe() ?if?err?!=?nil?{ ??panic(err) ?} ?defer?pipeReader.Close() ?defer?pipeWriter.Close() ?//?設(shè)置文件讀寫模式 ?//?筆者在標(biāo)準(zhǔn)庫中沒有找到對應(yīng)的常量說明 ?//?讀者可以參考這個文檔: ?//???https://pkg.go.dev/golang.org/x/sys/unix#pkg-constants ?//???SPLICE_F_NONBLOCK?=?0x2 ?spliceNonBlock?:=?0x02 ?//?使用?Splice?將數(shù)據(jù)從源文件描述符移動到管道?writer ?_,?err?=?syscall.Splice(int(src.Fd()),?nil,?int(pipeWriter.Fd()),?nil,?1024,?spliceNonBlock) ?if?err?!=?nil?{ ??panic(err) ?} ?//?使用?Splice?將數(shù)據(jù)從管道?reader?移動到目標(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語言中零拷貝的原理與實現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于Go零拷貝的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang 實現(xiàn)分片讀取http超大文件流和并發(fā)控制
這篇文章主要介紹了Golang 實現(xiàn)分片讀取http超大文件流和并發(fā)控制,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12Golang應(yīng)用執(zhí)行Shell命令實戰(zhàn)
本文主要介紹了Golang應(yīng)用執(zhí)行Shell命令實戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03使用Go module和GoLand初始化一個Go項目的方法
這篇文章主要介紹了使用Go module和GoLand初始化一個Go項目,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12