1、什么是零拷貝
維基上是這么描述零拷貝的:零拷貝描述的是CPU不執(zhí)行拷貝數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域到另一個(gè)存儲(chǔ)區(qū)域的任務(wù),這通常用于通過(guò)網(wǎng)絡(luò)傳輸一個(gè)文件時(shí)以減少CPU周期和內(nèi)存帶寬。
2、零拷貝給我們帶來(lái)的好處
- 1、減少甚至完全避免不要的CPU拷貝,讓CPU解脫出來(lái)去執(zhí)行其他的任務(wù)
- 2、減少內(nèi)存帶寬的占用
- 3、通常零拷貝技術(shù)還能夠減少用戶空間和操作系統(tǒng)內(nèi)核空間的上下文切換
3、 零拷貝的實(shí)現(xiàn)
- 實(shí)際的實(shí)現(xiàn)并沒(méi)有真正的標(biāo)準(zhǔn),取決于操作系統(tǒng)如何實(shí)現(xiàn)這一點(diǎn),零拷貝本質(zhì)完全依賴操作系統(tǒng),操作系統(tǒng)支持就有,不支持就沒(méi)有,不依賴Java本身(HeapByteBuffer,DirectByteBuffer),NIO只是解決部分拷貝過(guò)程、
- 零拷貝實(shí)現(xiàn)了內(nèi)存的數(shù)據(jù)重用性 減少拷貝過(guò)程次數(shù)來(lái)提高性能
4、傳統(tǒng)I/O
-
在Java中,我們可以通過(guò)InputStream從源數(shù)據(jù)中讀取數(shù)據(jù)流到一個(gè)緩沖區(qū)里,然后再將它們輸入到OutputStream里。我們知道,這種IO方式傳輸效率是比較低的。那么,當(dāng)使用上面的代碼時(shí)操作系統(tǒng)會(huì)發(fā)生什么情況:
傳統(tǒng)I/O
1、JVM發(fā)出read() 系統(tǒng)調(diào)用。
2、OS上下文切換到內(nèi)核模式(第一次上下文切換)并將數(shù)據(jù)讀取到內(nèi)核空間緩沖區(qū)。(第一次拷貝:hardware ----> kernel buffer)
3、OS內(nèi)核然后將數(shù)據(jù)復(fù)制到用戶空間緩沖區(qū)(第二次拷貝: kernel buffer --> user buffer),然后read系統(tǒng)調(diào)用返回。而系統(tǒng)調(diào)用的返回又會(huì)導(dǎo)致一次內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。
4、JVM處理代碼邏輯并發(fā)送write()系統(tǒng)調(diào)用。
5、OS上下文切換到內(nèi)核模式(第三次上下文切換)并從用戶空間緩沖區(qū)復(fù)制數(shù)據(jù)到內(nèi)核空間緩沖區(qū)(第三次拷貝: user buffer ——> kernel buffer)。
6、write系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的再次上下文切換(第四次上下文切換)。將內(nèi)核空間緩沖區(qū)中的數(shù)據(jù)寫(xiě)到hardware(第四次拷貝: kernel buffer ——> hardware)。
- 總的來(lái)說(shuō),傳統(tǒng)的I/O操作進(jìn)行了4次用戶空間與內(nèi)核空間的上下文切換,以及4次數(shù)據(jù)拷貝。顯然在這個(gè)用例中,從內(nèi)核空間到用戶空間內(nèi)存的復(fù)制是完全不必要的,因?yàn)槌藢?shù)據(jù)轉(zhuǎn)儲(chǔ)到不同的buffer之外,我們沒(méi)有做任何其他的事情。所以,我們能不能直接從hardware讀取數(shù)據(jù)到kernel buffer后,再?gòu)膋ernel buffer寫(xiě)到目標(biāo)地點(diǎn)不就好了。為了解決這種不必要的數(shù)據(jù)復(fù)制,操作系統(tǒng)出現(xiàn)了零拷貝的概念。注意,不同的操作系統(tǒng)對(duì)零拷貝的實(shí)現(xiàn)各不相同。在這里我們介紹linux下的零拷貝實(shí)現(xiàn)。
5、通過(guò)sendfile實(shí)現(xiàn)的零拷貝I/O
- sendfile
1、發(fā)出sendfile系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第一次上下文切換)。通過(guò)DMA將磁盤文件中的內(nèi)容拷貝到內(nèi)核空間緩沖區(qū)中(第一次拷貝: hard driver ——> kernel buffer)。
2、然后再將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)拷貝到內(nèi)核中與socket相關(guān)的緩沖區(qū)中(第二次拷貝: kernel buffer ——> socket buffer)。
3、sendfile系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。通過(guò)DMA引擎將內(nèi)核空間socket緩沖區(qū)中的數(shù)據(jù)傳遞到協(xié)議引擎(第三次拷貝: socket buffer ——> protocol engine)。
- 通過(guò)sendfile實(shí)現(xiàn)的零拷貝I/O只使用了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)的拷貝。你可能會(huì)說(shuō)操作系統(tǒng)仍然需要在內(nèi)核內(nèi)存空間中復(fù)制數(shù)據(jù)(kernel buffer —>socket buffer)。 是的,但從操作系統(tǒng)的角度來(lái)看,這已經(jīng)是零拷貝,因?yàn)闆](méi)有數(shù)據(jù)從內(nèi)核空間復(fù)制到用戶空間。 內(nèi)核需要復(fù)制的原因是因?yàn)橥ㄓ糜布﨑MA訪問(wèn)需要連續(xù)的內(nèi)存空間(因此需要緩沖區(qū))。 但是,如果硬件支持scatter-and-gather,這是可以避免的。
6、帶有DMA收集拷貝功能的sendfile實(shí)現(xiàn)的I/O

1、發(fā)出sendfile系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第一次上下文切換)。通過(guò)DMA引擎將磁盤文件中的內(nèi)容拷貝到內(nèi)核空間緩沖區(qū)中(第一次拷貝: hard drive —> kernel buffer)。
2、沒(méi)有數(shù)據(jù)拷貝到socket緩沖區(qū)。取而代之的是只有相應(yīng)的描述符信息會(huì)被拷貝到相應(yīng)的socket緩沖區(qū)當(dāng)中。該描述符包含了兩方面的信息:a)kernel buffer的內(nèi)存地址;b)kernel buffer的偏移量。
3、sendfile系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。DMA gather copy根據(jù)socket緩沖區(qū)中描述符提供的位置和偏移量信息直接將內(nèi)核空間緩沖區(qū)中的數(shù)據(jù)拷貝到協(xié)議引擎上(第二次拷貝: kernel buffer ——> protocol engine),這樣就避免了最后一次CPU數(shù)據(jù)拷貝。
4、帶有DMA收集拷貝功能的sendfile實(shí)現(xiàn)的I/O只使用了2次用戶空間與內(nèi)核空間的上下文切換,以及2次數(shù)據(jù)的拷貝,而且這2次的數(shù)據(jù)拷貝都是非CPU拷貝。這樣一來(lái)我們就實(shí)現(xiàn)了最理想的零拷貝I/O傳輸了,不需要任何一次的CPU拷貝,以及最少的上下文切換。
許多Web服務(wù)器都支持零拷貝,如Tomcat和Apache。 例如Apache的相關(guān)文檔可以在這里找到,但默認(rèn)情況下關(guān)閉。
注意:Java的NIO通過(guò)transferTo()提供了這個(gè)功能。
- 傳統(tǒng)I/O用戶空間緩沖區(qū)中存有數(shù)據(jù),因此應(yīng)用程序能夠?qū)Υ藬?shù)據(jù)進(jìn)行修改等操作;而sendfile零拷貝消除了所有內(nèi)核空間緩沖區(qū)與用戶空間緩沖區(qū)之間的數(shù)據(jù)拷貝過(guò)程,因此sendfile零拷貝I/O的實(shí)現(xiàn)是完成在內(nèi)核空間中完成的,這對(duì)于應(yīng)用程序來(lái)說(shuō)就無(wú)法對(duì)數(shù)據(jù)進(jìn)行操作了。為了解決這個(gè)問(wèn)題,Linux提供了mmap零拷貝來(lái)實(shí)現(xiàn)我們的需求。
7 通過(guò)mmap實(shí)現(xiàn)的零拷貝I/O
-
mmap(內(nèi)存映射)是一個(gè)比sendfile昂貴但優(yōu)于傳統(tǒng)I/O的方法。
mmap 1、發(fā)出mmap系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第一次上下文切換)。通過(guò)DMA引擎將磁盤文件中的內(nèi)容拷貝到內(nèi)核空間緩沖區(qū)中(第一次拷貝: hard drive ——> kernel buffer)。
2、mmap系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。接著用戶空間和內(nèi)核空間共享這個(gè)緩沖區(qū),而不需要將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間。因?yàn)橛脩艨臻g和內(nèi)核空間共享了這個(gè)緩沖區(qū)數(shù)據(jù),所以用戶空間就可以像在操作自己緩沖區(qū)中數(shù)據(jù)一般操作這個(gè)由內(nèi)核空間共享的緩沖區(qū)數(shù)據(jù)。
3、發(fā)出write系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第三次上下文切換)。將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)拷貝到內(nèi)核空間socket相關(guān)聯(lián)的緩沖區(qū)(第二次拷貝: kernel buffer ——> socket buffer)。
4、write系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第四次上下文切換)。通過(guò)DMA引擎將內(nèi)核空間socket緩沖區(qū)中的數(shù)據(jù)傳遞到協(xié)議引擎(第三次拷貝: socket buffer ——> protocol engine)
通過(guò)mmap實(shí)現(xiàn)的零拷貝I/O進(jìn)行了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中包括了2次DMA拷貝和1次CPU拷貝。明顯,它與傳統(tǒng)I/O相比僅僅少了1次內(nèi)核空間緩沖區(qū)和用戶空間緩沖區(qū)之間的CPU拷貝。這樣的好處是,我們可以將整個(gè)文件或者整個(gè)文件的一部分映射到內(nèi)存當(dāng)中,用戶直接對(duì)內(nèi)存中對(duì)文件進(jìn)行操作,然后是由操作系統(tǒng)來(lái)進(jìn)行相關(guān)的頁(yè)面請(qǐng)求并將內(nèi)存的修改寫(xiě)入到文件當(dāng)中。我們的應(yīng)用程序只需要處理內(nèi)存的數(shù)據(jù),這樣可以實(shí)現(xiàn)非常迅速的I/O操作。
8、NIO DirectByteBuffer
- Java NIO引入了用于通道的緩沖區(qū)的ByteBuffer。 ByteBuffer有三個(gè)主要的實(shí)現(xiàn):HeapByteBuffer、DirectByteBuffer、MappedByteBuffer
| 類名 | 類描述說(shuō)明 |
|---|---|
| HeapByteBuffer | 堆緩沖區(qū) |
| DirectByteBuffer | 直接緩沖區(qū) |
| MappedByteBuffer | 映射緩沖區(qū) |
HeapByteBuffer
-
在調(diào)用ByteBuffer.allocate()時(shí)使用。 它被稱為堆,因?yàn)樗4嬖贘VM的堆空間中,因此你可以獲得所有優(yōu)勢(shì),如GC支持和緩存優(yōu)化。 但是,它不是頁(yè)面對(duì)齊的,這意味著如果你需要通過(guò)JNI與本地代碼交談,JVM將不得不復(fù)制到對(duì)齊的緩沖區(qū)空間。
HeapByteBuffer
DirectByteBuffer
-
在調(diào)用ByteBuffer.allocateDirect()時(shí)使用。 JVM將使用malloc()在堆空間之外分配內(nèi)存空間。 因?yàn)樗皇怯蒍VM管理的,所以你的內(nèi)存空間是頁(yè)面對(duì)齊的,不受GC影響,這使得它成為處理本地代碼的完美選擇。 然而,你要C程序員一樣,自己管理這個(gè)內(nèi)存,必須自己分配和釋放內(nèi)存來(lái)防止內(nèi)存泄漏。
DirectByteBuffer
MappedByteBuffer
-
在調(diào)用FileChannel.map()時(shí)使用。 與DirectByteBuffer類似,這也是JVM堆外部的情況。 它基本上作為OS mmap()系統(tǒng)調(diào)用的包裝函數(shù),以便代碼直接操作映射的物理內(nèi)存數(shù)據(jù)
MappedByteBuffer
9 總結(jié)
- 零拷貝是操作系統(tǒng)底層的一種實(shí)現(xiàn),我們?cè)诰W(wǎng)絡(luò)編程中,利用操作系統(tǒng)這一特性,可以大大提高數(shù)據(jù)傳輸?shù)男?。這也是目前網(wǎng)絡(luò)編程框架中都會(huì)采用的方式,理解好零拷貝,有助于我們進(jìn)一步學(xué)習(xí)Netty等網(wǎng)絡(luò)通信框架的底層原理。
- 以上文章來(lái)自 Java NIO學(xué)習(xí)筆記四(零拷貝詳解)進(jìn)行個(gè)人整理
推薦看堆外內(nèi)存、零拷貝、DirectByteBuffer





