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

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

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

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

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

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

其它
AF_XDP[11]是Linux 4.18新增加的功能,以前稱為AF_PACKETv4(從未包含在主線內(nèi)核中),是一個(gè)針對(duì)高性能數(shù)據(jù)包處理優(yōu)化的原始套接字,并允許內(nèi)核和應(yīng)用程序之間的零拷貝。由于套接字可用于接收和發(fā)送,因此它僅支持用戶空間中的高性能網(wǎng)絡(luò)應(yīng)用。
當(dāng)然零拷貝技術(shù)和數(shù)據(jù)拷貝的優(yōu)化一直是大家追求性能優(yōu)化的方式之一,相關(guān)技術(shù)也在不斷研究之中,歡迎在原文的評(píng)論中寫出你的看法。
##參考文章 以下文章是我整理的關(guān)于零拷貝技術(shù)一部分文章,如果你想深入了解零拷貝技術(shù),可以閱讀這些更多的文章。
- 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é)跳動(dòng)在 Go 網(wǎng)絡(luò)庫(kù)上的實(shí)踐: 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更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號(hào),回復(fù)關(guān)鍵詞 [實(shí)戰(zhàn)群]? ,就有機(jī)會(huì)進(jìn)群和我們進(jìn)行交流
分享、在看與點(diǎn)贊Go?




