<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【性能】零拷貝

          共 7432字,需瀏覽 15分鐘

           ·

          2022-11-01 18:14

          零拷貝概念

          零拷貝就是一種避免 CPU 將數(shù)據(jù)從一塊存儲拷貝到另外一塊存儲的技術。針對操作系統(tǒng)中的設備驅(qū)動程序、文件系統(tǒng)以及網(wǎng)絡協(xié)議堆棧而出現(xiàn)的各種零拷貝技術極大地提升了特定應用程序的性能,并且使得這些應用程序可以更加有效地利用系統(tǒng)資源。這種性能的提升就是通過在數(shù)據(jù)拷貝進行的同時,允許 CPU 執(zhí)行其他的任務來實現(xiàn)的。

          零拷貝技術可以減少數(shù)據(jù)拷貝和共享總線操作的次數(shù),消除傳輸數(shù)據(jù)在存儲器之間不必要的中間拷貝次數(shù),從而有效地提高數(shù)據(jù)傳輸效率。而且,零拷貝技術減少了用戶應用程序地址空間和操作系統(tǒng)內(nèi)核地址空間之間因為上下文切換而帶來的開銷。進行大量的數(shù)據(jù)拷貝操作其實是一件簡單的任務,從操作系統(tǒng)的角度來說,如果 CPU 一直被占用著去執(zhí)行這項簡單的任務,那么這將會是很浪費資源的;如果有其他比較簡單的系統(tǒng)部件可以代勞這件事情,從而使得 CPU 解脫出來可以做別的事情,那么系統(tǒng)資源的利用則會更加有效。綜上所述,零拷貝技術的目標可以概括如下:

          1. 避免數(shù)據(jù)拷貝
            ①避免操作系統(tǒng)內(nèi)核緩沖區(qū)之間進行數(shù)據(jù)拷貝操作。
            ②避免操作系統(tǒng)內(nèi)核和用戶應用程序地址空間這兩者之間進行數(shù)據(jù)拷貝操作。
            ③用戶應用程序可以避開操作系統(tǒng)直接訪問硬件存儲。
            ④數(shù)據(jù)傳輸盡量讓 DMA 來做。

          2. 綜合目標
            ①避免不必要的系統(tǒng)調(diào)用和上下文切換。
            ②需要拷貝的數(shù)據(jù)可以先被緩存起來。
            ③對數(shù)據(jù)進行處理盡量讓硬件來做。

          需要注意,它不能用于實現(xiàn)了數(shù)據(jù)加密或者壓縮的文件系統(tǒng)上,只有傳輸文件的原始內(nèi)容。這類原始內(nèi)容也包括加密了的文件內(nèi)容。

           多次數(shù)據(jù)拷貝

          從上圖中可以看出,共產(chǎn)生了四次數(shù)據(jù)拷貝,即使使用了DMA來處理了與硬件的通訊,CPU仍然需要處理兩次數(shù)據(jù)拷貝,與此同時,在用戶態(tài)與內(nèi)核態(tài)也發(fā)生了多次上下文切換,無疑也加重了CPU負擔。
          在此過程中,我們沒有對文件內(nèi)容做任何修改,那么在內(nèi)核空間和用戶空間來回拷貝數(shù)據(jù)無疑就是一種浪費,而零拷貝主要就是為了解決這種低效性。

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

          比如想實現(xiàn)一個下載功能,服務端的任務就是:將服務器主機磁盤中的文件從已連接的socket中發(fā)出去,關鍵代碼如下:

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

          傳統(tǒng)的IO流程包括read以及write的過程

          1. read:將數(shù)據(jù)從磁盤讀取到內(nèi)核緩存區(qū)中,在拷貝到用戶緩沖區(qū)

          2. write:先將數(shù)據(jù)寫入到socket緩沖區(qū)中,最后寫入網(wǎng)卡設備

          流程圖如下:

          1.應用程序調(diào)用read函數(shù),向操作系統(tǒng)發(fā)起IO調(diào)用,上下文從用戶態(tài)切換至內(nèi)核態(tài)

          2.DMA控制器把數(shù)據(jù)從磁盤中讀取到內(nèi)核緩沖區(qū)

          3.CPU把內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到用戶應用緩沖區(qū),上下文從內(nèi)核態(tài)切換至用戶態(tài),此時read函數(shù)返回

          4.用戶應用進程通過write函數(shù),發(fā)起IO調(diào)用,上下文從用戶態(tài)切換至內(nèi)核態(tài)

          5.CPU將緩沖區(qū)的數(shù)據(jù)拷貝到socket緩沖區(qū)

          6.DMA控制器將數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡設備,上下文從內(nèi)核態(tài)切換至用戶態(tài),此時write函數(shù)返回

          從流程圖中可以看出傳統(tǒng)的IO流程包括***4次上下文的切換***,4次拷貝數(shù)據(jù)(兩次CPU拷貝以及兩次DMA拷貝)

          DMA技術

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

          簡單的說它就是幫住CPU轉發(fā)一下IO請求以及拷貝數(shù)據(jù),那為什么需要它呢?其實主要是效率問題。它幫忙CPU做事情,這時候,CPU就可以閑下來去做別的事情,提高了CPU的利用效率。大白話解釋就是,CPU老哥太忙太累啦,所以他找了個小弟(名叫DMA) ,替他完成一部分的拷貝工作,這樣CPU老哥就能著手去做其他事情。

          下面看下DMA具體是做了哪些工作

          1.用戶應用程序調(diào)read函數(shù),向操作系統(tǒng)發(fā)起IO調(diào)用,進入阻塞狀態(tài)等待數(shù)據(jù)返回。

          2.CPU接到指令后,對DMA控制器發(fā)起指令調(diào)度。

          3.DMA收到請求后,將請求發(fā)送給磁盤。

          4.磁盤將數(shù)據(jù)放入磁盤控制緩沖區(qū)并通知DMA。

          5.DMA將數(shù)據(jù)從磁盤控制器緩沖區(qū)拷貝到內(nèi)核緩沖區(qū)。

          6.DMA向CPU發(fā)送數(shù)據(jù)讀完的信號,CPU負責將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)。

          7.用戶應用進程由內(nèi)核態(tài)切回用戶態(tài),解除阻塞狀態(tài)。

          java提供的零拷貝方式

          mmap

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

          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()); } }}

          使用mmap替代read很明顯減少了一次拷貝,當拷貝數(shù)據(jù)量很大時,無疑提升了效率。但是使用mmap是有代價的。當你使用mmap時,你可能會遇到一些隱藏的陷阱。例如,當你的程序map了一個文件,但是當這個文件被另一個進程截斷(truncate)時, write系統(tǒng)調(diào)用會因為訪問非法地址而被SIGBUS信號終止。SIGBUS信號默認會殺死你的進程并產(chǎn)生一個coredump,如果你的服務器這樣被中止了,那會產(chǎn)生一筆損失。
          通常我們使用以下解決方案避免這種問題:

          1. 為SIGBUS信號建立信號處理程序
            當遇到SIGBUS信號時,信號處理程序簡單地返回,write系統(tǒng)調(diào)用在被中斷之前會返回已經(jīng)寫入的字節(jié)數(shù),并且errno會被設置成success,但是這是一種糟糕的處理辦法,因為你并沒有解決問題的實質(zhì)核心。

          2. 使用文件租借鎖
            通常我們使用這種方法,在文件描述符上使用租借鎖,我們?yōu)槲募騼?nèi)核申請一個租借鎖,當其它進程想要截斷這個文件時,內(nèi)核會向我們發(fā)送一個實時的RT_SIGNAL_LEASE信號,告訴我們內(nèi)核正在破壞你加持在文件上的讀寫鎖。這樣在程序訪問非法內(nèi)存并且被SIGBUS殺死之前,你的write系統(tǒng)調(diào)用會被中斷。write會返回已經(jīng)寫入的字節(jié)數(shù),并且置errno為success。
            我們應該在mmap文件之前加鎖,并且在操作完文件后解鎖

            if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
            perror("kernel lease set signal");
            return -1;
            }
            /* l_type can be F_RDLCK F_WRLCK 加鎖*/
            /* l_type can be F_UNLCK 解鎖*/
            if(fcntl(diskfd, F_SETLEASE, l_type)){
            perror("kernel lease set type");
            return -1;
            }

          sendfile

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

          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());        }    }}

          從2.1版內(nèi)核開始,Linux引入了sendfile來簡化操作:

          #include<sys/sendfile.h>
          ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

          系統(tǒng)調(diào)用sendfile()在代表輸入文件的描述符in_fd和代表輸出文件的描述符out_fd之間傳送文件內(nèi)容(字節(jié))。描述符out_fd必須指向一個套接字,而in_fd指向的文件必須是可以mmap的。這些局限限制了sendfile的使用,使sendfile只能將數(shù)據(jù)從文件傳遞到套接字上,反之則不行。使用sendfile不僅減少了數(shù)據(jù)拷貝的次數(shù),還減少了上下文切換,數(shù)據(jù)傳送始終只發(fā)生在kernel space。

          在我們調(diào)用sendfile時,如果有其它進程截斷了文件會發(fā)生什么呢?假設我們沒有設置任何信號處理程序,sendfile調(diào)用僅僅返回它在被中斷之前已經(jīng)傳輸?shù)淖止?jié)數(shù),errno會被置為success。如果我們在調(diào)用sendfile之前給文件加了鎖,sendfile的行為仍然和之前相同,我們還會收到RT_SIGNAL_LEASE的信號。
          目前為止,我們已經(jīng)減少了數(shù)據(jù)拷貝的次數(shù)了,但是仍然存在一次拷貝,就是頁緩存到socket緩存的拷貝。那么能不能把這個拷貝也省略呢?
          借助于硬件上的幫助,我們是可以辦到的。之前我們是把頁緩存的數(shù)據(jù)拷貝到socket緩存中,實際上,我們僅僅需要把緩沖區(qū)描述符傳到socket緩沖區(qū),再把數(shù)據(jù)長度傳過去,這樣DMA控制器直接將頁緩存中的數(shù)據(jù)打包發(fā)送到網(wǎng)絡中就可以了。
          總結一下,sendfile系統(tǒng)調(diào)用利用DMA引擎將文件內(nèi)容拷貝到內(nèi)核緩沖區(qū)去,然后將帶有文件位置和長度信息的緩沖區(qū)描述符添加socket緩沖區(qū)去,這一步不會將內(nèi)核中的數(shù)據(jù)拷貝到socket緩沖區(qū)中,DMA引擎會將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到協(xié)議引擎中去,避免了最后一次拷貝。不過這一種收集拷貝功能是需要硬件以及驅(qū)動程序支持的。

          無論是傳統(tǒng)的 I/O 方式,還是引入了零拷貝之后,2 次 DMA copy是都少不了的。因為兩次 DMA 都是依賴硬件完成的。所以,所謂的零拷貝,都是為了減少 CPU copy 及減少了上下文的切換。

          下圖展示了各種零拷貝技術的對比圖:

          Netty 的零拷貝

          主要包含三個方面:

          (1)Netty 的接收和發(fā)送 ByteBuffer 采用 DIRECT BUFFERS ,使用堆外直接內(nèi)存進行 Socket 讀寫,不需要進行字節(jié)緩沖區(qū)的二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存 ( HEAP BUFFERS)進行 Socket 讀寫, JVM 會將堆內(nèi)存 Buffer 拷貝一份到直接內(nèi) 存中,然后才寫入 Socket 中。相比于堆外直接內(nèi)存,消息在發(fā)送過程中多了一次緩 沖區(qū)的內(nèi)存拷貝。

          (2)Netty 提供了組合 Buffer 對象,可以聚合多個 ByteBuffer 對象,用戶可以像操作一個 Buffer 那樣方便的對組合 Buffer 進行操作,避免了傳統(tǒng)通過內(nèi)存拷貝的方式 將幾個 小 Buffer 合并成一個大的 Buffer 。

          (3)Netty 的文件傳輸采用了 transferTo 方法,它可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標 Channel ,避免了傳統(tǒng)通過循環(huán) write 方式導致的內(nèi)存拷貝問題。

          零拷貝機制是Netty高性能的一個原因,之前都是說netty的線程模型,責任鏈,說說netty底層的優(yōu)化,優(yōu)化就是netty自己的一個緩沖區(qū)。

          (一)Netty自己的ByteBuf

          介紹

          ByteBuf 是為解決 ByteBuffer的問題和滿足網(wǎng)絡應用程序開發(fā)人員的日常需求而設計的。

          對比JDK byteBuffer的缺點

          無法動態(tài)擴容

          長度是固定的,不能動態(tài)擴展和收縮,當數(shù)據(jù)大于ByteBuffer容量時,會發(fā)生索引越界異常。

          API 使用復雜

          讀寫的時候需要手工調(diào)用flip()和rewind()等方法,使用時需要非常謹慎的考慮這些API,否則容出現(xiàn)錯誤。

          Netty的ByteBuf 操作

          ByteBuf三個重要屬性:capacity容量,readerIndex讀取位置,writerIndex 寫入位置。提供了兩個指針變量來支持順序和寫操作,分別是讀操作readerIndex 和寫操作writeIndex。

          常見的方法定義

          • 隨機訪問索引 getByte

          • 順序讀 read*

          • 順序?qū)?write*

          • 清除已讀內(nèi)容discardReadBytes

          • 清除緩沖區(qū) clear

          • 搜索操作

          • 標記和重置

          • 引用計數(shù)和釋放

          緩沖區(qū)是如何被兩個指針分割成三個區(qū)域的

          • discardable bytes 已讀可丟棄區(qū)域

          • readable bytes 可讀區(qū)域

          • writable bytes 待寫區(qū)域

          ByteBuf 動態(tài)擴容

          capacity 默認值:256字節(jié),最大值:Integer.MAX_VALUE(2GB)

          write 方法調(diào)用時,通過AbstractByteBuf.ensureWritable進行檢查。

          容量計算方法:AbstractByteBufAllocator.calculateNewCapacity(新capacity的最小要求,capacity最大值)

          根據(jù)新的capacity的最小值要求,對應有兩套計算方法

          沒超過4兆:從64字節(jié)開發(fā),每次增加一倍,直至計算出來的newCapacity滿足新容量最小要求。示例:當前大小256,已寫250,繼續(xù)寫10字節(jié)數(shù)據(jù),需要的容量最小要求是261,則新容量是6422*2=512

          超過4兆:新容量 = 新容量最小要求/4兆 * 4兆 +4兆

          示例:當前大小3兆,已寫3兆,繼續(xù)寫2兆數(shù)據(jù),需要的容量最小要求是5兆, 則新容量是9兆(不能超過最大值)

          選擇合適的 ByteBuf 實現(xiàn)

          在實際使用中都是通過 ByteBufAllocator 分配器進行申請,同時分配器具有內(nèi)存管理的功能。

          • unsafe 用到了 Unsafe 工具類,Unsafe 是 Java 保留的一個底層工具包,safe 則沒有用到 unsafe 工具類。

          • unsafe 意味著不安全的操作,但是更底層的操作會帶來性能提升和特殊功能,Netty 中會盡力使用 unsafe。

          • Java 語言很重要的特性是“一次編寫導出運行”,所以它針對底層的內(nèi)存或其他操作,做了很多封裝。而 unsafe 提供了一系列操作底層的方法,可能會導致不兼容或者不可知的異常。

          • unpool 每次申請緩沖區(qū)時會新建一個,并不會復用,使用 Unpooled 工具類可以創(chuàng)建 unpool 的緩沖區(qū)。

          • Netty 沒有給出很便捷的 pool 類型的緩沖區(qū)的創(chuàng)建方法。使用 ChannelConfig.getAllocator() 時,獲取到的分配器是默認支持內(nèi)存復用的。

          • pooledByteBuf對象、內(nèi)存

          • PoolThreadCache: PooledByteBufAllocator 實例維護了一個線程變量。

          • 多種分類的MemoryRegionCache數(shù)組用作內(nèi)存緩存,MemoryRegionCache內(nèi)部是鏈表,隊列里面存Chunk。

          • Pool Chunk里面維護了內(nèi)存引用,內(nèi)存復用的做法就是把buf的memory指向Chunck的memory。


          Netty 的零拷貝機制,是一種應用層的實現(xiàn)。

          拷貝方式

          一般的數(shù)組合并,會創(chuàng)建一個大的數(shù)組,然后將需要合并的數(shù)組放進去。

          Netty 的 CompositeButyBuf 將多個 ByteBuf 合并為一個邏輯上的 ByteBuf,避免了各個 ByteBuf 之間的拷貝。

          wrappedBuffer 方法將 byte[] 數(shù)組包裝成 ByteBuf 對象

          slice 方法將一個 ByteBuf 對象切分成多個 ByteBuf 對象

          實例

          PS:API操作便捷性,動態(tài)擴容,多種ByteBuf實現(xiàn),高效的零拷貝機制(邏輯上邊的設計)上邊的所有就是nettyByteBuf所做的工作,性能提升,操作性增強。


          瀏覽 50
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲三级无码视频 | 国产最新在线 | 亚洲爆乳一区二区三区 | 成人做爰A片免费看网站找不到了 | 性爱偷情短视频在线播放 |