Linux 百萬并發(fā)「零拷貝」實(shí)現(xiàn)原理
傳統(tǒng)的I/O操作讀取文件并通過Socket發(fā)送,需要經(jīng)過4次上下文切換、2次CPU數(shù)據(jù)拷貝和2次DMA控制器數(shù)據(jù)拷貝,如下圖:

操作系統(tǒng)層面的減少數(shù)據(jù)拷貝次數(shù)主要是指用戶空間和內(nèi)核空間的數(shù)據(jù)拷貝,因?yàn)橹挥兴麄兊目截愂谴罅肯腃PU時(shí)間片的,而DMA控制器拷貝數(shù)據(jù)CPU參與的工作較少,只是輔助作用。
現(xiàn)實(shí)中對(duì)零拷貝的概念有廣義和狹義之分,廣義上是指只要減少了數(shù)據(jù)拷貝的次數(shù)就稱之為零拷貝;狹義上是指真正的零拷貝,比如上例中避免2和3的CPU拷貝。
下面我們逐一看看他們的設(shè)計(jì)思想和實(shí)現(xiàn)方案

每個(gè)進(jìn)程都有自己的PageTable,進(jìn)程的虛擬內(nèi)存地址通過PageTable對(duì)應(yīng)于物理內(nèi)存,內(nèi)存分配具有惰性,它的過程一般是這樣的:進(jìn)程創(chuàng)建后新建與進(jìn)程對(duì)應(yīng)的PageTable,當(dāng)進(jìn)程需要內(nèi)存時(shí)會(huì)通過PageTable尋找物理內(nèi)存,如果沒有找到對(duì)應(yīng)的頁(yè)幀就會(huì)發(fā)生缺頁(yè)中斷,從而創(chuàng)建PageTable與物理內(nèi)存的對(duì)應(yīng)關(guān)系。虛擬內(nèi)存不僅可以對(duì)物理內(nèi)存進(jìn)行擴(kuò)展,還可以更方便地靈活分配,并對(duì)編程提供更友好的操作。
內(nèi)存映射(mmap)是指用戶空間和內(nèi)核空間的虛擬內(nèi)存地址同時(shí)映射到同一塊物理內(nèi)存,用戶態(tài)進(jìn)程可以直接操作物理內(nèi)存,避免用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝。

它的具體執(zhí)行流程是這樣的

用戶進(jìn)程通過系統(tǒng)調(diào)用mmap函數(shù)進(jìn)入內(nèi)核態(tài),發(fā)生第1次上下文切換,并建立內(nèi)核緩沖區(qū);
發(fā)生缺頁(yè)中斷,CPU通知DMA讀取數(shù)據(jù);
DMA拷貝數(shù)據(jù)到物理內(nèi)存,并建立內(nèi)核緩沖區(qū)和物理內(nèi)存的映射關(guān)系;
建立用戶空間的進(jìn)程緩沖區(qū)和同一塊物理內(nèi)存的映射關(guān)系,由內(nèi)核態(tài)轉(zhuǎn)變?yōu)橛脩魬B(tài),發(fā)生第2次上下文切換;
用戶進(jìn)程進(jìn)行邏輯處理后,通過系統(tǒng)調(diào)用Socket send,用戶態(tài)進(jìn)入內(nèi)核態(tài),發(fā)生第3次上下文切換;
系統(tǒng)調(diào)用Send創(chuàng)建網(wǎng)絡(luò)緩沖區(qū),并拷貝內(nèi)核讀緩沖區(qū)數(shù)據(jù);
DMA控制器將網(wǎng)絡(luò)緩沖區(qū)的數(shù)據(jù)發(fā)送網(wǎng)卡,并返回,由內(nèi)核態(tài)進(jìn)入用戶態(tài),發(fā)生第4次上下文切換;
避免了內(nèi)核空間和用戶空間的2次CPU拷貝,但增加了1次內(nèi)核空間的CPU拷貝,整體上相當(dāng)于只減少了1次CPU拷貝;
針對(duì)大文件比較適合mmap,小文件則會(huì)造成較多的內(nèi)存碎片,得不償失;
當(dāng)mmap一個(gè)文件時(shí),如果文件被另一個(gè)進(jìn)程截獲可能會(huì)因?yàn)榉欠ㄔL問導(dǎo)致進(jìn)程被SIGBUS 信號(hào)終止;
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);out_fd為文件描述符,in_fd為網(wǎng)絡(luò)緩沖區(qū)描述符,offset偏移量(默認(rèn)NULL),count文件大小。
它的內(nèi)部執(zhí)行流程是這樣的

用戶進(jìn)程系統(tǒng)調(diào)用senfile,由用戶態(tài)進(jìn)入內(nèi)核態(tài),發(fā)生第1次上下文切換;
CPU通知DMA控制器把文件數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū);
內(nèi)核空間自動(dòng)調(diào)用網(wǎng)絡(luò)發(fā)送功能并拷貝數(shù)據(jù)到網(wǎng)絡(luò)緩沖區(qū);
CPU通知DMA控制器發(fā)送數(shù)據(jù);
sendfile系統(tǒng)調(diào)用結(jié)束并返回,進(jìn)程由內(nèi)核態(tài)進(jìn)入用戶態(tài),發(fā)生第2次上下文切換;
總結(jié)
數(shù)據(jù)處理完全是由內(nèi)核操作,減少了2次上下文切換,整個(gè)過程2次上下文切換、1次CPU拷貝,2次DMA拷貝;
雖然可以設(shè)置偏移量,但不能對(duì)數(shù)據(jù)進(jìn)行任何的修改;

用戶進(jìn)程系統(tǒng)調(diào)用senfile,由用戶態(tài)進(jìn)入內(nèi)核態(tài),發(fā)生第1次上下文切換;
CPU通知DMA控制器把文件數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū);
把內(nèi)核緩沖區(qū)地址和sendfile的相關(guān)參數(shù)作為數(shù)據(jù)描述信息存在網(wǎng)絡(luò)緩沖區(qū)中;
CPU通知DMA控制器,DMA根據(jù)網(wǎng)絡(luò)緩沖區(qū)中的數(shù)據(jù)描述截取數(shù)據(jù)并發(fā)送;
sendfile系統(tǒng)調(diào)用結(jié)束并返回,進(jìn)程由內(nèi)核態(tài)進(jìn)入用戶態(tài),發(fā)生第2次上下文切換;
需要硬件支持,如DMA;
整個(gè)過程2次上下文切換,0次CPU拷貝,2次DMA拷貝,實(shí)現(xiàn)真正意義上的零拷貝;
依然不能修改數(shù)據(jù);
但那時(shí)的sendfile有個(gè)致命的缺陷,如果你查看Sendfild手冊(cè),你會(huì)發(fā)現(xiàn)如下描述

in_fd不僅僅不能是socket,而且在2.6.33之前Sendfile的out_fd必須是socket,因此sendfile幾乎成了專為網(wǎng)絡(luò)傳輸而設(shè)計(jì)的,限制了其使用范圍比較狹窄。2.6.33之后out_fd才可以是任何file,于是乎出現(xiàn)了splice。
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);它的執(zhí)行流程如下

用戶進(jìn)程系統(tǒng)調(diào)用splice,由用戶態(tài)進(jìn)入內(nèi)核態(tài),發(fā)生第1次上下文切換;
CPU通知DMA控制器把文件數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū);
建立內(nèi)核緩沖區(qū)和網(wǎng)絡(luò)緩沖區(qū)的管道;
CPU通知DMA控制器,DMA從管道讀取數(shù)據(jù)并發(fā)送;
splice系統(tǒng)調(diào)用結(jié)束并返回,進(jìn)程由內(nèi)核態(tài)進(jìn)入用戶態(tài),發(fā)生第2次上下文切換;
整個(gè)過程2次上下文切換,0次CPU拷貝,2次DMA拷貝,實(shí)現(xiàn)真正意義上的零拷貝;
依然不能修改數(shù)據(jù);
fd_in和fd_out必須有一個(gè)是管道;

推薦閱讀:
推薦一款,比 Navicat 還要好用,功能還很強(qiáng)大的 工具!
5T技術(shù)資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機(jī),樹莓派,等等。在公眾號(hào)內(nèi)回復(fù)「1024」,即可免費(fèi)獲?。?!


