1. 網(wǎng)上關于“零拷貝”原理相關的文章滿天飛,但你知道如何使用零拷貝嗎?

        共 3764字,需瀏覽 8分鐘

         ·

        2021-11-19 15:20

        ,


        零拷貝是中間件相關面試中必考題,本文就和大家一起來總結(jié)一下NIO拷貝的原理,并結(jié)合Netty代碼,從代碼實現(xiàn)層面近距離觀摩如何使用java實現(xiàn)零拷貝。

        1、零拷貝實現(xiàn)原理

        **“零拷貝”**其實包括兩個層面的含義:

        • 拷貝 一份相同的數(shù)據(jù)從一個地方移動到另外一個地方的過程,叫拷貝。
        • 零 希望在IO讀寫過程中,CPU控制的數(shù)據(jù)拷貝到次數(shù)為0。

        在IO編程領域,當然是拷貝的次數(shù)越少越好,逐步優(yōu)化,將其拷貝次數(shù)將為0,最大化的提高性能。

        那接下來我們循序漸進來看一下如何減少數(shù)據(jù)復制。

        接下來我們將以RocketMQ消息發(fā)送、消息讀取場景來闡述IO讀寫過程中可能需要進行的數(shù)據(jù)復制與上下文切換。

        1.1 傳統(tǒng)的IO讀流程

        一次傳統(tǒng)的IO讀序列流程如下所示:java應用中,如果要將從文件中讀取數(shù)據(jù),其基本的流程如下所示:

        1. 當broker收到拉取請求時發(fā)起一次read系統(tǒng)調(diào)用,此時操作系統(tǒng)會進行一次上下文的切換,從用態(tài)間切換到內(nèi)核態(tài)。
        2. 通過直接存儲訪問器(DMA)從磁盤將數(shù)據(jù)加載到內(nèi)核緩存區(qū)DMA Copy,這個階段不需要CPU參與,如果是阻塞型IO,該過程用戶線程會處于阻塞狀態(tài))
        3. 然后在CPU的控制下,將內(nèi)核緩存區(qū)的數(shù)據(jù)copy到用戶空間的緩存區(qū)(由于這個是操作系統(tǒng)級別的行為,通常這里指的內(nèi)存緩存區(qū),通常使用的是堆外內(nèi)存),這里將發(fā)生一次CPU復制與一次上下文切換(從內(nèi)核態(tài)切換到用戶態(tài))
        4. 堆外內(nèi)存中的數(shù)據(jù)復制到應用程序的堆內(nèi)存,供應用程序使用,本次復制需要經(jīng)過CPU控制。
        5. 將數(shù)據(jù)加載到堆空間,需要傳輸?shù)骄W(wǎng)卡,這個過程又要進入到內(nèi)核空間,然后復制到sockebuffer,然后進入網(wǎng)卡協(xié)議引擎,從而進入到網(wǎng)絡傳輸中。該部分會在接下來會詳細介紹。

        溫馨提示:RocketMQ底層的工作機制并不是上述模型,是經(jīng)過優(yōu)化后的讀寫模型,本文將循序漸進的介紹優(yōu)化過程。

        1.2 傳統(tǒng)的IO寫流程

        一次傳統(tǒng)的IO寫入流程如下圖所示:核心關鍵步驟如下:

        1. 在broker收到消息時首先會在堆空間中創(chuàng)建一個堆緩存區(qū),用于存儲用戶需要寫入的數(shù)據(jù),然后需要將jvm堆內(nèi)存中數(shù)據(jù)復制到操作系統(tǒng)內(nèi)存(CPU COPY)
        2. 發(fā)起write系統(tǒng)調(diào)用,將用戶空間中的數(shù)據(jù)復制到內(nèi)存緩存區(qū),**此過程發(fā)生一次上下文切換(用戶態(tài)切換到內(nèi)核態(tài))**并進行一次CPU Copy。
        3. 通過直接存儲訪問器(DMA)將內(nèi)核空間的數(shù)據(jù)寫入到磁盤,并返回結(jié)果,此過程發(fā)生一次DMA Copy 與一次上下文切換(內(nèi)核態(tài)切換到用戶態(tài))

        1.3 讀寫優(yōu)化技巧

        從上面兩張流程圖,我們不能看出讀寫處理流程中存在太多復制,同樣的數(shù)據(jù)需要被復制多次,造成性能損耗,故IO讀寫通常的優(yōu)化方向主要為:減少復制次數(shù)、減少用戶態(tài)/內(nèi)核態(tài)切換次數(shù)。

        1.3.1 引入堆外內(nèi)存

        jvm堆空間中數(shù)據(jù)要發(fā)送到內(nèi)核緩存區(qū),通常需要先將jvm堆空間中的數(shù)據(jù)拷貝到系統(tǒng)內(nèi)存(一個非官方的理解,用C語言實現(xiàn)的本地方法調(diào)用中,首先需要將堆空間中數(shù)據(jù)拷貝到C語言相關的存儲結(jié)構(gòu)),故提高性能的第一個措施:使用堆外內(nèi)存。

        不過堆外內(nèi)存中的數(shù)據(jù),通常還是需要從堆空間中獲取,從這個角度來看,貌似提升的性能有限。

        1.3.2 引入內(nèi)存映射(MMap與write)

        通過引入內(nèi)存映射機制,減少用戶空間與內(nèi)核空間之間的數(shù)據(jù)復制,如下圖所示:內(nèi)存映射的核心思想就是將內(nèi)核緩存區(qū)、用戶空間緩存區(qū)映射到同一個物理地址上,可以減少用戶緩存區(qū)與內(nèi)核緩存區(qū)之間的數(shù)據(jù)拷貝。

        但由于內(nèi)存映射機制并不會減少上下文切換次數(shù)。

        1.3.3 大名鼎鼎鼎sendfile

        在Linux 2.1內(nèi)核引入了sendfile函數(shù)用于將文件通過socket傳送。

        注意sendfile的傳播方向:使用于將文件中的內(nèi)容直接傳播到Socket,通常使用客戶端從服務端文件中讀取數(shù)據(jù),在服務端內(nèi)部實現(xiàn)零拷貝。

        在1.3.1中介紹客戶端從服務端讀取消息的過程中,并沒有展開介紹從服務端寫入到客戶端網(wǎng)絡中的過程,接下來看看sendfile的數(shù)據(jù)拷貝圖解:sendfile的主要特點是在內(nèi)核空間中通過DMA將數(shù)據(jù)從磁盤文件拷貝到內(nèi)核緩存區(qū),然后可以直接將內(nèi)核緩存區(qū)中的數(shù)據(jù)在CPU控制下將數(shù)據(jù)復制到socket緩存區(qū),最終在DMA的控制下將socketbufer中拷貝到協(xié)議引擎,然后經(jīng)網(wǎng)卡傳輸?shù)侥繕硕恕?/p>

        sendfile的優(yōu)勢(特點):

        • 一次sendfile調(diào)用會只設計兩次上下文切換,比read+write減少兩次上下文切換。
        • 一次sendfile會存在3次copy,其中一次CPU拷貝,兩次DMA拷貝。
        1.3.4 Linux Gather

        Linux2.4內(nèi)核引入了gather機制,用以消除最后一次CPU拷貝,即不再將內(nèi)核緩存區(qū)中的數(shù)據(jù)拷貝到socketbuffer,而是將內(nèi)存緩存區(qū)中的內(nèi)存地址、需要讀取數(shù)據(jù)的長度寫入到socketbuffer中,然后DMA直接根據(jù)socketbuffer中存儲的內(nèi)存地址,直接從內(nèi)核緩存區(qū)中的數(shù)據(jù)拷貝到協(xié)議引擎(注意,這次拷貝由DMA控制)。

        從而實現(xiàn)真正的零拷貝。

        2、結(jié)合Netty談零拷貝實戰(zhàn)

        上面講述了“零拷貝”的實現(xiàn)原理,接下來將嘗試從Netty源碼去探究在代碼層面如何使用“零拷貝”。

        從網(wǎng)上的資料可以得知,在java nio提供的類庫中真正能運用底層操作系統(tǒng)的零拷貝機制只有FileChannel的transferTo,而在Netty中也不出意料的對這種方式進行了封裝,其類圖如下:其主要的核心要點是FileRegion的transferTo方法,我們結(jié)合該方法再來介紹DefaultFileRegion各個核心屬性的含義。上述代碼并不復雜,我們不難得出如下觀點:

        • 首先介紹DefaultFileRegion的核心屬性含義:
          • File f 底層抽取數(shù)據(jù)來源的底層磁盤文件
          • FileChannel file 底層文件的文件通道。
          • long position 數(shù)據(jù)從通道中抽取的起始位置
          • long count 需要傳遞的總字節(jié)數(shù)
          • long transfered 已傳遞的字節(jié)數(shù)量。
        • 核心要點是調(diào)用java nio FileChannel的transferTo方法,底層調(diào)用的是操作系統(tǒng)的sendfile函數(shù),即真正的零拷貝。
        • 調(diào)用一次transferTo方法并不一定能將需要的數(shù)據(jù)全部傳輸完成,故該方法返回已傳輸?shù)淖止?jié)數(shù),是否需要再次調(diào)用該方法的判斷方法:已傳遞的字節(jié)數(shù)是否等于需要傳遞的總字節(jié)數(shù)(transfered == count)

        接下來我們看一下FileRegion的transferTo在netty中的調(diào)用鏈,從而推斷一下Netty中的零拷貝的觸發(fā)要點。在Netty中代表兩個類型的通道:

        • EpollSocketChannel 基于Epoll機制進行事件的就緒選擇機制。

        • NioSocketChannel

          基于select機制的事件就緒選擇。

        在Netty中調(diào)用通道Channel的flush或writeAndFlush方法,都會最終觸發(fā)底層通道的網(wǎng)絡寫事件,如果待寫入的對象是FileRegion,則會觸發(fā)零拷貝機制,接下來我們對兩個簡單介紹一下:

        2.1 EpollSocketChannel 通道零拷貝

        寫入的入口函數(shù)為如下:核心思想為:如果待寫入的消息是DefaultFileRegion,EpollSocketChannel將直接調(diào)用sendfile函數(shù)進行數(shù)據(jù)傳遞;如果是FileRegion類型,則按照約定調(diào)用FileRegion的transferTo進行數(shù)據(jù)傳遞,這種方式是否真正進行零拷貝取決于FileRegion的transferTo中是否調(diào)用了FileChannel的transferTo方法。

        溫馨提示:本文并沒有打算詳細分析Epoll機制以及編程實踐。

        2.2 NioSocketChannel 通道零拷貝實現(xiàn)

        實現(xiàn)入口為:從這里可知,NioSocketChannel就是中規(guī)中矩的調(diào)用FileRegion的transferTo方法,是否真正實現(xiàn)了零拷貝,取決于底層是否調(diào)用了FileChannel的transferTo方法。

        2.3 零拷貝實踐總結(jié)

        從Netty的實現(xiàn)中我們基本可以得出結(jié)論:是否是零拷貝,判斷的依據(jù)是是否調(diào)用了FileChannel的transferTo方法,更準備的表述是底層是否調(diào)用了操作系統(tǒng)的sendfile函數(shù),并且操作系統(tǒng)底層還需要支持gather機制,即linux的內(nèi)核版本不低于2.4。

        最后說一句(求關注,別白嫖我)

        如果這篇文章對您有所幫助,或者有所啟發(fā)的話,幫忙掃描下發(fā)二維碼關注一下,您的支持是我堅持寫作最大的動力。

        求一鍵三連:點贊、轉(zhuǎn)發(fā)、在看。


        推薦閱讀:

        為什么阿里把MySQL的事務隔離級別改成RC?

        一個架構(gòu)師的緩存修煉之路

        Redis主節(jié)點的Key已過期的處理

        Redis 哨兵模式


        互聯(lián)網(wǎng)全棧架構(gòu)

        瀏覽 65
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
          
          

            1. se999se| 国产精品久久久免费视频 | 99re在线视频免费观看 | 少妇交换被菊爆视频 | 91九色视频pron |