[TOC]
先提出兩個(gè)問題:
IO過程中,哪些步驟進(jìn)行了拷貝充甚?哪些地方零拷貝爬立?
Java支持哪些零拷貝?
哪里聽說過零拷貝?真的0次拷貝嗎?
零拷貝(英語: Zero-copy) 技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí)券时,CPU不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域。這種技術(shù)通常用于通過網(wǎng)絡(luò)傳輸文件時(shí)節(jié)省CPU周期和內(nèi)存帶寬伏伯。
- 零拷貝技術(shù)可以減少數(shù)據(jù)拷貝和共享總線操作的次數(shù)橘洞,消除傳輸數(shù)據(jù)在存儲器之間不必要的中間拷貝次數(shù)织中,從而有效地提高數(shù)據(jù)傳輸效率
- 零拷貝技術(shù)減少了用戶進(jìn)程地址空間和內(nèi)核地址空間之間因?yàn)樯?下文切換而帶來的開銷(可以看出沒有說不需要拷貝囤热,只是說減少冗余[不必要]的拷貝)。
LinuxI/O機(jī)制及零拷貝介紹
IO中斷與DMA
IO中斷民镜,需要CPU響應(yīng),需要CPU參與适肠,因此效率比較低霍衫。
用戶進(jìn)程需要讀取磁盤數(shù)據(jù),需要CPU中斷侯养,發(fā)起IO請求敦跌,每次的IO中斷,都帶來CPU的上下文切換逛揩。
因此出現(xiàn)了——DMA柠傍。
DMA(Direct Memory Access,直接內(nèi)存存取) 是所有現(xiàn)代電腦的重要特色辩稽,它允許不同速度的硬件裝置來溝通惧笛,而不需要依賴于CPU 的大量中斷負(fù)載。
DMA控制器逞泄,接管了數(shù)據(jù)讀寫請求患整,減少CPU的負(fù)擔(dān)。這樣一來喷众,CPU能高效工作了各谚。
現(xiàn)代硬盤基本都支持DMA。
Linux IO流程
實(shí)際因此IO讀取到千,涉及兩個(gè)過程:
1昌渤、DMA等待數(shù)據(jù)準(zhǔn)備好,把磁盤數(shù)據(jù)讀取到操作系統(tǒng)內(nèi)核緩沖區(qū)父阻;
2愈涩、用戶進(jìn)程,將內(nèi)核緩沖區(qū)的數(shù)據(jù)copy到用戶空間加矛。
這兩個(gè)過程履婉,都是阻塞的。
傳統(tǒng)數(shù)據(jù)傳送
比如:讀取文件斟览,再用socket發(fā)送出去
傳統(tǒng)方式實(shí)現(xiàn):
先讀取毁腿、再發(fā)送,實(shí)際經(jīng)過1~4四次copy苛茂。
buffer = File.read
Socket.send(buffer)
1已烤、第一次:將磁盤文件,讀取到操作系統(tǒng)內(nèi)核緩沖區(qū)妓羊;
2胯究、第二次:將內(nèi)核緩沖區(qū)的數(shù)據(jù),copy到application應(yīng)用程序的buffer躁绸;
3裕循、第三步:將application應(yīng)用程序buffer中的數(shù)據(jù)臣嚣,copy到socket網(wǎng)絡(luò)發(fā)送緩沖區(qū)(屬于操作系統(tǒng)內(nèi)核的緩沖區(qū));
4剥哑、第四次:將socket buffer的數(shù)據(jù)硅则,copy到網(wǎng)卡,由網(wǎng)卡進(jìn)行網(wǎng)絡(luò)傳輸株婴。
傳統(tǒng)方式怎虫,讀取磁盤文件并進(jìn)行網(wǎng)絡(luò)發(fā)送,經(jīng)過的四次數(shù)據(jù)copy是非常繁瑣的困介。實(shí)際IO讀寫大审,需要進(jìn)行IO中斷,需要CPU響應(yīng)中斷(帶來上下文切換)逻翁,盡管后來引入DMA來接管CPU的中斷請求饥努,但四次copy是存在“不必要的拷貝”的捡鱼。
重新思考傳統(tǒng)IO方式八回,會注意到實(shí)際上并不需要第二個(gè)和第三個(gè)數(shù)據(jù)副本。應(yīng)用程序除了緩存數(shù)據(jù)并將其傳輸回套接字緩沖區(qū)之外什么都不做驾诈。相反缠诅,數(shù)據(jù)可以直接從讀緩沖區(qū)傳輸?shù)教捉幼志彌_區(qū)。
顯然乍迄,第二次和第三次數(shù)據(jù)copy 其實(shí)在這種場景下沒有什么幫助反而帶來開銷管引,這也正是零拷貝出現(xiàn)的背景和意義。
傳統(tǒng)數(shù)據(jù)傳送所消耗的成本:4次拷貝闯两,4次上下文切換褥伴。
4次拷貝,其中兩次是DMA copy漾狼,兩次是CPU copy重慢。如下圖所示
拷貝是個(gè)IO過程,需要系統(tǒng)調(diào)用逊躁。
注意一點(diǎn)的是 內(nèi)核從磁盤上面讀取數(shù)據(jù) 是 不消耗CPU時(shí)間的似踱,是通過磁盤控制器完成;稱之為DMA Copy稽煤。
網(wǎng)卡發(fā)送也用DMA核芽。
零拷貝的出現(xiàn)
目的:減少IO流程中不必要的拷貝
零拷貝需要OS支持,也就是需要kernel暴露api酵熙。虛擬機(jī)不能操作內(nèi)核轧简,
Linux支持的(常見)零拷貝
一、mmap內(nèi)存映射
data loaded from disk is stored in a kernel buffer by DMA copy. Then the pages of the application buffer are mapped to the kernel buffer, so that the data copy between kernel buffers and application buffers are omitted.
DMA加載磁盤數(shù)據(jù)到kernel buffer后匾二,應(yīng)用程序緩沖區(qū)(application buffers)和內(nèi)核緩沖區(qū)(kernel buffer)進(jìn)行映射哮独,數(shù)據(jù)再應(yīng)用緩沖區(qū)和內(nèi)核緩存區(qū)的拷貝就能省略庐橙。
mmap內(nèi)存映射將會經(jīng)歷:3次拷貝: 1次cpu copy,2次DMA copy借嗽;
以及4次上下文切換
二态鳖、sendfile
linux 2.1支持的sendfile
when calling the sendfile() system call, data are fetched from disk and copied into a kernel buffer by DMA copy. Then data are copied directly from the kernel buffer to the socket buffer. Once all data are copied into the socket buffer, the sendfile() system call will return to indicate the completion of data transfer from the kernel buffer to socket buffer. Then, data will be copied to the buffer on the network card and transferred to the network.
當(dāng)調(diào)用sendfile()時(shí),DMA將磁盤數(shù)據(jù)復(fù)制到kernel buffer恶导,然后將內(nèi)核中的kernel buffer直接拷貝到socket buffer浆竭;
一旦數(shù)據(jù)全都拷貝到socket buffer,sendfile()系統(tǒng)調(diào)用將會return惨寿、代表數(shù)據(jù)轉(zhuǎn)化的完成邦泄。
socket buffer里的數(shù)據(jù)就能在網(wǎng)絡(luò)傳輸了。
sendfile會經(jīng)歷:3次拷貝裂垦,1次CPU copy 2次DMA copy顺囊;
以及2次上下文切換
三、Sendfile With DMA Scatter/Gather Copy
Then by using the DMA scatter/gather operation, the network interface card can gather all the data from different memory locations and store the assembled packet in the network card buffer.
Scatter/Gather可以看作是sendfile的增強(qiáng)版蕉拢,批量sendfile特碳。
Scatter/Gather會經(jīng)歷2次拷貝: 0次cpu copy,2次DMA copy
IO請求批量化
DMA scatter/gather:需要DMA控制器支持的晕换。
DMA工作流程:cpu發(fā)送IO請求給DMA午乓,DMA然后讀取數(shù)據(jù)。
IO請求:相當(dāng)于可以看作包含一個(gè)物理地址闸准。
從一系列物理地址(10)讀數(shù)據(jù):普通的DMA (10請求)
dma scatter/gather:一次給10個(gè)物理地址益愈, 一個(gè)請求就可以(批量處理)。
Linux零拷貝機(jī)制對比
無論是傳統(tǒng)IO方式夷家,還是引入零拷貝之后蒸其,2次DMA copy 是都少不了的。因?yàn)閮纱蜠MA都是依賴硬件完成的库快。
零拷貝的廣義狹義之分
實(shí)際上摸袁,零拷貝時(shí)有廣義和狹義之分的。
廣義零拷貝: 能減少拷貝次數(shù)缺谴,減少不必要的數(shù)據(jù)拷貝但惶,就算作“零拷貝”。
這是目前湿蛔,對零拷貝最為廣泛的定義膀曾,我們需要知道的是,這是廣義上的零拷貝阳啥,并不是操作系統(tǒng) 意義上的零拷貝添谊。
零拷貝的廣義性
最早的零拷貝定義,來源于
Linux 2.4內(nèi)核新增 sendfile 系統(tǒng)調(diào)用察迟,提供了零拷貝斩狱。磁盤數(shù)據(jù)通過 DMA 拷貝到內(nèi)核態(tài) Buffer 后耳高,直接通過 DMA 拷貝到 NIC Buffer(socket buffer),無需 CPU 拷貝所踊。這也是零拷貝這一說法的來源泌枪。這是真正操作系統(tǒng) 意義上的零拷貝(也就是狹義零拷貝)。
但是我們知道秕岛,由OS內(nèi)核提供的 操作系統(tǒng)意義上的零拷貝碌燕,發(fā)展到目前也并沒有很多種,也就是這樣的零拷貝并不是很多继薛;
隨著發(fā)展修壕,零拷貝的概念得到了延伸,就是目前的減少不必要的數(shù)據(jù)拷貝都算作零拷貝的范疇遏考;
Java零拷貝機(jī)制解析
Linux提供的領(lǐng)拷貝技術(shù) Java并不是全支持慈鸠,支持2種(內(nèi)存映射mmap、sendfile)灌具;
NIO提供的內(nèi)存映射 MappedByteBuffer
- 首先要說明的是青团,JavaNlO中 的Channel (通道)就相當(dāng)于操作系統(tǒng)中的內(nèi)核緩沖區(qū),有可能是讀緩沖區(qū)稽亏,也有可能是網(wǎng)絡(luò)緩沖區(qū)壶冒,而Buffer就相當(dāng)于操作系統(tǒng)中的用戶緩沖區(qū)缕题。
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);
底層就是調(diào)用Linux mmap()實(shí)現(xiàn)的截歉。
NIO中的FileChannel.map()方法其實(shí)就是采用了操作系統(tǒng)中的內(nèi)存映射方式,底層就是調(diào)用Linux mmap()實(shí)現(xiàn)的烟零。
將內(nèi)核緩沖區(qū)的內(nèi)存和用戶緩沖區(qū)的內(nèi)存做了一個(gè)地址映射瘪松。這種方式適合讀取大文件,同時(shí)也能對文件內(nèi)容進(jìn)行更改锨阿,但是如果其后要通過SocketChannel發(fā)送宵睦,還是需要CPU進(jìn)行數(shù)據(jù)的拷貝。
使用MappedByteBuffer墅诡,小文件壳嚎,效率不高;一個(gè)進(jìn)程訪問末早,效率也不高烟馅。
MappedByteBuffer只能通過調(diào)用FileChannel的map()取得,再沒有其他方式然磷。
FileChannel.map()是抽象方法郑趁,具體實(shí)現(xiàn)是在 FileChannelImpl.c 可自行查看JDK源碼,其map0()方法就是調(diào)用了Linux內(nèi)核的mmap的API姿搜。
使用 MappedByteBuffer類要注意的是:mmap的文件映射寡润,在full gc時(shí)才會進(jìn)行釋放捆憎。當(dāng)close時(shí),需要手動(dòng)清除內(nèi)存映射文件梭纹,可以反射調(diào)用sun.misc.Cleaner方法躲惰。
NIO提供的sendfile
- FileChannel.transferTo()方法直接將當(dāng)前通道內(nèi)容傳輸?shù)搅硪粋€(gè)通道,沒有涉及到Buffer的任何操作变抽,NIO中 的Buffer是JVM堆或者堆外內(nèi)存礁扮,但不論如何他們都是操作系統(tǒng)內(nèi)核空間的內(nèi)存
- transferTo()的實(shí)現(xiàn)方式就是通過系統(tǒng)調(diào)用sendfile() (當(dāng)然這是Linux中的系統(tǒng)調(diào)用)
//使用sendfile:讀取磁盤文件,并網(wǎng)絡(luò)發(fā)送
FileChannel sourceChannel = new RandomAccessFile(source, "rw").getChannel();
SocketChannel socketChannel = SocketChannel.open(sa);
sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);
ZeroCopyFile實(shí)現(xiàn)文件復(fù)制
class ZeroCopyFile {
public void copyFile(File src, File dest) {
try (FileChannel srcChannel = new FileInputStream(src).getChannel();
FileChannel destChannel = new FileInputStream(dest).getChannel()) {
srcChannel.transferTo(0, srcChannel.size(), destChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java NIO提供的FileChannel.transferTo 和 transferFrom 并不保證一定能使用零拷貝瞬沦。實(shí)際上是否能使用零拷貝與操作系統(tǒng)相關(guān)太伊,如果操作系統(tǒng)提供 sendfile 這樣的零拷貝系統(tǒng)調(diào)用,則這兩個(gè)方法會通過這樣的系統(tǒng)調(diào)用充分利用零拷貝的優(yōu)勢逛钻,否則并不能通過這兩個(gè)方法本身實(shí)現(xiàn)零拷貝僚焦。
Kafka中的零拷貝
Kafka兩個(gè)重要過程都使用了零拷貝技術(shù),且都是操作系統(tǒng)層面的狹義零拷貝曙痘,一是Producer生產(chǎn)的數(shù)據(jù)存到broker芳悲,二是 Consumer從broker讀取數(shù)據(jù)。
- Producer生產(chǎn)的數(shù)據(jù)持久化到broker边坤,采用mmap文件映射名扛,實(shí)現(xiàn)順序的快速寫入;
- Customer從broker讀取數(shù)據(jù)茧痒,采用sendfile肮韧,將磁盤文件讀到OS內(nèi)核緩沖區(qū)后,直接轉(zhuǎn)到socket buffer進(jìn)行網(wǎng)絡(luò)發(fā)送旺订。
Netty中的零拷貝
Netty中的Zero-copy與上面我們所提到到OS層面上的Zero-copy不太一樣, Netty的Zero-copy完全是在用戶態(tài)(Java層面)的弄企,它的Zero-copy的更多的是偏向于優(yōu)化數(shù)據(jù)操作這樣的概念
Netty的Zero-copy體現(xiàn)在如下幾個(gè)個(gè)方面:
- Netty提供了CompositeByteBuf類,它可以將多個(gè)ByteBuf合并為一個(gè)邏輯上的ByteBuf区拳,避免了各個(gè)ByteBuf之間的拷貝拘领。
- 通過wrap操作,我們可以將byte[]數(shù)組樱调、ByteBuf约素、 ByteBuffer 等包裝成一個(gè) Netty ByteBuf對象,進(jìn)而避免了拷貝操作笆凌。
- ByteBuf支持slice 操作圣猎,因此可以將ByteBuf分解為多個(gè)共享同一個(gè)存儲區(qū)域的ByteBuf,避免了內(nèi)存的拷貝菩颖。
- 通過FileRegion包裝的FileChannel.tranferTo實(shí)現(xiàn)文件傳輸样漆,可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標(biāo)Channel,避免了傳統(tǒng)通過循環(huán)write方式導(dǎo)致的內(nèi)存拷貝問題晦闰。
前三個(gè)都是 廣義零拷貝放祟,都是減少不必要數(shù)據(jù)copy鳍怨;偏向于應(yīng)用層數(shù)據(jù)優(yōu)化的操作。