1. 零拷貝技術(shù)第一篇:綜述

        共 8186字,需瀏覽 17分鐘

         ·

        2022-11-24 16:34

        零拷貝(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ù)拷貝情況:

        8d936476617c75aed6ab839ccbe0255a.webp


        程序開始使用系統(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ù)拷貝。

        d0b8c9ba484f54635912b489b544e334.webp

        通過(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ā)送文件。

        593a8572fe315ad8c30237c43e5f55c5.webp

        用戶調(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拷貝。

        5b0267d48d4c6af1b5e050e546cddeb8.webp

        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è)是就行。

        69856b9023374970d5da728e448df5f1.webp

        當(dāng)然,如果我們處理的源和目的不是管道的話,我們可以先建立一個(gè)管道,這樣就可以使用splice系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)零拷貝了。

        但是,如果每次都創(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ì)提升性能。

        3fe683de8974f9a975e6f864b5155da7.webp


        其它

        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ù),可以閱讀這些更多的文章。

        1. https://www.zhihu.com/question/35093238?utm_id=0
        2. https://strikefreedom.top/archives/pipe-pool-for-splice-in-go
        3. https://www.modb.pro/db/212924
        4. https://blog.lpflpf.cn/passages/golang-zerocopy/
        5. https://medium.com/swlh/linux-zero-copy-using-sendfile-75d2eb56b39b
        6. 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
        7. https://zhuanlan.zhihu.com/p/360343446
        8. https://blog.devgenius.io/linux-zero-copy-d61d712813fe
        9. https://www.kernel.org/doc/html/v4.18/networking/msg_zerocopy.html
        10. https://lwn.net/Articles/879724/
        11. https://www.phoronix.com/news/Linux-5.20-IO_uring-ZC-Send
        12. https://en.wikipedia.org/wiki/Zero-copy
        13. https://aijishu.com/a/1060000000149804
        14. https://github.com/golang/go/issues/48530
        15. https://juejin.cn/post/6863264864140935175
        16. https://www.linuxjournal.com/article/6345
        17. 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/


        往期推薦



        c6dc1e483bd015b4c743cd477e570370.webp

        2022 GopherChina大會(huì)緊急通知!

        3edb152d4863795af671a18eb45627ca.webp

        Go中的HTTP資源泄露之謎


        8271854349193cd21202711b543a9ef8.webp

        「每周譯Go」理解?Go?中包的可見性

        想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號(hào),回復(fù)關(guān)鍵詞 [實(shí)戰(zhàn)群]? ,就有機(jī)會(huì)進(jìn)群和我們進(jìn)行交流

        分享、在看與點(diǎn)贊Go? 380de137162ee7a880ed1e303cd95ebc.webp

        瀏覽 81
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 扒下小娇妻的内裤打屁股 | 成人免费做爱 | 日韩少妇精品Av一区二区 | 偷拍综合网 | 亚洲色图欧美视频 |