<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>

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

          共 3670字,需瀏覽 8分鐘

           ·

          2021-11-19 12:39

          大家好,我是躍哥。Java 內(nèi)卷大家都知道了吧,哈哈。今天就和大家一起來卷一個(gè)概念,叫做零拷貝。

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

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

          **“零拷貝”**其實(shí)包括兩個(gè)層面的含義:

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

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

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

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

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

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

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

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

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

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

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

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

          從上面兩張流程圖,我們不能看出讀寫處理流程中存在太多復(fù)制,同樣的數(shù)據(jù)需要被復(fù)制多次,造成性能損耗,故IO讀寫通常的優(yōu)化方向主要為:減少?gòu)?fù)制次數(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)存(一個(gè)非官方的理解,用C語(yǔ)言實(shí)現(xiàn)的本地方法調(diào)用中,首先需要將堆空間中數(shù)據(jù)拷貝到C語(yǔ)言相關(guān)的存儲(chǔ)結(jié)構(gòu)),故提高性能的第一個(gè)措施:使用堆外內(nèi)存。

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

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

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

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

          1.3.3 大名鼎鼎鼎sendfile

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

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

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

          sendfile的優(yōu)勢(shì)(特點(diǎn)):

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

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

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

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

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

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

          • 首先介紹DefaultFileRegion的核心屬性含義:
            • File f 底層抽取數(shù)據(jù)來源的底層磁盤文件
            • FileChannel file 底層文件的文件通道。
            • long position 數(shù)據(jù)從通道中抽取的起始位置
            • long count 需要傳遞的總字節(jié)數(shù)
            • long transfered 已傳遞的字節(jié)數(shù)量。
          • 核心要點(diǎn)是調(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ā)要點(diǎn)。在Netty中代表兩個(gè)類型的通道:

          • EpollSocketChannel 基于Epoll機(jī)制進(jìn)行事件的就緒選擇機(jī)制。

          • NioSocketChannel

            基于select機(jī)制的事件就緒選擇。

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

          2.1 EpollSocketChannel 通道零拷貝

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

          溫馨提示:本文并沒有打算詳細(xì)分析Epoll機(jī)制以及編程實(shí)踐。

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

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

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

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





          0、重磅!兩萬(wàn)字長(zhǎng)文總結(jié),梳理 Java 入門進(jìn)階哪些事(推薦收藏)

          1、講真的:我達(dá)成了一個(gè)優(yōu)秀的小目標(biāo)

          2、看完這設(shè)計(jì)模式匯總,你確定不加收藏嗎


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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  操美少妇| 99热在线精品播放 | 午夜精品一区二区三区在线视频 | 欧美精品A片在线观看报备 | 大香蕉性伊|