Go 語言中的零拷貝
傳統讀寫模式
傳統讀寫模式流程圖
-
第一次數據拷貝: 用戶進程發起 read() 系統調用,當前上下文從用戶態切換至內核態,DMA(Direct Memory Access) 引擎從文件中讀取數據,並存儲到內核態緩衝區 (DMA 拷貝)
-
第二次數據拷貝: 將數據從內核態緩衝區拷貝到用戶態緩衝區 (CPU 拷貝),然後返回給用戶進程,拷貝數據時會發生一次上下文切換 (從內核態切換到用戶態)
-
第三次數據拷貝: 用戶進程發起 write() 系統調用,當前上下文從用戶態切換至內核態,數據從用戶態緩衝區被拷貝到 Socket 緩衝區 (CPU 拷貝)
-
第四次數據拷貝: write() 系統調用結束返回到用戶進程,當前上下文從內核態切換至用戶態,第四次數據拷貝爲異步執行,從 Socket 緩衝區拷貝到網卡 (DMA 拷貝)
transferTo
transferTo() 和 send() 類似,也是一個系統調用,用於在文件之間高效地傳輸數據。
transferTo 在操作系統層面實現了零拷貝技術,允許將數據直接從一個文件傳輸到另一個文件,而無需通過用戶空間進行中轉。
transferTo 流程圖
-
第一次數據拷貝: 用戶進程發起 transferTo() 調用,將文件數據拷貝到一個 Read buffer(內核態)中,當前上下文從用戶態切換至內核態
-
第二次數據拷貝: 內核將 Read buffer 中的數據拷貝到 Socket 緩衝區
-
第三次數據拷貝: 數據從 Socket 緩衝區拷貝到網卡,當前上下文從內核態切換至用戶態
相比較於傳統的讀寫模式, transferTo 把上下文的切換次數從 4 次減少到 2 次,同時把數據拷貝的次數從 4 次降低到了 3 次, 雖然已經前進了一大步,但是作爲過渡階段,transferTo 距離零拷貝還有一些距離。
零拷貝
零拷貝是相對於用戶態來講的,數據在用戶態不發生任何拷貝。
sendfile + DMA
sendfile() 是作用於兩個文件描述符之間的數據拷貝的系統調用,這個拷貝操作是直接在內核中進行的,沒有用戶態到內核態的數據拷貝和上下文切換帶來的開銷,所以稱爲零拷貝技術。
Linux2.4 內核對 sendfile 系統調用做了改進:
sendfile 改進
-
用戶進程發起 sendfile() 系統調用,當前上下文從用戶態切換至內核態,DMA 將數據拷貝到內核緩衝區
-
向 Socket 緩衝區中發送當前數據在內核緩衝區的地址和偏移量兩個值
-
根據 Socket 緩衝區的地址和偏移量,直接將內核緩衝區的數據拷貝到網卡,當前上下文從內核態切換至用戶態
零拷貝流程圖
相比較於傳統的讀寫模式, sendfile + DMA 把上下文的切換次數從 4 次減少到 2 次,同時把數據拷貝的次數從 4 次降低到了 2 次 (2 次均爲 DMA 拷貝),完全消除了數據從用戶態和內核態之間拷貝數據帶來的開銷。
sendfile + DMA 雖然已經足夠高效,但是依然存在兩個不足之處:
-
方案本身需要引入新的硬件支持
-
輸入文件描述符僅支持文件類型
splice
針對 sendfile + DMA 方案存在的不足,Linux 引入了 splice() 系統調用, splice() 不需要硬件支持,能夠實現在任意的兩個文件描述符時之間傳輸數據。
splice() 是基於管道緩衝區機制實現的,所以兩個參數文件描述符必須有一個是管道設備。在實際開發中,splice() 作爲實現零拷貝的首選,因此 sendfile() 的內部實現也替換爲了 splice()。
Go 語言中的零拷貝
現在有了前文的理論基礎後,我們來看下在 Go 語言中標準庫的零拷貝方法原型和應用方法,筆者的 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() {
// 設置源文件
src, err := os.Open("/tmp/source.txt")
if err != nil {
panic(err)
}
defer src.Close()
// 設置目標文件
target, err := os.Create("/tmp/target.txt")
if err != nil {
panic(err)
}
defer target.Close()
// 獲取源文件的文件描述符
srcFd := int(src.Fd())
// 獲取目標文件的文件描述符
targetFd := int(target.Fd())
// 使用 Sendfile 實現零拷貝 (拷貝 10 個字節)
// 如果因爲字符編碼導致的字符截斷問題 (如中文亂碼問題), 結果自動保留到截斷前的最後完整字節
// 例如文件內容爲 “星期三四五六七”,count 參數爲 4, 那麼只會拷貝第一個字 (一個漢字 3 個字節)
// 但是需要注意的是,方法的返回值 written 不受影響 (和 count 參數保持一致)
// 所以實際開發中,第三個參數 offset 必須設置正確,否則就可能引起亂碼或數據丟失問題
n, err := syscall.Sendfile(targetFd, srcFd, nil, 4)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("寫入字節數: %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() {
// 設置源文件
src, err := os.Open("/tmp/source.txt")
if err != nil {
panic(err)
}
defer src.Close()
// 設置目標文件
target, err := os.Create("/tmp/target.txt")
if err != nil {
panic(err)
}
defer target.Close()
// 創建管道文件
// 作爲兩個文件傳輸數據的中介
pipeReader, pipeWriter, err := os.Pipe()
if err != nil {
panic(err)
}
defer pipeReader.Close()
defer pipeWriter.Close()
// 設置文件讀寫模式
// 筆者在標準庫中沒有找到對應的常量說明
// 讀者可以參考這個文檔:
// https://pkg.go.dev/golang.org/x/sys/unix#pkg-constants
// SPLICE_F_NONBLOCK = 0x2
spliceNonBlock := 0x02
// 使用 Splice 將數據從源文件描述符移動到管道 writer
_, err = syscall.Splice(int(src.Fd()), nil, int(pipeWriter.Fd()), nil, 1024, spliceNonBlock)
if err != nil {
panic(err)
}
// 使用 Splice 將數據從管道 reader 移動到目標文件描述符
n, err := syscall.Splice(int(pipeReader.Fd()), nil, int(target.Fd()), nil, 1024, spliceNonBlock)
if err != nil {
panic(err)
}
fmt.Printf("寫入字節數: %d", n)
}
擴展閱讀
-
Why Kafka Is so Fast[1]
-
Efficient data transfer through zero copy[2]
-
Optimizing Large File Transfers in Linux with Go — An Exploration of TCP and Syscall[3]
-
directio[4]
-
Go 語言中的零拷貝優化 [5]
-
Linux I/O 原理和 Zero-copy 技術全面揭祕 [6]
-
零拷貝技術第一篇:綜述 [7]
-
direct io[8]
-
sys/unix[9]
-
sendfile[10]
-
splice[11]
-
zerocopy[12]
鏈接
[1]
Why Kafka Is so Fast: https://medium.com/swlh/why-kafka-is-so-fast-bde0d987cd03
[2]
Efficient data transfer through zero copy: https://developer.ibm.com/articles/j-zerocopy/
[3]
Optimizing Large File Transfers in Linux with Go — An Exploration of TCP and Syscall: https://itnext.io/optimizing-large-file-transfers-in-linux-with-go-an-exploration-of-tcp-and-syscall-ebe1b93fb72f
[4]
directio: https://github.com/ncw/directio
[5]
Go 語言中的零拷貝優化: https://strikefreedom.top/archives/pipe-pool-for-splice-in-go
[6]
Linux I/O 原理和 Zero-copy 技術全面揭祕: https://strikefreedom.top/archives/linux-io-and-zero-copy#toc-head-15
[7]
零拷貝技術第一篇:綜述: https://colobu.com/2022/11/19/zero-copy-and-how-to-use-it-in-go/
[8]
direct io: https://github.com/cch123/golang-notes/blob/master/io.md
[9]
sys/unix: https://pkg.go.dev/golang.org/x/sys/unix#Splice
[10]
sendfile: https://github.com/hslam/sendfile
[11]
splice: https://github.com/hslam/splice
[12]
zerocopy: https://github.com/acln0/zerocopy
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ytvDPx6Cii3srKPGFlPKCw