1. 原來這才是零拷貝!

        共 6607字,需瀏覽 14分鐘

         ·

        2021-12-27 02:12

        零拷貝是老生常談的問題啦,大廠非常喜歡問。比如Kafka為什么快,RocketMQ為什么快等,都涉及到零拷貝知識(shí)點(diǎn)。最近技術(shù)討論群幾個(gè)伙伴分享了阿里、蝦皮的面試真題,也都涉及到零拷貝。因此本文將跟大家一起來學(xué)習(xí)零拷貝原理。

        1. 什么是零拷貝
        2. 傳統(tǒng)的IO執(zhí)行流程
        3. 零拷貝相關(guān)的知識(shí)點(diǎn)回顧
        4. 零拷貝實(shí)現(xiàn)的幾種方式
        5. java提供的零拷貝方式

        1.什么是零拷貝

        零拷貝字面上的意思包括兩個(gè),“零”和“拷貝”:

        • “拷貝”:就是指數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域轉(zhuǎn)移到另一個(gè)存儲(chǔ)區(qū)域。
        • “零” :表示次數(shù)為0,它表示拷貝數(shù)據(jù)的次數(shù)為0。

        合起來,那零拷貝就是不需要將數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域復(fù)制到另一個(gè)存儲(chǔ)區(qū)域咯。

        零拷貝是指計(jì)算機(jī)執(zhí)行IO操作時(shí),CPU不需要將數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域復(fù)制到另一個(gè)存儲(chǔ)區(qū)域,從而可以減少上下文切換以及CPU的拷貝時(shí)間。它是一種I/O操作優(yōu)化技術(shù)。

        2. 傳統(tǒng) IO 的執(zhí)行流程

        做服務(wù)端開發(fā)的小伙伴,文件下載功能應(yīng)該實(shí)現(xiàn)過不少了吧。如果你實(shí)現(xiàn)的是一個(gè)web程序,前端請(qǐng)求過來,服務(wù)端的任務(wù)就是:將服務(wù)端主機(jī)磁盤中的文件從已連接的socket發(fā)出去。關(guān)鍵實(shí)現(xiàn)代碼如下:

        while((n?=?read(diskfd,?buf,?BUF_SIZE))?>?0)
        ????write(sockfd,?buf?,?n);

        傳統(tǒng)的IO流程,包括read和write的過程。

        • read:把數(shù)據(jù)從磁盤讀取到內(nèi)核緩沖區(qū),再拷貝到用戶緩沖區(qū)
        • write:先把數(shù)據(jù)寫入到socket緩沖區(qū),最后寫入網(wǎng)卡設(shè)備。

        流程圖如下:

        • 用戶應(yīng)用進(jìn)程調(diào)用read函數(shù),向操作系統(tǒng)發(fā)起IO調(diào)用,上下文從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)(切換1)
        • DMA控制器把數(shù)據(jù)從磁盤中,讀取到內(nèi)核緩沖區(qū)。
        • CPU把內(nèi)核緩沖區(qū)數(shù)據(jù),拷貝到用戶應(yīng)用緩沖區(qū),上下文從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)(切換2),read函數(shù)返回
        • 用戶應(yīng)用進(jìn)程通過write函數(shù),發(fā)起IO調(diào)用,上下文從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)(切換3)
        • CPU將用戶緩沖區(qū)中的數(shù)據(jù),拷貝到socket緩沖區(qū)
        • DMA控制器把數(shù)據(jù)從socket緩沖區(qū),拷貝到網(wǎng)卡設(shè)備,上下文從內(nèi)核態(tài)切換回用戶態(tài)(切換4),write函數(shù)返回

        從流程圖可以看出,傳統(tǒng)IO的讀寫流程,包括了4次上下文切換(4次用戶態(tài)和內(nèi)核態(tài)的切換),4次數(shù)據(jù)拷貝(兩次CPU拷貝以及兩次的DMA拷貝),什么是DMA拷貝呢?我們一起來回顧下,零拷貝涉及的操作系統(tǒng)知識(shí)點(diǎn)哈。

        3. 零拷貝相關(guān)的知識(shí)點(diǎn)回顧

        3.1 內(nèi)核空間和用戶空間

        我們電腦上跑著的應(yīng)用程序,其實(shí)是需要經(jīng)過操作系統(tǒng),才能做一些特殊操作,如磁盤文件讀寫、內(nèi)存的讀寫等等。因?yàn)檫@些都是比較危險(xiǎn)的操作,不可以由應(yīng)用程序亂來,只能交給底層操作系統(tǒng)來。

        因此,操作系統(tǒng)為每個(gè)進(jìn)程都分配了內(nèi)存空間,一部分是用戶空間,一部分是內(nèi)核空間。內(nèi)核空間是操作系統(tǒng)內(nèi)核訪問的區(qū)域,是受保護(hù)的內(nèi)存空間,而用戶空間是用戶應(yīng)用程序訪問的內(nèi)存區(qū)域。 以32位操作系統(tǒng)為例,它會(huì)為每一個(gè)進(jìn)程都分配了4G(2的32次方)的內(nèi)存空間。

        • 內(nèi)核空間:主要提供進(jìn)程調(diào)度、內(nèi)存分配、連接硬件資源等功能
        • 用戶空間:提供給各個(gè)程序進(jìn)程的空間,它不具有訪問內(nèi)核空間資源的權(quán)限,如果應(yīng)用程序需要使用到內(nèi)核空間的資源,則需要通過系統(tǒng)調(diào)用來完成。進(jìn)程從用戶空間切換到內(nèi)核空間,完成相關(guān)操作后,再從內(nèi)核空間切換回用戶空間。

        3.2 什么是用戶態(tài)、內(nèi)核態(tài)

        • 如果進(jìn)程運(yùn)行于內(nèi)核空間,被稱為進(jìn)程的內(nèi)核態(tài)
        • 如果進(jìn)程運(yùn)行于用戶空間,被稱為進(jìn)程的用戶態(tài)。

        3.3 什么是上下文切換

        • 什么是CPU上下文?

        CPU 寄存器,是CPU內(nèi)置的容量小、但速度極快的內(nèi)存。而程序計(jì)數(shù)器,則是用來存儲(chǔ) CPU 正在執(zhí)行的指令位置、或者即將執(zhí)行的下一條指令位置。它們都是 CPU 在運(yùn)行任何任務(wù)前,必須的依賴環(huán)境,因此叫做CPU上下文。

        • 什么是CPU上下文切換?

        它是指,先把前一個(gè)任務(wù)的CPU上下文(也就是CPU寄存器和程序計(jì)數(shù)器)保存起來,然后加載新任務(wù)的上下文到這些寄存器和程序計(jì)數(shù)器,最后再跳轉(zhuǎn)到程序計(jì)數(shù)器所指的新位置,運(yùn)行新任務(wù)。

        一般我們說的上下文切換,就是指內(nèi)核(操作系統(tǒng)的核心)在CPU上對(duì)進(jìn)程或者線程進(jìn)行切換。進(jìn)程從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)變,需要通過系統(tǒng)調(diào)用來完成。系統(tǒng)調(diào)用的過程,會(huì)發(fā)生CPU上下文的切換。

        CPU 寄存器里原來用戶態(tài)的指令位置,需要先保存起來。接著,為了執(zhí)行內(nèi)核態(tài)代碼,CPU 寄存器需要更新為內(nèi)核態(tài)指令的新位置。最后才是跳轉(zhuǎn)到內(nèi)核態(tài)運(yùn)行內(nèi)核任務(wù)。

        3.4 虛擬內(nèi)存

        現(xiàn)代操作系統(tǒng)使用虛擬內(nèi)存,即虛擬地址取代物理地址,使用虛擬內(nèi)存可以有2個(gè)好處:

        • 虛擬內(nèi)存空間可以遠(yuǎn)遠(yuǎn)大于物理內(nèi)存空間
        • 多個(gè)虛擬內(nèi)存可以指向同一個(gè)物理地址

        正是多個(gè)虛擬內(nèi)存可以指向同一個(gè)物理地址,可以把內(nèi)核空間和用戶空間的虛擬地址映射到同一個(gè)物理地址,這樣的話,就可以減少IO的數(shù)據(jù)拷貝次數(shù)啦,示意圖如下

        3.5 DMA技術(shù)

        DMA,英文全稱是Direct Memory Access,即直接內(nèi)存訪問。DMA本質(zhì)上是一塊主板上獨(dú)立的芯片,允許外設(shè)設(shè)備和內(nèi)存存儲(chǔ)器之間直接進(jìn)行IO數(shù)據(jù)傳輸,其過程不需要CPU的參與。

        我們一起來看下IO流程,DMA幫忙做了什么事情.

        • 用戶應(yīng)用進(jìn)程調(diào)用read函數(shù),向操作系統(tǒng)發(fā)起IO調(diào)用,進(jìn)入阻塞狀態(tài),等待數(shù)據(jù)返回。
        • CPU收到指令后,對(duì)DMA控制器發(fā)起指令調(diào)度。
        • DMA收到IO請(qǐng)求后,將請(qǐng)求發(fā)送給磁盤;
        • 磁盤將數(shù)據(jù)放入磁盤控制緩沖區(qū),并通知DMA
        • DMA將數(shù)據(jù)從磁盤控制器緩沖區(qū)拷貝到內(nèi)核緩沖區(qū)。
        • DMA向CPU發(fā)出數(shù)據(jù)讀完的信號(hào),把工作交換給CPU,由CPU負(fù)責(zé)將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)。
        • 用戶應(yīng)用進(jìn)程由內(nèi)核態(tài)切換回用戶態(tài),解除阻塞狀態(tài)

        可以發(fā)現(xiàn),DMA做的事情很清晰啦,它主要就是幫忙CPU轉(zhuǎn)發(fā)一下IO請(qǐng)求,以及拷貝數(shù)據(jù)。為什么需要它的?

        主要就是效率,它幫忙CPU做事情,這時(shí)候,CPU就可以閑下來去做別的事情,提高了CPU的利用效率。大白話解釋就是,CPU老哥太忙太累啦,所以他找了個(gè)小弟(名叫DMA) ,替他完成一部分的拷貝工作,這樣CPU老哥就能著手去做其他事情。

        4. 零拷貝實(shí)現(xiàn)的幾種方式

        零拷貝并不是沒有拷貝數(shù)據(jù),而是減少用戶態(tài)/內(nèi)核態(tài)的切換次數(shù)以及CPU拷貝的次數(shù)。零拷貝實(shí)現(xiàn)有多種方式,分別是

        • mmap+write
        • sendfile
        • 帶有DMA收集拷貝功能的sendfile

        4.1 mmap+write實(shí)現(xiàn)的零拷貝

        mmap 的函數(shù)原型如下:

        void?*mmap(void?*addr,?size_t?length,?int?prot,?int?flags,?int?fd,?off_t?offset);
        • addr:指定映射的虛擬內(nèi)存地址
        • length:映射的長(zhǎng)度
        • prot:映射內(nèi)存的保護(hù)模式
        • flags:指定映射的類型
        • fd:進(jìn)行映射的文件句柄
        • offset:文件偏移量

        前面一小節(jié),零拷貝相關(guān)的知識(shí)點(diǎn)回顧,我們介紹了虛擬內(nèi)存,可以把內(nèi)核空間和用戶空間的虛擬地址映射到同一個(gè)物理地址,從而減少數(shù)據(jù)拷貝次數(shù)!mmap就是用了虛擬內(nèi)存這個(gè)特點(diǎn),它將內(nèi)核中的讀緩沖區(qū)與用戶空間的緩沖區(qū)進(jìn)行映射,所有的IO都在內(nèi)核中完成。

        mmap+write實(shí)現(xiàn)的零拷貝流程如下:

        • 用戶進(jìn)程通過mmap方法向操作系統(tǒng)內(nèi)核發(fā)起IO調(diào)用,上下文從用戶態(tài)切換為內(nèi)核態(tài)。
        • CPU利用DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。
        • 上下文從內(nèi)核態(tài)切換回用戶態(tài),mmap方法返回。
        • 用戶進(jìn)程通過write方法向操作系統(tǒng)內(nèi)核發(fā)起IO調(diào)用,上下文從用戶態(tài)切換為內(nèi)核態(tài)。
        • CPU將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到的socket緩沖區(qū)。
        • CPU利用DMA控制器,把數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡,上下文從內(nèi)核態(tài)切換回用戶態(tài),write調(diào)用返回。

        可以發(fā)現(xiàn),mmap+write實(shí)現(xiàn)的零拷貝,I/O發(fā)生了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中,包括了2次DMA拷貝和1次CPU拷貝

        mmap是將讀緩沖區(qū)的地址和用戶緩沖區(qū)的地址進(jìn)行映射,內(nèi)核緩沖區(qū)和應(yīng)用緩沖區(qū)共享,所以節(jié)省了一次CPU拷貝‘’并且用戶進(jìn)程內(nèi)存是虛擬的,只是映射到內(nèi)核的讀緩沖區(qū),可以節(jié)省一半的內(nèi)存空間。

        4.2 sendfile實(shí)現(xiàn)的零拷貝

        sendfile是Linux2.1內(nèi)核版本后引入的一個(gè)系統(tǒng)調(diào)用函數(shù),API如下:

        ssize_t?sendfile(int?out_fd,?int?in_fd,?off_t?*offset,?size_t?count);
        • out_fd:為待寫入內(nèi)容的文件描述符,一個(gè)socket描述符。,
        • in_fd:為待讀出內(nèi)容的文件描述符,必須是真實(shí)的文件,不能是socket和管道。
        • offset:指定從讀入文件的哪個(gè)位置開始讀,如果為NULL,表示文件的默認(rèn)起始位置。
        • count:指定在fdout和fdin之間傳輸?shù)淖止?jié)數(shù)。

        sendfile表示在兩個(gè)文件描述符之間傳輸數(shù)據(jù),它是在操作系統(tǒng)內(nèi)核中操作的,避免了數(shù)據(jù)從內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的拷貝操作,因此可以使用它來實(shí)現(xiàn)零拷貝。

        sendfile實(shí)現(xiàn)的零拷貝流程如下:

        sendfile實(shí)現(xiàn)的零拷貝
        1. 用戶進(jìn)程發(fā)起sendfile系統(tǒng)調(diào)用,上下文(切換1)從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài)
        2. DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。
        3. CPU將讀緩沖區(qū)中數(shù)據(jù)拷貝到socket緩沖區(qū)
        4. DMA控制器,異步把數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡,
        5. 上下文(切換2)從內(nèi)核態(tài)切換回用戶態(tài),sendfile調(diào)用返回。

        可以發(fā)現(xiàn),sendfile實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中,包括了2次DMA拷貝和1次CPU拷貝。那能不能把CPU拷貝的次數(shù)減少到0次呢?有的,即帶有DMA收集拷貝功能的sendfile

        4.3 sendfile+DMA scatter/gather實(shí)現(xiàn)的零拷貝

        linux 2.4版本之后,對(duì)sendfile做了優(yōu)化升級(jí),引入SG-DMA技術(shù),其實(shí)就是對(duì)DMA拷貝加入了scatter/gather操作,它可以直接從內(nèi)核空間緩沖區(qū)中將數(shù)據(jù)讀取到網(wǎng)卡。使用這個(gè)特點(diǎn)搞零拷貝,即還可以多省去一次CPU拷貝。

        sendfile+DMA scatter/gather實(shí)現(xiàn)的零拷貝流程如下:

        1. 用戶進(jìn)程發(fā)起sendfile系統(tǒng)調(diào)用,上下文(切換1)從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài)
        2. DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。
        3. CPU把內(nèi)核緩沖區(qū)中的文件描述符信息(包括內(nèi)核緩沖區(qū)的內(nèi)存地址和偏移量)發(fā)送到socket緩沖區(qū)
        4. DMA控制器根據(jù)文件描述符信息,直接把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到網(wǎng)卡
        5. 上下文(切換2)從內(nèi)核態(tài)切換回用戶態(tài),sendfile調(diào)用返回。

        可以發(fā)現(xiàn),sendfile+DMA scatter/gather實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及2次數(shù)據(jù)拷貝。其中2次數(shù)據(jù)拷貝都是包DMA拷貝。這就是真正的 零拷貝(Zero-copy) 技術(shù),全程都沒有通過CPU來搬運(yùn)數(shù)據(jù),所有的數(shù)據(jù)都是通過DMA來進(jìn)行傳輸?shù)摹?/p>

        5. java提供的零拷貝方式

        • Java NIO對(duì)mmap的支持
        • Java NIO對(duì)sendfile的支持

        5.1 Java NIO對(duì)mmap的支持

        Java NIO有一個(gè)MappedByteBuffer的類,可以用來實(shí)現(xiàn)內(nèi)存映射。它的底層是調(diào)用了Linux內(nèi)核的mmap的API。

        mmap的小demo如下:

        public?class?MmapTest?{

        ????public?static?void?main(String[]?args)?{
        ????????try?{
        ????????????FileChannel?readChannel?=?FileChannel.open(Paths.get("./jay.txt"),?StandardOpenOption.READ);
        ????????????MappedByteBuffer?data?=?readChannel.map(FileChannel.MapMode.READ_ONLY,?0,?1024?*?1024?*?40);
        ????????????FileChannel?writeChannel?=?FileChannel.open(Paths.get("./siting.txt"),?StandardOpenOption.WRITE,?StandardOpenOption.CREATE);
        ????????????//數(shù)據(jù)傳輸
        ????????????writeChannel.write(data);
        ????????????readChannel.close();
        ????????????writeChannel.close();
        ????????}catch?(Exception?e){
        ????????????System.out.println(e.getMessage());
        ????????}
        ????}
        }

        5.2 Java NIO對(duì)sendfile的支持

        FileChannel的transferTo()/transferFrom(),底層就是sendfile() 系統(tǒng)調(diào)用函數(shù)。Kafka 這個(gè)開源項(xiàng)目就用到它,平時(shí)面試的時(shí)候,回答面試官為什么這么快,就可以提到零拷貝sendfile這個(gè)點(diǎn)。

        @Override
        public?long?transferFrom(FileChannel?fileChannel,?long?position,?long?count)?throws?IOException?{
        ???return?fileChannel.transferTo(position,?count,?socketChannel);
        }

        sendfile的小demo如下:

        public?class?SendFileTest?{
        ????public?static?void?main(String[]?args)?{
        ????????try?{
        ????????????FileChannel?readChannel?=?FileChannel.open(Paths.get("./jay.txt"),?StandardOpenOption.READ);
        ????????????long?len?=?readChannel.size();
        ????????????long?position?=?readChannel.position();
        ????????????
        ????????????FileChannel?writeChannel?=?FileChannel.open(Paths.get("./siting.txt"),?StandardOpenOption.WRITE,?StandardOpenOption.CREATE);
        ????????????//數(shù)據(jù)傳輸
        ????????????readChannel.transferTo(position,?len,?writeChannel);
        ????????????readChannel.close();
        ????????????writeChannel.close();
        ????????}?catch?(Exception?e)?{
        ????????????System.out.println(e.getMessage());
        ????????}
        ????}
        }

        參考與感謝

        • 框架篇:小白也能秒懂的Linux零拷貝原理[1]
        • 深入剖析Linux IO原理和幾種零拷貝機(jī)制的實(shí)現(xiàn)[2]
        • 阿里二面:什么是mmap?

        [1]

        框架篇:小白也能秒懂的Linux零拷貝原理: https://juejin.cn/post/6887469050515947528

        [2]

        深入剖析Linux IO原理和幾種零拷貝機(jī)制的實(shí)現(xiàn): https://juejin.cn/post/6844903949359644680#heading-11


        求點(diǎn)贊、在看、分享
        瀏覽 33
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 日韩精品理论 | 看美国一级片 | 97久久久久 | 欧美精品成人一区二区在线观看 | 操我逼|