Linux中傳統(tǒng)的I/O操作是一種緩存I/O,I/O過(guò)程中產(chǎn)生的數(shù)據(jù)傳輸通常需要在緩沖區(qū)中進(jìn)行多次拷貝。當(dāng)應(yīng)用程序需要訪問(wèn)某個(gè)數(shù)據(jù)(read()操作)時(shí),操作系統(tǒng)會(huì)先判斷這塊數(shù)據(jù)是否在內(nèi)核緩沖區(qū)中,如果在內(nèi)核緩沖區(qū)中找不到這塊數(shù)據(jù),內(nèi)核會(huì)先將這塊數(shù)據(jù)從磁盤中讀出來(lái)放到內(nèi)核緩沖區(qū)中,應(yīng)用程序再?gòu)木彌_區(qū)中讀取。當(dāng)應(yīng)用程序需要將數(shù)據(jù)輸出(write())時(shí),同樣需要先將數(shù)據(jù)拷貝到輸出堆棧相關(guān)的內(nèi)核緩沖區(qū),再?gòu)膬?nèi)核緩沖區(qū)拷貝到輸出設(shè)備中。
while((n = read(diskfd, buf, BUF_SIZE)) > 0)
? ?write(sockfd, buf , n);
以一次網(wǎng)絡(luò)請(qǐng)求為例,如下圖。對(duì)于一次數(shù)據(jù)讀取,用戶應(yīng)用程序只需要調(diào)用read()及write()兩個(gè)系統(tǒng)調(diào)用就可以完成一次數(shù)據(jù)傳輸,但這個(gè)過(guò)程中數(shù)據(jù)經(jīng)過(guò)了四次拷貝,且數(shù)據(jù)拷貝需要由CPU來(lái)調(diào)控。在某些情況下,這些數(shù)據(jù)拷貝會(huì)極大地降低系統(tǒng)數(shù)據(jù)傳輸?shù)男阅?,比如文件服?wù)器中,一個(gè)文件從磁盤讀取后不加修改地回傳給調(diào)用方,那么這占用CPU時(shí)間去處理這四次數(shù)據(jù)拷貝的性價(jià)比是極低的。
一次處理網(wǎng)絡(luò)調(diào)用的系統(tǒng)I/O的流程:
發(fā)出read()系統(tǒng)調(diào)用,導(dǎo)致應(yīng)用程序空間到內(nèi)核空間的上下文切換,將文件數(shù)據(jù)從磁盤上讀取到內(nèi)核空間緩沖區(qū)。
將內(nèi)核空間緩沖區(qū)的數(shù)據(jù)拷貝到應(yīng)用程序空間緩沖區(qū),read()系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到應(yīng)用程序空間的上下文切換。
發(fā)出write()系統(tǒng)調(diào)用,導(dǎo)致應(yīng)用程序空間到內(nèi)核空間的上下文切換,將應(yīng)用程序緩沖區(qū)的數(shù)據(jù)拷貝到網(wǎng)絡(luò)堆棧相關(guān)的內(nèi)核緩沖區(qū)(socket緩沖區(qū))。
write()系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到應(yīng)用程序空間的上下文切換,數(shù)據(jù)被拷貝到網(wǎng)卡子系統(tǒng)進(jìn)行打包發(fā)送。
以上可以發(fā)現(xiàn),傳統(tǒng)的Linux系統(tǒng)I/O 操作要進(jìn)行4次內(nèi)核空間與應(yīng)用程序空間的上下文切換,以及4次數(shù)據(jù)拷貝。
image.png
硬件系統(tǒng)的支持
DMA
直接內(nèi)存訪問(wèn)(Direct Memory Access,DMA)是計(jì)算機(jī)科學(xué)中的一種內(nèi)存訪問(wèn)技術(shù),允許某些電腦內(nèi)部的硬件子系統(tǒng)獨(dú)立地讀取系統(tǒng)內(nèi)存,而不需要中央處理器(CPU)的介入。在同等程度的處理器負(fù)擔(dān)下,DMA是一種快速的數(shù)據(jù)傳送方式。這類子系統(tǒng)包括硬盤控制器、顯卡、網(wǎng)卡和聲卡。
文件cache(buffer cache/page cache)
在Linux系統(tǒng)中,當(dāng)應(yīng)用程序需要讀取文件中的數(shù)據(jù)時(shí),操作系統(tǒng)先分配一些內(nèi)存,將數(shù)據(jù)從存儲(chǔ)設(shè)備讀入到這些內(nèi)存中,然后再將數(shù)據(jù)傳遞應(yīng)用進(jìn)程;當(dāng)需要往文件中寫數(shù)據(jù)時(shí),操作系統(tǒng)先分配內(nèi)存接收用戶數(shù)據(jù),然后再將數(shù)據(jù)從內(nèi)存寫入磁盤。文件cache管理就是對(duì)這些由操作系統(tǒng)分配并用開存儲(chǔ)文件數(shù)據(jù)的內(nèi)存的管理。
在Linux系統(tǒng)中,文件cache分為兩個(gè)層面,page cache 與 Buffer cache,每個(gè)page cache包含若干個(gè)buffer cache。操作系統(tǒng)中,磁盤文件都是由一系列的數(shù)據(jù)塊(Block)組成,buffer cache也叫塊緩存,是對(duì)磁盤一個(gè)數(shù)據(jù)塊的緩存,目的是為了在程序多次訪問(wèn)同一個(gè)磁盤塊時(shí)減少訪問(wèn)時(shí)間;而文件系統(tǒng)對(duì)數(shù)據(jù)的組織形式為頁(yè),page cache為頁(yè)緩存,是由多個(gè)塊緩存構(gòu)成,其對(duì)應(yīng)的緩存數(shù)據(jù)塊在磁盤上不一定是連續(xù)的。也就是說(shuō)buffer cache緩存文件的具體內(nèi)容--物理磁盤上的磁盤塊,加速對(duì)磁盤的訪問(wèn),而page cache緩存文件的邏輯內(nèi)容,加速對(duì)文件內(nèi)容的訪問(wèn)。
buffer cache的大小一般為1k,page cache在32位系統(tǒng)上一般為4k,在64位系統(tǒng)上一般為8k。磁盤數(shù)據(jù)塊、buffer cache、page cache及文件的關(guān)系如下圖:
image.png
文件cache的目的是加快對(duì)數(shù)據(jù)文件的訪問(wèn),同時(shí)會(huì)有一個(gè)預(yù)讀過(guò)程。對(duì)于每個(gè)文件的第一次讀請(qǐng)求,系統(tǒng)會(huì)讀入所請(qǐng)求的頁(yè)面并讀入緊隨其后的幾個(gè)頁(yè)面;對(duì)于第二次讀請(qǐng)求,如果所讀頁(yè)面在cache中,則會(huì)直接返回,同時(shí)又一個(gè)異步預(yù)讀的過(guò)程(將讀取頁(yè)面的下幾頁(yè)讀入cache中),如果不在cache中,說(shuō)明讀請(qǐng)求不是順序讀,則會(huì)從磁盤中讀取文件內(nèi)容并刷新cache。因此在順序讀取情況下,讀取數(shù)據(jù)的性能近乎內(nèi)存讀取。
DMA允許硬件子系統(tǒng)直接將數(shù)據(jù)從磁盤讀取到內(nèi)核緩沖區(qū),那么在一次數(shù)據(jù)傳輸中,磁盤與內(nèi)核緩沖區(qū),輸出設(shè)備與內(nèi)核緩沖區(qū)之間的兩次數(shù)據(jù)拷貝就不需要CPU進(jìn)行調(diào)度,CPU只需要進(jìn)行緩沖區(qū)管理、以及創(chuàng)建和處理DMA。而Page Cache/Buffer Cache的預(yù)讀取機(jī)制則加快了數(shù)據(jù)的訪問(wèn)效率。如下圖所示,還是以文件服務(wù)器請(qǐng)求為例,此時(shí)CPU負(fù)責(zé)的數(shù)據(jù)拷貝次數(shù)減少了兩次,數(shù)據(jù)傳輸性能有了較大的提高。
使用DMA的系統(tǒng)I/O操作要進(jìn)行4次內(nèi)核空間與應(yīng)用程序空間的上下文切換,2次CPU數(shù)據(jù)拷貝及2次DMA數(shù)據(jù)拷貝。
image.png
系統(tǒng)調(diào)用的豐富
Mmap
mmap()系統(tǒng)調(diào)用
Mmap內(nèi)存映射與標(biāo)準(zhǔn)I/O操作的區(qū)別在于當(dāng)應(yīng)用程序需要訪問(wèn)數(shù)據(jù)時(shí),不需要進(jìn)行內(nèi)核緩沖區(qū)到應(yīng)用程序緩沖區(qū)之間的數(shù)據(jù)拷貝。Mmap使得應(yīng)用程序和操作系統(tǒng)共享內(nèi)核緩沖區(qū),應(yīng)用程序直接對(duì)內(nèi)核緩沖區(qū)進(jìn)行讀寫操作,不需要進(jìn)行數(shù)據(jù)拷貝。Linux系統(tǒng)中通過(guò)調(diào)用mmap()替代read()操作。
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
同樣以文件服務(wù)器獲取文件(不加修改)為例,通過(guò)mmap操作的一次系統(tǒng)I/O過(guò)程如下:
應(yīng)用程序發(fā)出mmap()系統(tǒng)調(diào)用,如果文件數(shù)據(jù)不在緩沖區(qū)中,通過(guò)DMA將磁盤文件中的內(nèi)容拷貝到內(nèi)核緩沖區(qū)(頁(yè)緩存)。
mmap系統(tǒng)調(diào)用返回,應(yīng)用程序空間與內(nèi)核空間共享這個(gè)緩沖區(qū),應(yīng)用程序可以像操作應(yīng)用程序緩沖區(qū)中的數(shù)據(jù)一樣操作這個(gè)由內(nèi)核空間共享的緩沖區(qū)數(shù)據(jù)。
應(yīng)用程序發(fā)出write()系統(tǒng)調(diào)用,將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到內(nèi)核空間網(wǎng)絡(luò)堆棧相關(guān)聯(lián)的緩沖區(qū)(如socket緩沖區(qū))。
write系統(tǒng)調(diào)用返回,通過(guò)DMA將socket緩沖區(qū)中的數(shù)據(jù)傳遞給網(wǎng)卡子系統(tǒng)進(jìn)行打包發(fā)送。
通過(guò)以上流程可以看到,數(shù)據(jù)拷貝從原來(lái)的4次變?yōu)?次,2次DMA拷貝1次內(nèi)核空間數(shù)據(jù)拷貝,CPU只需要調(diào)控1次內(nèi)核空間之間的數(shù)據(jù)拷貝,CPU花費(fèi)在數(shù)據(jù)拷貝上的時(shí)間進(jìn)一步減少(4次上下文切換沒(méi)有改變)。對(duì)于大容量文件讀寫,采用mmap的方式其讀寫效率和性能都比較高。(數(shù)據(jù)頁(yè)較多,需要多次拷貝)
image.png
注:mmap()是讓應(yīng)用程序空間與內(nèi)核空間共享DMA從磁盤中讀取的文件緩沖,也就是應(yīng)用程序能直接讀寫這部分PageCache,至于上圖中從頁(yè)緩存到socket緩沖區(qū)的數(shù)據(jù)拷貝只是文件服務(wù)器的處理,根據(jù)應(yīng)用程序的不同會(huì)有不同的處理,應(yīng)用程序也可以讀取數(shù)據(jù)后進(jìn)行修改。重點(diǎn)是虛擬內(nèi)存映射,內(nèi)核緩存共享。
JDK NIO MappedByteBuffer 與 mmap
djk中nio包下的MappedByteBuffer,官方注釋為A direct byte buffer whose content is a memory-mapped region of a file,即直接字節(jié)緩沖區(qū),其內(nèi)容是文件的內(nèi)存映射區(qū)域。FileChannel是是nio操作文件的類,其map()方法在在實(shí)現(xiàn)類中調(diào)用native map0()本地方法,該方法通過(guò)mmap()實(shí)現(xiàn),因此是將文件從磁盤讀取到內(nèi)核緩沖區(qū),用戶應(yīng)用程序空間直接操作內(nèi)核空間共享的緩沖區(qū),Java程序通過(guò)MappedByteBuffer的get()方法獲取內(nèi)存數(shù)據(jù)。
MappedByteBuffer允許Java程序直接從內(nèi)存訪問(wèn)文件,可以將整個(gè)文件或文件的一部分映射到內(nèi)存中,由操作系統(tǒng)進(jìn)行相關(guān)的請(qǐng)求并將內(nèi)存中的修改寫入到磁盤中。
FileChannel map有三種模式
// 只讀模式,不能進(jìn)行寫操作
public static final MapMode READ_ONLY = new MapMode("READ_ONLY");
// 可讀可寫模式,對(duì)緩沖區(qū)的修改最終會(huì)同步到文件中
public static final MapMode READ_WRITE = new MapMode("READ_WRITE");
// 私有模式,對(duì)緩沖區(qū)數(shù)據(jù)進(jìn)行修改時(shí),被修改部分緩沖區(qū)會(huì)拷貝一份到應(yīng)用程序空間,copy-on-write
public static final MapMode PRIVATE = new MapMode("PRIVATE");
MappedByteBuffer的應(yīng)用,以rocketMQ為例(簡(jiǎn)單介紹)。
producer端發(fā)送消息最終會(huì)被寫入到commitLog文件中,consumer端消費(fèi)時(shí)先從訂閱的consumeQueue中讀取持久化消息的commitLogOffset、size等內(nèi)容,隨后再根據(jù)offset、size從commitLog中讀取消息的真正實(shí)體內(nèi)容。其中,commitLog是混合部署的,所有topic下的消息隊(duì)列共用一個(gè)commitLog日志數(shù)據(jù)文件,consumeQueue類似于索引,同時(shí)區(qū)分開不同topic下不同MessageQueue的消息。
rocketMQ利用MappedByteBuffer及PageCache加速對(duì)持久化文件的讀寫操作。rocketMQ通過(guò)MappedByteBuffer將日志數(shù)據(jù)文件映射到OS的虛擬內(nèi)存中(PageCache),寫消息時(shí)首先寫入PageCache,通過(guò)刷盤方式(異步或同步)將消息批量持久化到磁盤;consumer消費(fèi)消息時(shí),讀取consumeQueue是順序讀取的,雖然有多個(gè)消費(fèi)者操作不同的consumeQueue,對(duì)混合部署的commitLog的訪問(wèn)時(shí)隨機(jī)的,但整體上是從舊到新的有序讀,加上PageCache的預(yù)讀機(jī)制,大部分情況下消息還是從PageCache中讀取,不會(huì)產(chǎn)生太多的缺頁(yè)中斷(要讀取的消息不在pageCache中)而從磁盤中讀取。
rocketMQ利用mmap()使程序與內(nèi)核空間共享內(nèi)核緩沖區(qū),直接對(duì)PageCache中的文件進(jìn)行讀寫操作,加速對(duì)消息的讀寫請(qǐng)求,這是其高吞吐量的重要手段。
image.png
Mmap的問(wèn)題
使用mmap能減少CPU數(shù)據(jù)拷貝的次數(shù),但也存在一些問(wèn)題。
當(dāng)對(duì)文件進(jìn)行內(nèi)存映射,然后調(diào)用write(),如果此時(shí)有其他進(jìn)程截?cái)啵╰runcate)了這個(gè)文件,write()調(diào)用會(huì)因?yàn)樵L問(wèn)非法地址而被SIGBUS信號(hào)終止,SIGBUS信號(hào)默認(rèn)會(huì)殺掉進(jìn)程。解決方法一個(gè)是設(shè)置新的信號(hào)處理器,當(dāng)發(fā)生上述情況時(shí)不結(jié)束進(jìn)程;另一個(gè)是在對(duì)文件進(jìn)行內(nèi)存映射前使用文件租借鎖。
mmap內(nèi)存映射的大小是有限制的,其大小受OS虛擬內(nèi)存大小的限制,一般只能映射1.5-2G的文件至用戶態(tài)的虛擬內(nèi)存空間。對(duì)于MappedByteBuffer來(lái)說(shuō),虛擬內(nèi)存是不屬于jvm堆內(nèi)存的,所以大小不受JVM的-Xmx參數(shù)限制,且Java應(yīng)用程序無(wú)法占用全部的虛擬內(nèi)存(其他進(jìn)程使用)。這也是rocketMQ默認(rèn)設(shè)置單個(gè)commitLog日志數(shù)據(jù)文件大小為1G的原因。
sendfile
sendfile()流程
從Linux2.1開始,Linux引入sendfile()簡(jiǎn)化操作。取消read()/write(),mmap()/write()。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
調(diào)用sendfile的流程如下:
應(yīng)用程序調(diào)用sendfile(),導(dǎo)致應(yīng)用程序空間到內(nèi)核空間的上下文切換,數(shù)據(jù)如果不在緩存中,則通過(guò)DMA將數(shù)據(jù)從磁盤拷貝到內(nèi)核緩沖區(qū),然后再將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到內(nèi)核空間socket緩沖區(qū)中。
sendfile()返回,導(dǎo)致內(nèi)核空間到應(yīng)用程序空間的上下文切換。通過(guò)DMA將內(nèi)核空間socket緩沖區(qū)的數(shù)據(jù)發(fā)送到網(wǎng)卡子系統(tǒng)進(jìn)行打包發(fā)送。
image.png
通過(guò)sendfile()的I/O進(jìn)行了2次應(yīng)用程序空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝,其中2次是DMA拷貝,1次是CPU拷貝。sendfile相比起mmap,數(shù)據(jù)信息沒(méi)有進(jìn)入到應(yīng)用程序空間,所以能減少2次上下文切換的開銷,而數(shù)據(jù)拷貝次數(shù)是一樣的。
上述流程也可以看出,sendfile()適合對(duì)文件不加修改的I/O操作。
帶有DMA收集拷貝功能的sendfile
sendfile()只是減少應(yīng)用程序空間與內(nèi)核空間的上下文切換,并沒(méi)有減少CPU數(shù)據(jù)拷貝的次數(shù),還存在一次內(nèi)核空間的兩個(gè)緩沖區(qū)的數(shù)據(jù)拷貝。要實(shí)現(xiàn)CPU零數(shù)據(jù)拷貝,需要引入一些硬件上的支持。在上一小節(jié)的sendfile流程中,數(shù)據(jù)需要從內(nèi)核緩沖區(qū)拷貝到內(nèi)核空間socket緩沖區(qū),數(shù)據(jù)都是在內(nèi)核空間,如果socket緩沖區(qū)到網(wǎng)卡的這次DMA數(shù)據(jù)傳輸操作能直接讀取到內(nèi)核緩沖區(qū)中的數(shù)據(jù),那么這一次的CPU數(shù)據(jù)拷貝也就能避免。要達(dá)到這個(gè)目的,DMA需要知道存有文件位置和長(zhǎng)度信息的緩沖區(qū)描述符,即socket緩沖區(qū)需要從內(nèi)核緩沖區(qū)接收這部分信息,DMA需要支持?jǐn)?shù)據(jù)收集功能。