術語
零拷貝
"零拷貝"中的"拷貝"是操作系統在I/O操作中,將數據從一個內存區域復制到另外一個內存區域. 而"零"並不是指0次復制, 更多的是指在用戶態和內核態之前的復制是0次.
CPU COPY
通過計算機的組成原理我們知道, 內存的讀寫操作是需要CPU的協調數據總線,地址總線和控制總線來完成的
因此在"拷貝"發生的時候,往往需要CPU暫停現有的處理邏輯,來協助內存的讀寫.這種我們稱為CPU COPY
cpu copy不但占用了CPU資源,還占用了總線的帶寬.
DMA COPY
DMA(DIRECT MEMORY ACCESS)是現代計算機的重要功能. 它的一個重要 的特點就是, 當需要與外設進行數據交換時, CPU只需要初始化這個動作便可以繼續執行其他指令,剩下的數據傳輸的動作完全由DMA來完成
可以看到DMA COPY是可以避免大量的CPU中斷的
上下文切換
本文中的上下文切換時指由用戶態切換到內核態, 以及由內核態切換到用戶態
存在多次拷貝的原因
-
操作系統為了保護系統不被應用程序有意或無意地破壞,為操作系統設置了用戶態和內核態兩種狀態.用戶態想要獲取系統資源(例如訪問硬盤), 必須通過系統調用進入到內核態, 由內核態獲取到系統資源,再切換回用戶態返回應用程序.
-
出於"readahead cache"和異步寫入等等性能優化的需要, 操作系統在內核態中也增加了一個"內核緩沖區"(kernel buffer). 讀取數據時並不是直接把數據讀取到應用程序的buffer, 而先讀取到kernel buffer, 再由kernel buffer復制到應用程序的buffer. 因此,數據在被應用程序使用之前,可能需要被多次拷貝
都有哪些不必要的拷貝
再回答這個問題之前, 我們先來看一個應用場景
回想現實世界的所有系統中, 不管是web應用服務器, ftp服務器,數據庫服務器, 靜態文件服務器等等, 所有涉及到數據傳輸的場景, 無非就一種:
從硬盤上讀取文件數據, 發送到網絡上去.
這個場景我們簡化為一個模型:
File.read(fileDesc, buf, len); Socket.send(socket, buf, len);
為了方便描述,上面這兩行代碼, 我們給它起個名字: read-send模型
操作系統在實現這個read-send模型時,需要有以下步驟:
1. 應用程序開始讀文件的操作 2. 應用程序發起系統調用, 從用戶態切換到內核態(第一次上下文切換) 3. 內核態中把數據從硬盤文件讀取到內核中間緩沖區(kernel buf) 4. 數據從內核中間緩沖區(kernel buf)復制到(用戶態)應用程序緩沖區(app buf),從內核態切換回到用戶態(第二次上下文切換) 5. 應用程序開始發送數據到網絡上 6. 應用程序發起系統調用,從用戶態切換到內核態(第三次上下文切換) 7. 內核中把數據從應用程序(app buf)的緩沖區復制到socket的緩沖區(socket) 8. 內核中再把數據從socket的緩沖區(socket buf)發送的網卡的緩沖區(NIC buf)上 9. 從內核態切換回到用戶態(第四次上下文切換)
如下圖表示:
由上圖可以很清晰地看到, 一次read-send涉及到了四次拷貝:
1. 硬盤拷貝到內核緩沖區(DMA COPY) 2. 內核緩沖區拷貝到應用程序緩沖區(CPU COPY) 3. 應用程序緩沖區拷貝到socket緩沖區(CPU COPY) 4. socket buf拷貝到網卡的buf(DMA COPY)
其中涉及到2次cpu中斷, 還有4次的上下文切換
很明顯,第2次和第3次的的copy只是把數據復制到app buffer又原封不動的復制回來, 為此帶來了兩次的cpu copy和兩次上下文切換, 是完全沒有必要的
linux的零拷貝技術就是為了優化掉這兩次不必要的拷貝
sendFile
linux內核2.1開始引入一個叫sendFile系統調用,這個系統調用可以在內核態內把數據從內核緩沖區直接復制到套接字(SOCKET)緩沖區內, 從而可以減少上下文的切換和不必要數據的復制
這個系統調用其實就是一個高級I/O函數, 函數簽名如下:
#include<sys/sendfile.h> ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);
- out_fd是寫出的文件描述符,而且必須是一個socket
- in_fd是讀取內容的文件描述符,必須是一個真實的文件, 不能是管道或socket
- offset是開始讀的位置
- count是將要讀取的字節數
有了sendFile這個系統調用后, 我們read-send模型就可以簡化為:
1. 應用程序開始讀文件的操作 2. 應用程序發起系統調用, 從用戶態切換到內核態(第一次上下文切換) 3. 內核態中把數據從硬盤文件讀取到內核中間緩沖區 4. 通過sendFile,在內核態中把數據從內核緩沖區復制到socket的緩沖區 5. 內核中再把數據從socket的緩沖區發送的網卡的buf上 6. 從內核態切換到用戶態(第二次上下文切換)
如下圖所示:
涉及到數據拷貝變成:
1. 硬盤拷貝到內核緩沖區(DMA COPY) 2. 內核緩沖區拷貝到socket緩沖區(CPU COPY) 3. socket緩沖區拷貝到網卡的buf(DMA COPY)
可以看到,一次read-send模型中, 利用sendFile系統調用后, 可以將4次數據拷貝減少到3次, 4次上下文切換減少到2次, 2次CPU中斷減少到1次
相對傳統I/O, 這種零拷貝技術通過減少兩次上下文切換, 1次cpu copy, 可以將I/O性能提高50%以上(網絡數據, 未親測)
開始的術語中說到, 所謂的零拷貝的"零", 是指用戶態和內核態之間的拷貝次數為0, 從這個定義上來說, 現在的這個零拷貝技術已經是真正的"零"了
然而, 對性能追求極致的偉大的科學家和工程師們並不滿足於此. 精益求精的他們對中間第2次的cpu copy依舊耿耿於懷, 想盡千方百計要去掉這一次沒有必要的數據拷貝和CPU中斷
支持scatter-gather特性的sendFile
在內核2.4以后的版本中, linux內核對socket緩沖區描述符做了優化. 通過這次優化, sendFile系統調用可以在只復制kernel buffer的少量元信息的基礎上, 把數據直接從kernel buffer 復制到網卡的buffer中去.從而避免了從"內核緩沖區"拷貝到"socket緩沖區"的這一次拷貝.
這個優化后的sendFile, 我們稱之為支持scatter-gather特性的sendFile
在支持scatter-gather特性的sendFile的支撐下, 我們的read-send模型可以優化為:
1. 應用程序開始讀文件的操作 2. 應用程序發起系統調用, 從用戶態進入到內核態(第一次上下文切換) 3. 內核態中把數據從硬盤文件讀取到內核中間緩沖區 4. 內核態中把數據在內核緩沖區的位置(offset)和數據大小(size)兩個信息追加(append)到socket的緩沖區中去 5. 網卡的buf上根據socekt緩沖區的offset和size從內核緩沖區中直接拷貝數據 6. 從內核態返回到用戶態(第二次上下文切換)
這個過程如下圖所示:
最后數據拷貝變成只有兩次DMA COPY:
1. 硬盤拷貝到內核緩沖區(DMA COPY) 2. 內核緩沖區拷貝到網卡的buf(DMA COPY)
完美
mmap和sendFile
MMAP(內存映射文件), 是指將文件映射到進程的地址空間去, 實現硬盤上的物理地址跟進程空間的虛擬地址的一一對應關系.
MMAP是另外一個用於實現零拷貝的系統調用.跟sendFile不一樣的地方是, 它是利用共享內存空間的方式, 避免app buf和kernel buf之間的數據拷貝(兩個buf共享同一段內存)
mmap相對於sendFile的好處:
- 多個進程訪問同一個文件時, 可以節省大量內存.
- 由於數據在內核中直接發送到網絡上, 用戶態中的應用程序無法再次操作數據.
mmap相對於sendFile的缺點:
- 當內存映射一個文件,然后調用write,而另一個進程截斷同一個文件,可能被總線錯誤信號SIGBUS中斷, 這個信號的默認行為是kill掉進程和dump core.這個是一般服務器不能接受的
- 連續順序訪問小文件時,不如sendFile的readahead cahce高效
參考
https://juejin.im/post/5c70d808e51d45370467c30e
https://www.jianshu.com/p/e9f422586749
https://www.ibm.com/developerworks/cn/java/j-zerocopy/