零拷貝技術第一篇:綜述
零拷貝(zero copy)在一些語境下指代的意思有所不同,本文講的零拷貝就是大家常說的,通過這個技術讓CPU釋放出來不去執(zhí)行內存中數(shù)據(jù)拷貝的功能,或者避免不必要的拷貝,所以說零拷貝不是沒有數(shù)據(jù)的拷貝(復制),而是廣義上講的減少和避免不必要的數(shù)據(jù)拷貝,可以用來節(jié)省CPU使用和內帶寬等,比如通過網(wǎng)絡高速傳輸文件、實現(xiàn)網(wǎng)絡proxy等等,零拷技術可以極大的提高程序的性能。
本文總結零拷貝的各種技術,下一篇介紹常見的零拷貝技術在Go語言中的應用。
零拷貝技術
其實,零拷貝很久以來都被用在提升程序的性能上,比如nginx、kafka等,而且很多文章也詳細介紹了零拷貝就要解決的問題,我在這里還是在總結一下,如果你已經了解了零拷貝的計數(shù),不妨回顧一下。
我們來分析一個從網(wǎng)絡讀取文件的場景。服務器從磁盤讀取一個文件,并寫入到socket中返回給客戶端。我們看看服務端的數(shù)據(jù)拷貝情況:

程序開始使用系統(tǒng)調用read[1]告訴操作系統(tǒng)要從磁盤文件中讀取數(shù)據(jù),它首先從用戶態(tài)切換到內核態(tài),這個切換是有花費的,操作系統(tǒng)需要保存用戶態(tài)的狀態(tài),一些寄存器的地址等,等read系統(tǒng)調用完成后返回,程序又需要從內核態(tài)切換到用戶態(tài),把保存的用戶態(tài)的狀態(tài)恢復,所以一次系統(tǒng)調用需要兩次的用戶態(tài)/內核態(tài)的切換。同樣,把文件的內容寫入到socket的時候,程序調用write[2]系統(tǒng)調用,又進行了兩次用戶態(tài)/內核態(tài)的切換。
從操作的數(shù)據(jù)來看,這個數(shù)據(jù)還被拷貝了四次。在read系統(tǒng)調用的時候,DMA方式從磁盤拷貝到內核緩沖區(qū),又通過CPU拷貝從內核緩沖區(qū)拷貝到用戶的程序緩沖區(qū),這里發(fā)生了兩次拷貝。在寫入socket的時候,數(shù)據(jù)先從用戶程序緩沖區(qū)寫入到socket緩沖區(qū),又通過DMA方式從socket緩沖區(qū)寫入到網(wǎng)卡。數(shù)據(jù)拷貝也發(fā)生了四次。
DMA(Direct Memory Access,直接存儲器訪問) 是計算機科學中的一種內存訪問技術。它允許某些電腦內部的硬件子系統(tǒng)(電腦外設),可以獨立地直接讀寫系統(tǒng)內存,允許不同速度的硬件設備來溝通,而不需要依于中央處理器的大量中斷負載。
你可以看到,傳統(tǒng)的IO讀寫方式,包括了四次用戶態(tài)/內核態(tài)的上下文切換,四次數(shù)據(jù)的拷貝,對性能的影響還是挺大的。廣義的零拷貝的技術,就是要盡量減少用戶態(tài)/內核態(tài)的上下文切換,以及數(shù)據(jù)的拷貝次數(shù),為此操作系統(tǒng)也提供了幾種方法。
mmap + write
通過mmap系統(tǒng)調用,將用戶空間的虛擬地址和內核空間的虛擬地址映射成同一個物理地址這樣可以減少內核空間和內核空間的數(shù)據(jù)拷貝。

通過mmap系統(tǒng)調用發(fā)起IO讀取,DMA將磁盤數(shù)據(jù)寫入到內核緩沖區(qū),此時mmap系統(tǒng)調用就返回了。程序調用write系統(tǒng)調用,CPU將內核緩沖區(qū)的數(shù)據(jù)寫入到socket緩沖區(qū),DMA又將數(shù)據(jù)從socket緩沖區(qū)謝瑞到網(wǎng)卡。
可以看到,mmap+write方式有兩次系統(tǒng)調用,發(fā)生四次用戶態(tài)/內核態(tài)的切換,三次數(shù)據(jù)拷貝。
相對傳統(tǒng)的IO方式,減少了一次數(shù)據(jù)拷貝,但是應該還有優(yōu)化的空間。
sendfile
sendfile[3]是Linux2.1內核版本后引入的一個系統(tǒng)調用函數(shù),用來優(yōu)化數(shù)據(jù)傳輸。它可以在文件描述符之間傳遞數(shù)據(jù),因為都是在內核之間傳遞數(shù)據(jù),所以非常高效。Linux 2.6.33之前目的文件描述符必須是文件,以后的版本就沒有限制了,可以是任意的文件。
但是源文件描述符要求必須是支持mmap[4]操作的文件描述符,普通的文件可以,但是socket就不行了。所以sendfile適合從文件讀取數(shù)據(jù)寫socket場景,所以sendfile這個名字還是很貼切的,發(fā)送文件。

用戶調用sendfile系統(tǒng)調用,數(shù)據(jù)通過DMA拷貝到內核緩沖區(qū),CPU將數(shù)據(jù)從內核緩沖區(qū)再寫入到socket緩沖區(qū),DMA將socket緩沖區(qū)數(shù)據(jù)寫入到網(wǎng)卡,然后sendfile系統(tǒng)調用返回。
可以看到,這里只有一次系統(tǒng)調用,也就是兩次用戶態(tài)/內核態(tài)的切換,三次數(shù)據(jù)拷貝。
相對來說,這種方式對性能已經有所提升。
linux 2.4之后,又對sendfile做了優(yōu)化,對于支持 dms scatter/gather功能的網(wǎng)卡,只把關于數(shù)據(jù)的位置和長度的信息的描述符被追加到了socket緩沖區(qū)中。DMA引擎直接把數(shù)據(jù)從內核緩沖區(qū)傳輸?shù)骄W(wǎng)卡(protocol engine),從而消除了僅有的一次CPU拷貝。

splice、tee、vmsplice
sendfile性能雖好,但是還是有些場景下是不能使用的,比如我們想做一個socket proxy,源和目的都是socket,就不能直接使用sendfile了。這個時候我們可以考慮splice[5]。
Linux 2.6.30版本之前,源和目的只能有一個是管道(pipe), 自2.6.31開始, 源和目的只要保證有一個是就行。

但是,如果每次都創(chuàng)建一個管道,你會發(fā)現(xiàn)每次都會多一次系統(tǒng)調用,也就是兩次用戶態(tài)/內核態(tài)的切換,所以你如果頻繁的拷貝數(shù)據(jù),那么可以建立一個管道池就像潘建給Go的標準庫提供的一個補丁一樣,利用pipe pool對Go語言中的splice做了優(yōu)化。
tee系統(tǒng)調用用來在兩個管道中拷貝數(shù)據(jù)。vmsplice系統(tǒng)調用pipe指向的內核緩沖區(qū)和用戶程序的緩沖區(qū)之間的數(shù)據(jù)拷貝。
MSG_ZEROCOPY
Linux v4.14 版本接受了在TCP send系統(tǒng)調用中實現(xiàn)的支持零拷貝(MSG_ZEROCOPY[6])的patch,通過這個patch,用戶進程就能夠把用戶緩沖區(qū)的數(shù)據(jù)通過零拷貝的方式經過內核空間發(fā)送到網(wǎng)絡套接字中去,在5.0中支持UDP。Willem de Bruijn 在他的論文里給出的壓測數(shù)據(jù)是:采用 netperf 大包發(fā)送測試,性能提升 39%,而線上環(huán)境的數(shù)據(jù)發(fā)送性能則提升了 5%~8%,官方文檔陳述說這個特性通常只在發(fā)送 10KB 左右大包的場景下才會有顯著的性能提升。一開始這個特性只支持 TCP,到內核 v5.0 版本之后才支持 UDP。這里也有一篇官方文檔介紹:Zero-copy networking[7]
首先你需要設置socket選項:
if?(setsockopt(fd,?SOL_SOCKET,?SO_ZEROCOPY,?&one,?sizeof(one)))
????????error(1,?errno,?"setsockopt?zerocopy");
然后調用send系統(tǒng)調用是傳入MSG_ZEROCOPY參數(shù):
ret?=?send(fd,?buf,?sizeof(buf),?MSG_ZEROCOPY);
這里我們傳入了buf,但是啥時候buf可以重用呢?這個內核會通知程序進程。它將完成通知放在socket error隊列中,所以你需要讀取這個隊列,知道拷貝啥時候完成buf可釋放或者重用了:
pfd.fd?=?fd;
pfd.events?=?0;
if?(poll(&pfd,?1,?-1)?!=?1?||?pfd.revents?&?POLLERR?==?0)
????????error(1,?errno,?"poll");
ret?=?recvmsg(fd,?&msg,?MSG_ERRQUEUE);
if?(ret?==?-1)
????????error(1,?errno,?"recvmsg");
read_notification(msg);
因為它可能異步發(fā)送數(shù)據(jù),你需要檢查buf啥時候釋放,增加代碼復雜度,以及會導致多次用戶態(tài)和內核態(tài)的上下文切換;
Linux 4.18中也支持的receive MSG_ZEROCOPY機制(Zero-copy TCP receive[8]).
字節(jié)跳動的同學2021年10曾寫過文章,通過修改內核的方式兼容先前的send調用方式。這畢竟是特殊的優(yōu)化,不適合大眾的使用方式,所以這個零拷貝的方式還是只在一些特殊的場景下進行優(yōu)化:
字節(jié)跳動框架組和字節(jié)跳動內核組合作,由內核組提供了同步的接口:當調用 sendmsg 的時候,內核會監(jiān)聽并攔截內核原先給業(yè)務的回調,并且在回調完成后才會讓 sendmsg 返回。這使得我們無需更改原有模型,可以很方便地接入 ZeroCopy send。同時,字節(jié)跳動內核組還實現(xiàn)了基于 unix domain socket 的 ZeroCopy,可以使得業(yè)務進程與 Mesh sidecar 之間的通信也達到零拷貝。
字節(jié)跳動在 Go 網(wǎng)絡庫上的實踐 [9]
copy_file_range
Linux 4.5 增加了一個新的API: copy_file_range[10], 它在內核態(tài)進行文件的拷貝,不再切換用戶空間,所以會比cp少塊一些,在一些場景下會提升性能。

其它
AF_XDP[11]是Linux 4.18新增加的功能,以前稱為AF_PACKETv4(從未包含在主線內核中),是一個針對高性能數(shù)據(jù)包處理優(yōu)化的原始套接字,并允許內核和應用程序之間的零拷貝。由于套接字可用于接收和發(fā)送,因此它僅支持用戶空間中的高性能網(wǎng)絡應用。
當然零拷貝技術和數(shù)據(jù)拷貝的優(yōu)化一直是大家追求性能優(yōu)化的方式之一,相關技術也在不斷研究之中,歡迎在原文的評論中寫出你的看法。
##參考文章 以下文章是我整理的關于零拷貝技術一部分文章,如果你想深入了解零拷貝技術,可以閱讀這些更多的文章。
- https://www.zhihu.com/question/35093238?utm_id=0
- https://strikefreedom.top/archives/pipe-pool-for-splice-in-go
- https://www.modb.pro/db/212924
- https://blog.lpflpf.cn/passages/golang-zerocopy/
- https://medium.com/swlh/linux-zero-copy-using-sendfile-75d2eb56b39b
- https://www.cloudwego.io/zh/blog/2021/10/09/%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E5%9C%A8-go-%E7%BD%91%E7%BB%9C%E5%BA%93%E4%B8%8A%E7%9A%84%E5%AE%9E%E8%B7%B5/#zerocopy
- https://zhuanlan.zhihu.com/p/360343446
- https://blog.devgenius.io/linux-zero-copy-d61d712813fe
- https://www.kernel.org/doc/html/v4.18/networking/msg_zerocopy.html
- https://lwn.net/Articles/879724/
- https://www.phoronix.com/news/Linux-5.20-IO_uring-ZC-Send
- https://en.wikipedia.org/wiki/Zero-copy
- https://aijishu.com/a/1060000000149804
- https://github.com/golang/go/issues/48530
- https://juejin.cn/post/6863264864140935175
- https://www.linuxjournal.com/article/6345
- https://jishuin.proginn.com/p/763bfbd47570
參考資料
[1]read: https://man7.org/linux/man-pages/man2/read.2.html
[2]write: https://man7.org/linux/man-pages/man2/write.2.html
[3]sendfile: https://man7.org/linux/man-pages/man2/sendfile.2.html
[4]mmap: https://man7.org/linux/man-pages/man2/mmap.2.html
[5]splice: https://man7.org/linux/man-pages/man2/splice.2.html
[6]MSG_ZEROCOPY: https://www.kernel.org/doc/html/v4.17/networking/msg_zerocopy.html
[7]Zero-copy networking: https://lwn.net/Articles/726917/
[8]Zero-copy TCP receive: https://lwn.net/Articles/752188/
[9]字節(jié)跳動在 Go 網(wǎng)絡庫上的實踐: https://www.cloudwego.io/zh/blog/2021/10/09/%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E5%9C%A8-go-%E7%BD%91%E7%BB%9C%E5%BA%93%E4%B8%8A%E7%9A%84%E5%AE%9E%E8%B7%B5/
[10]copy_file_range: https://man7.org/linux/man-pages/man2/copy_file_range.2.html
[11]AF_XDP: https://lwn.net/Articles/750845/
往期推薦
「每周譯Go」理解?Go?中包的可見性
想要了解Go更多內容,歡迎掃描下方??關注公眾號,回復關鍵詞 [實戰(zhàn)群]? ,就有機會進群和我們進行交流
分享、在看與點贊Go?




