單獨(dú)從理論上理解可能會(huì)有些晦澀究飞,我在這從一個(gè)實(shí)際的場景進(jìn)行闡述舌劳,一步一步演進(jìn)Zero-Copy的原理。
概述
我們的系統(tǒng)哮针,不管是電商系統(tǒng)還是官網(wǎng)都是一種交互式請(qǐng)求響應(yīng)模式,不過今天不是講http的請(qǐng)求/響應(yīng)模式坦袍。打開一個(gè)頁面十厢,對(duì)于系統(tǒng)而言需要將系統(tǒng)生成的view靜態(tài)內(nèi)容(類似圖片、文件)展示給用戶捂齐,以此場景切入蛮放,系統(tǒng)需要先將靜態(tài)內(nèi)容從磁盤中拷貝出來放到一個(gè)內(nèi)存buf中,然后將這個(gè)buf通過socket傳輸給用戶奠宜,進(jìn)而用戶或者靜態(tài)內(nèi)容的展示包颁。這看起來再正常不過了瞻想,但是實(shí)際上這是很低效的流程,我們把上面的這種情形抽象成下面的過程:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
首先調(diào)用read將靜態(tài)內(nèi)容娩嚼,這里假設(shè)為文件File蘑险,讀取到tmp_buf, 然后調(diào)用write將tmp_buf寫入到socket中,如圖:
在這個(gè)過程中文件File的經(jīng)歷了4次copy的過程:
首先岳悟,調(diào)用read時(shí)佃迄,文件File拷貝到了kernel模式;
之后贵少,CPU控制將kernel模式數(shù)據(jù)copy到user模式下呵俏;
調(diào)用write時(shí),先將user模式下的內(nèi)容copy到kernel模式下的socket的buffer中春瞬;
最后將kernel模式下的socket buffer的數(shù)據(jù)copy到網(wǎng)卡設(shè)備中傳送;
從上面的過程可以看出套啤,數(shù)據(jù)白白從kernel模式到user模式走了一圈宽气,浪費(fèi)了2次copy(第一次,從kernel模式拷貝到user模式潜沦;第二次從user模式再拷貝回kernel模式萄涯,即上面4次過程的第2和3步驟。)唆鸡。而且上面的過程中kernel和user模式的上下文的切換也是4次涝影。
幸運(yùn)的是,你可以用一種叫做Zero-Copy的技術(shù)來去掉這些無謂的copy争占。應(yīng)用程序用Zero-Copy來請(qǐng)求kernel直接把disk的data傳輸給socket燃逻,而不是通過應(yīng)用程序傳輸。Zero-Copy大大提高了應(yīng)用程序的性能臂痕,并且減少了kernel和user模式上下文的切換伯襟。
javaNIO 改進(jìn)
Zero-Copy技術(shù)省去了將操作系統(tǒng)的read buffer拷貝到程序的buffer,以及從程序buffer拷貝到socket buffer的步驟握童,直接將read buffer拷貝到socket buffer. Java NIO中的FileChannal.transferTo()方法就是這樣的實(shí)現(xiàn)姆怪,這個(gè)實(shí)現(xiàn)是依賴于操作系統(tǒng)底層的sendFile()實(shí)現(xiàn)的。
public abstract long transferTo(long position, long count,
WritableByteChannel target)
底層調(diào)用sendfile方法
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
下圖為transferTo()之后的數(shù)據(jù)流向:
下圖展示了在使用transferTo()之后的上下文切換:
使用了Zero-Copy技術(shù)之后澡绩,整個(gè)過程如下:
transferTo()方法使得文件A的內(nèi)容直接拷貝到一個(gè)read buffer(kernel buffer)中稽揭;
然后數(shù)據(jù)(kernel buffer)拷貝到socket buffer中。
最后將socket buffer中的數(shù)據(jù)拷貝到網(wǎng)卡設(shè)備(protocol engine)中傳輸肥卡;
這顯然是一個(gè)偉大的進(jìn)步:這里把上下文的切換次數(shù)從4次減少到2次溪掀,同時(shí)也把數(shù)據(jù)copy的次數(shù)從4次降低到了3次。
但是這是Zero-Copy么步鉴,答案是否定的膨桥。
真正的Zero-Copy進(jìn)階
Linux 2.1內(nèi)核開始引入了sendfile函數(shù)(上一節(jié)有提到),用于將文件通過socket傳送蛮浑。
sendfile(socket, file, len);
該函數(shù)通過一次系統(tǒng)調(diào)用完成了文件的傳送,減少了原來read/write方式的模式切換只嚣。此外更是減少了數(shù)據(jù)的copy, sendfile的詳細(xì)過程如圖:
通過sendfile傳送文件只需要一次系統(tǒng)調(diào)用沮稚,當(dāng)調(diào)用sendfile時(shí):
首先(通過DMA)將數(shù)據(jù)從磁盤讀取到kernel buffer中;
然后將kernel buffer拷貝到socket buffer中册舞;
最后將socket buffer中的數(shù)據(jù)copy到網(wǎng)卡設(shè)備(protocol engine)中發(fā)送蕴掏;
這個(gè)過程就是第二節(jié)(詳述)中的那個(gè)步驟。
sendfile與read/write模式相比调鲸,少了一次copy盛杰。但是從上述過程中也可以發(fā)現(xiàn)從kernel buffer中將數(shù)據(jù)copy到socket buffer是沒有必要的。
Linux2.4 內(nèi)核對(duì)sendfile做了改進(jìn)藐石,如圖:
改進(jìn)后的處理過程如下:
將文件拷貝到kernel buffer中即供;
向socket buffer中追加當(dāng)前要發(fā)生的數(shù)據(jù)在kernel buffer中的位置和偏移量;
根據(jù)socket buffer中的位置和偏移量直接將kernel buffer的數(shù)據(jù)copy到網(wǎng)卡設(shè)備(protocol engine)中于微;
經(jīng)過上述過程逗嫡,數(shù)據(jù)只經(jīng)過了2次copy就從磁盤傳送出去了。這個(gè)才是真正的Zero-Copy(這里的零拷貝是針對(duì)kernel來講的株依,數(shù)據(jù)在kernel模式下是Zero-Copy)驱证。
正是Linux2.4的內(nèi)核做了改進(jìn),Java中的TransferTo()實(shí)現(xiàn)了Zero-Copy,如下圖:
Zero-Copy技術(shù)的使用場景有很多恋腕,比如Kafka, 又或者是Netty等抹锄,可以大大提升程序的性能。