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