什么是零拷貝
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)存帶寬的占用猖败。
- 通常零拷貝技術(shù)還能夠減少用戶空間和操作系統(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ā)生什么情況:
這是一個從磁盤文件讀取并且通過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ù)讀寫的過程。這種拷貝過程相對來說比較消耗資源艰躺。
mmap內(nèi)存映射方式I/O
mmap 通過內(nèi)存映射呻袭,將文件映射到內(nèi)核緩沖區(qū),同時腺兴,用戶空間可以共享內(nèi)核空間的數(shù)據(jù)左电。這樣,在進行網(wǎng)絡傳輸時含长,就可以減少內(nèi)核空間到用戶控件的拷貝次數(shù)券腔。
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
Linux 2.1 版本 提供了 sendFile 函數(shù)杂拨,其基本原理如下:數(shù)據(jù)根本不經(jīng)過用戶態(tài),直接從內(nèi)核緩沖區(qū)進入到 Socket Buffer悯衬,同時弹沽,由于和用戶態(tài)完全無關(guān),就減少了一次上下文切換。
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方式。
sendfile()系統(tǒng)調(diào)用旁仿,依舊有一次CPU進行數(shù)據(jù)拷貝藕夫,兩次用戶態(tài)和內(nèi)核態(tài)的切換操作,相比較于內(nèi)存映射的方式有了很大的進步枯冈,但問題是程序不能對數(shù)據(jù)進行修改毅贮,而只是單純地進行了一次數(shù)據(jù)的傳輸過程。
理想狀態(tài)下的零拷貝I/O
Linux 在 2.4 版本中尘奏,做了一些修改滩褥,避免了從內(nèi)核緩沖區(qū)拷貝到 Socket buffer 的操作,直接拷貝到協(xié)議棧炫加,從而再一次減少了數(shù)據(jù)拷貝瑰煎。
依舊是系統(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)用。
零拷貝的再次理解
1原环、我們說零拷貝挠唆,是從操作系統(tǒng)的角度來說的。因為內(nèi)核緩沖區(qū)之間嘱吗,沒有數(shù)據(jù)是重復的(只有 kernel buffer 有一份數(shù)據(jù))玄组。
2、零拷貝不僅僅帶來更少的數(shù)據(jù)復制谒麦,還能帶來其他的性能優(yōu)勢俄讹,例如更少的上下文切換弃榨,更少的 CPU 緩存?zhèn)喂蚕硪约盁o CPU 校驗和計算瘟忱。
mmap 和 sendFile 的區(qū)別
1佑吝、mmap 適合小數(shù)據(jù)量讀寫红选,sendFile 適合大文件傳輸晴股。
2海雪、mmap 需要 4 次上下文切換锋叨,3 次數(shù)據(jù)拷貝含鳞;sendFile 需要 3 次上下文切換城丧,最少 2 次數(shù)據(jù)拷貝延曙。
2、sendFile 可以利用 DMA 方式亡哄,減少 CPU 拷貝枝缔,mmap 則不能(必須從內(nèi)核拷貝到 Socket 緩沖區(qū))。
Java NIO的零拷貝
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",7001));
String filename = "text.zip";
//得到一個文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
//在linux下一個transferTo 方法就可以完成傳輸
//在windows下 一次調(diào)用 transferTo 只能發(fā)送8M, 大文件就需要分段傳輸文件
//transferTo 底層使用到零拷貝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
//關(guān)閉
fileChannel.close();
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)用宦焦。
使用場景一般是:
- 1发钝、較大顿涣,讀寫較慢,追求速度酝豪。
- 2涛碑、M內(nèi)存不足,不能加載太大數(shù)據(jù)孵淘。
- 3蒲障、帶寬不夠,即存在其他程序或線程存在大量的IO操作瘫证,導致帶寬本來就小揉阎。
以上都建立在不需要進行數(shù)據(jù)文件操作的情況下,如果既需要這樣的速度背捌,也需要進行數(shù)據(jù)操作怎么辦余黎?
那么使用NIO的直接內(nèi)存!
NIO的直接內(nèi)存
File file = new File("test.zip");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//獲取對應的通道
FileChannel fileChannel = randomAccessFile.getChannel();
/**
* 參數(shù)1:FileChannel.MapMode.READ_WRITE 使用的讀寫模式
* 參數(shù)2:可以直接修改的起始位置
* 參數(shù)3: 是映射到內(nèi)存的大小(不是索引位置) ,即將 test.zip 的多少個字節(jié)映射到內(nèi)存
* 實際類型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size);
首先载萌,它的作用位置處于傳統(tǒng)IO(BIO)與零拷貝之間惧财,為何這么說?
1扭仁、IO垮衷,可以把磁盤的文件經(jīng)過內(nèi)核空間,讀到JVM空間乖坠,然后進行各種操作搀突,最后再寫到磁盤或是發(fā)送到網(wǎng)絡,效率較慢但支持數(shù)據(jù)文件操作熊泵。
2仰迁、零拷貝則是直接在內(nèi)核空間完成文件讀取并轉(zhuǎn)到磁盤(或發(fā)送到網(wǎng)絡)。由于它沒有讀取文件數(shù)據(jù)到JVM這一環(huán)顽分,因此程序無法操作該文件數(shù)據(jù)徐许,盡管效率很高!
而直接內(nèi)存則介于兩者之間卒蘸,效率一般且可操作文件數(shù)據(jù)雌隅。直接內(nèi)存(mmap技術(shù))將文件直接映射到內(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構(gòu)造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ù)來設(shè)置:-XX:MaxDirectMemorySize
。
NIO的MappedByteBuffer還有一個兄弟叫做HeapByteBuffer胯陋。顧名思義蕊温,它用來在堆中申請內(nèi)存,本質(zhì)是一個數(shù)組遏乔。由于它位于堆中义矛,因此可受GC管控,易于回收盟萨。