零拷貝的原理及Java實現(xiàn)

在談論Kafka高性能時不得不提到零拷貝和敬。Kafka通過采用零拷貝大大提供了應用性能,減少了內(nèi)核和用戶模式之間的上下文切換次數(shù)戏阅。那么什么是零拷貝昼弟,如何實現(xiàn)零拷貝呢?

什么是零拷貝

WIKI中對其有如下定義:

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

從WIKI的定義中奕筐,我們看到“零拷貝”是指計算機操作的過程中舱痘,CPU不需要為數(shù)據(jù)在內(nèi)存之間的拷貝消耗資源。而它通常是指計算機在網(wǎng)絡上發(fā)送文件時离赫,不需要將文件內(nèi)容拷貝到用戶空間(User Space)而直接在內(nèi)核空間(Kernel Space)中傳輸?shù)骄W(wǎng)絡的方式芭逝。

零拷貝給我們帶來的好處

  • 減少甚至完全避免不必要的CPU拷貝,從而讓CPU解脫出來去執(zhí)行其他的任務
  • 減少內(nèi)存帶寬的占用
  • 通常零拷貝技術還能夠減少用戶空間和操作系統(tǒng)內(nèi)核空間之間的上下文切換

零拷貝的實現(xiàn)

零拷貝實際的實現(xiàn)并沒有真正的標準渊胸,取決于操作系統(tǒng)如何實現(xiàn)這一點旬盯。零拷貝完全依賴于操作系統(tǒng)。操作系統(tǒng)支持翎猛,就有胖翰;不支持,就沒有办成。不依賴Java本身泡态。

傳統(tǒng)I/O

在Java中,我們可以通過InputStream從源數(shù)據(jù)中讀取數(shù)據(jù)流到一個緩沖區(qū)里迂卢,然后再將它們輸入到OutputStream里某弦。我們知道,這種IO方式傳輸效率是比較低的而克。那么靶壮,當使用上面的代碼時操作系統(tǒng)會發(fā)生什么情況:


傳統(tǒng)IO.jpg

這是一個從磁盤文件讀取并且通過socket寫出的過程,對應的系統(tǒng)調(diào)用如下:

read(file,tmp_buf,len)
write(socket,tmp_buf,len)
  1. 程序使用read()系統(tǒng)調(diào)用员萍。系統(tǒng)由用戶態(tài)轉(zhuǎn)換為內(nèi)核態(tài)(第一次上線文切換)腾降,磁盤中的數(shù)據(jù)有DMA(Direct Memory Access)的方式讀取到內(nèi)核緩沖區(qū)(kernel buffer)。DMA過程中CPU不需要參與數(shù)據(jù)的讀寫碎绎,而是DMA處理器直接將硬盤數(shù)據(jù)通過總線傳輸?shù)絻?nèi)存中螃壤。
  2. 系統(tǒng)由內(nèi)核態(tài)轉(zhuǎn)換為用戶態(tài)(第二次上下文切換),當程序要讀取的數(shù)據(jù)已經(jīng)完成寫入內(nèi)核緩沖區(qū)以后筋帖,程序會將數(shù)據(jù)由內(nèi)核緩存區(qū)奸晴,寫入用戶緩存區(qū)),這個過程需要CPU參與數(shù)據(jù)的讀寫日麸。
  3. 程序使用write()系統(tǒng)調(diào)用寄啼。系統(tǒng)由用戶態(tài)切換到內(nèi)核態(tài)(第三次上下文切換),數(shù)據(jù)從用戶態(tài)緩沖區(qū)寫入到網(wǎng)絡緩沖區(qū)(Socket Buffer),這個過程需要CPU參與數(shù)據(jù)的讀寫墩划。
  4. 系統(tǒng)由內(nèi)核態(tài)切換到用戶態(tài)(第四次上下文切換)涕刚,網(wǎng)絡緩沖區(qū)的數(shù)據(jù)通過DMA的方式傳輸?shù)骄W(wǎng)卡的驅(qū)動(存儲緩沖區(qū))中(protocol engine)

可以看到,傳統(tǒng)的I/O方式會經(jīng)過4次用戶態(tài)和內(nèi)核態(tài)的切換(上下文切換)乙帮,兩次CPU中內(nèi)存中進行數(shù)據(jù)讀寫的過程杜漠。這種拷貝過程相對來說比較消耗資源

內(nèi)存映射方式I/O
mmap.jpg
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

這是使用的系統(tǒng)調(diào)用方法,這種方式的I/O原理就是將用戶緩沖區(qū)(user buffer)的內(nèi)存地址和內(nèi)核緩沖區(qū)(kernel buffer)的內(nèi)存地址做一個映射察净,也就是說系統(tǒng)在用戶態(tài)可以直接讀取并操作內(nèi)核空間的數(shù)據(jù)碑幅。

  1. mmap()系統(tǒng)調(diào)用首先會使用DMA的方式將磁盤數(shù)據(jù)讀取到內(nèi)核緩沖區(qū),然后通過內(nèi)存映射的方式塞绿,使用戶緩沖區(qū)和內(nèi)核讀緩沖區(qū)的內(nèi)存地址為同一內(nèi)存地址沟涨,也就是說不需要CPU再講數(shù)據(jù)從內(nèi)核讀緩沖區(qū)復制到用戶緩沖區(qū)。
  2. 當使用write()系統(tǒng)調(diào)用的時候异吻,cpu將內(nèi)核緩沖區(qū)(等同于用戶緩沖區(qū))的數(shù)據(jù)直接寫入到網(wǎng)絡發(fā)送緩沖區(qū)(socket buffer)裹赴,然后通過DMA的方式將數(shù)據(jù)傳入到網(wǎng)卡驅(qū)動程序中準備發(fā)送。

可以看到這種內(nèi)存映射的方式減少了CPU的讀寫次數(shù)诀浪,但是用戶態(tài)到內(nèi)核態(tài)的切換(上下文切換)依舊有四次棋返,同時需要注意在進行這種內(nèi)存映射的時候,有可能會出現(xiàn)并發(fā)線程操作同一塊內(nèi)存區(qū)域而導致的嚴重的數(shù)據(jù)不一致問題雷猪,所以需要進行合理的并發(fā)編程來解決這些問題睛竣。

通過sendfile實現(xiàn)的零拷貝I/O
sendfile.jpg
sendfile(socket, file, len);

通過sendfile()系統(tǒng)調(diào)用,可以做到內(nèi)核空間內(nèi)部直接進行I/O傳輸求摇。

  1. sendfile()系統(tǒng)調(diào)用也會引起用戶態(tài)到內(nèi)核態(tài)的切換射沟,與內(nèi)存映射方式不同的是,用戶空間此時是無法看到或修改數(shù)據(jù)內(nèi)容与境,也就是說這是一次完全意義上的數(shù)據(jù)傳輸過程验夯。
  2. 從磁盤讀取到內(nèi)存是DMA的方式,從內(nèi)核讀緩沖區(qū)讀取到網(wǎng)絡發(fā)送緩沖區(qū)摔刁,依舊需要CPU參與拷貝挥转,而從網(wǎng)絡發(fā)送緩沖區(qū)到網(wǎng)卡中的緩沖區(qū)依舊是DMA方式。

依舊有一次CPU進行數(shù)據(jù)拷貝共屈,兩次用戶態(tài)和內(nèi)核態(tài)的切換操作绑谣,相比較于內(nèi)存映射的方式有了很大的進步,但問題是程序不能對數(shù)據(jù)進行修改拗引,而只是單純地進行了一次數(shù)據(jù)的傳輸過程借宵。

理想狀態(tài)下的零拷貝I/O
sendfile2.jpg

依舊是系統(tǒng)調(diào)用sendfile()

sendfile(socket, file, len);

可以看到,這是真正意義上的零拷貝寺擂,因為其間CPU已經(jīng)不參與數(shù)據(jù)的拷貝過程暇务,也就是說完全通過其他硬件和中斷的方式來實現(xiàn)數(shù)據(jù)的讀寫過程嗎,但是這樣的過程需要硬件的支持才能實現(xiàn)怔软。

借助于硬件上的幫助垦细,我們是可以辦到的。之前我們是把頁緩存的數(shù)據(jù)拷貝到socket緩存中挡逼,實際上括改,我們僅僅需要把緩沖區(qū)描述符傳到socket緩沖區(qū),再把數(shù)據(jù)長度傳過去家坎,這樣DMA控制器直接將頁緩存中的數(shù)據(jù)打包發(fā)送到網(wǎng)絡中就可以了嘱能。

  1. 系統(tǒng)調(diào)用sendfile()發(fā)起后,磁盤數(shù)據(jù)通過DMA方式讀取到內(nèi)核緩沖區(qū)虱疏,內(nèi)核緩沖區(qū)中的數(shù)據(jù)通過DMA聚合網(wǎng)絡緩沖區(qū)惹骂,然后一齊發(fā)送到網(wǎng)卡中。

可以看到在這種模式下做瞪,是沒有一次CPU進行數(shù)據(jù)拷貝的对粪,所以就做到了真正意義上的零拷貝,雖然和前一種是同一個系統(tǒng)調(diào)用装蓬,但是這種模式實現(xiàn)起來需要硬件的支持著拭,但對于基于操作系統(tǒng)的用戶來講,操作系統(tǒng)已經(jīng)屏蔽了這種差異牍帚,它會根據(jù)不同的硬件平臺來實現(xiàn)這個系統(tǒng)調(diào)用

Java的實現(xiàn)

NIO的零拷貝
  File file = new File("test.zip");
  RandomAccessFile raf = new RandomAccessFile(file, "rw");
  FileChannel fileChannel = raf.getChannel();
  SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
  // 直接使用了transferTo()進行通道間的數(shù)據(jù)傳輸
  fileChannel.transferTo(0, fileChannel.size(), socketChannel);

NIO的零拷貝由transferTo()方法實現(xiàn)儡遮。transferTo()方法將數(shù)據(jù)從FileChannel對象傳送到可寫的字節(jié)通道(如Socket Channel等)。在內(nèi)部實現(xiàn)中暗赶,由native方法transferTo0()來實現(xiàn)鄙币,它依賴底層操作系統(tǒng)的支持。在UNIX和Linux系統(tǒng)中蹂随,調(diào)用這個方法將會引起sendfile()系統(tǒng)調(diào)用爱榔。

使用場景一般是:

  • 較大,讀寫較慢糙及,追求速度
  • M內(nèi)存不足详幽,不能加載太大數(shù)據(jù)
  • 帶寬不夠,即存在其他程序或線程存在大量的IO操作浸锨,導致帶寬本來就小

以上都建立在不需要進行數(shù)據(jù)文件操作的情況下唇聘,如果既需要這樣的速度,也需要進行數(shù)據(jù)操作怎么辦柱搜?
那么使用NIO的直接內(nèi)存迟郎!

NIO的直接內(nèi)存
  File file = new File("test.zip");
  RandomAccessFile raf = new RandomAccessFile(file, "rw");
  FileChannel fileChannel = raf.getChannel();
  MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

首先,它的作用位置處于傳統(tǒng)IO(BIO)與零拷貝之間聪蘸,為何這么說宪肖?

  • IO表制,可以把磁盤的文件經(jīng)過內(nèi)核空間,讀到JVM空間控乾,然后進行各種操作么介,最后再寫到磁盤或是發(fā)送到網(wǎng)絡,效率較慢但支持數(shù)據(jù)文件操作蜕衡。
  • 零拷貝則是直接在內(nèi)核空間完成文件讀取并轉(zhuǎn)到磁盤(或發(fā)送到網(wǎng)絡)壤短。由于它沒有讀取文件數(shù)據(jù)到JVM這一環(huán),因此程序無法操作該文件數(shù)據(jù)慨仿,盡管效率很高久脯!

而直接內(nèi)存則介于兩者之間,效率一般且可操作文件數(shù)據(jù)镰吆。直接內(nèi)存(mmap技術)將文件直接映射到內(nèi)核空間的內(nèi)存帘撰,返回==一個操作地址(address)==,它解決了文件數(shù)據(jù)需要拷貝到JVM才能進行操作的窘境万皿。而是直接在內(nèi)核空間直接進行操作骡和,省去了內(nèi)核空間拷貝到用戶空間這一步操作。

NIO的直接內(nèi)存是由==MappedByteBuffer==實現(xiàn)的相寇。核心即是map()方法慰于,該方法把文件映射到內(nèi)存中,獲得內(nèi)存地址addr唤衫,然后通過這個addr構造MappedByteBuffer類婆赠,以暴露各種文件操作API。

由于MappedByteBuffer申請的是堆外內(nèi)存佳励,因此不受Minor GC控制休里,只能在發(fā)生Full GC時才能被回收。而==DirectByteBuffer==改善了這一情況赃承,它是MappedByteBuffer類的子類妙黍,同時它實現(xiàn)了DirectBuffer接口,維護一個Cleaner對象來完成內(nèi)存回收瞧剖。因此它既可以通過Full GC來回收內(nèi)存拭嫁,也可以調(diào)用clean()方法來進行回收。

另外抓于,直接內(nèi)存的大小可通過jvm參數(shù)來設置:-XX:MaxDirectMemorySize做粤。

NIO的MappedByteBuffer還有一個兄弟叫做HeapByteBuffer。顧名思義捉撮,它用來在堆中申請內(nèi)存怕品,本質(zhì)是一個數(shù)組。由于它位于堆中巾遭,因此可受GC管控肉康,易于回收闯估。

參考
https://blog.csdn.net/localhost01/article/details/83422888
https://blog.csdn.net/cringkong/article/details/80274148

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吼和,隨后出現(xiàn)的幾起案子涨薪,更是在濱河造成了極大的恐慌,老刑警劉巖纹安,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異砂豌,居然都是意外死亡厢岂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門阳距,熙熙樓的掌柜王于貴愁眉苦臉地迎上來塔粒,“玉大人,你說我怎么就攤上這事筐摘∽洳纾” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵咖熟,是天一觀的道長圃酵。 經(jīng)常有香客問我,道長馍管,這世上最難降的妖魔是什么郭赐? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮确沸,結果婚禮上捌锭,老公的妹妹穿的比我還像新娘。我一直安慰自己罗捎,他們只是感情好观谦,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著桨菜,像睡著了一般豁状。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上倒得,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天替蔬,我揣著相機與錄音,去河邊找鬼屎暇。 笑死承桥,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的根悼。 我是一名探鬼主播凶异,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼蜀撑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了剩彬?” 一聲冷哼從身側響起酷麦,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喉恋,沒想到半個月后沃饶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡轻黑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年糊肤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氓鄙。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡馆揉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抖拦,到底是詐尸還是另有隱情升酣,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布态罪,位于F島的核電站噩茄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏复颈。R本人自食惡果不足惜巢墅,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望券膀。 院中可真熱鬧君纫,春花似錦、人聲如沸芹彬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舒帮。三九已至会喝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間玩郊,已是汗流浹背肢执。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留译红,地道東北人预茄。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像侦厚,于是被迫代替她去往敵國和親耻陕。 傳聞我的和親對象是個殘疾皇子拙徽,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348