>
前言
在Linux系統內部緩存和內存容量都是有限的,更多的數據都是存儲在磁盤中。對于Web服務器來說,經常需要從磁盤中讀取數據到內存,然后再通過網卡傳輸給用戶:
那么這也算一次I O的過程,都知道IO過程中需要狀態的切換還有一系列拷貝過程,都是要時間開銷的,那么怎么優化用戶態和內核態的狀態的切換次數和各種緩沖區之間的拷貝次數,也是linux的服務器實現高并發的重要技術了!
傳統數據交互
傳統 io 的執行流程: 下面將圖左半部分read過程的硬件抽象為磁盤; 圖右半部分write過程的硬件設為網卡,模擬webserver進行一次IO的過程; 方便理解;
- read:將數據從 IO 設備讀取到內核緩存區中,再將數據從內核緩沖區拷貝到用戶緩沖區
- write:將數據從用戶緩沖區寫入到內核緩沖區中,再將數據從內核緩沖區拷貝到 IO 設備
read/write 屬于系統調用 syscall,每一次系統調用 ,發生兩次上下文切換
- 調用 syscall 從用戶態切換到內核態
- syscall 返回從內核態切換到用戶態
如圖所示,傳統 io 的過程中,發生了4次空間切換 + 4次拷貝
不難看出,傳統模式下的IO,涉及多次空間切換和數據冗余拷貝,效率并不高。而零拷貝 Zero-Copy 目的就是降低冗余數據拷貝,解放 CPU
- 減少數據在內核緩沖區和用戶緩沖區之間的冗余拷貝(CPU拷貝)
- 減少系統調用導致的空間切換
目前來看,零拷貝技術的實現手段主要包括:mmap+write、sendfile、sendfile+DMA、splice
零拷貝
首先解釋一下,零拷貝中的0,指的是CPU級別的數據拷貝(比如內核緩沖區到用戶緩沖區的拷貝,用戶緩沖區再到socket緩沖區; 或者內核緩沖區直接到socket緩沖區的拷貝!),并不是DMA硬件的拷貝,否則數據不靠DMA怎么轉移呢?
mmap+write
- 內存映射 memory mapping,mmap 是一種內存映射文件的方法,即將一個文件或者其他對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一映射關系。
mmap可以充當read的功能,將內核讀緩沖區地址與用戶緩沖區地址進行映射,實現內核緩沖區與用戶緩沖區的共享。這樣就減少了一次用戶態和內核態的CPU拷貝。
mmap + write 流程如圖所示,發生了4次切換 + 2次DMA拷貝 + 1次CPU拷貝
函數原型
#include <sys/mman.h> // 內存映射 void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset); /* 參數 start:指定映射的虛擬內存地址,通常定義為 NULL,由內核選定地址 length:映射的長度 prot:描述映射內存的訪問權限 PROT_EXEC頁面可以被 cpu 執行指令組成,PROT_NONE 頁面不能訪問 PROT_READ 頁面可讀,PROT_WRITE 頁面可寫, flags:指定映射的類型,MAP_SHARED共享對象,MAP_PRIVATE私有的,寫時復制對象 fd:要進行映射的文件句柄 offset:文件偏移量 */ // 解除映射 int munmap(void *addr, size_t length);
例: 發送方:
// 建立內存映射 char *pMap = (char*) mmap (NULL, fileInfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); send(clientFd, pMap, fileInfo.st_size, 0); // 解除映射 munmap(pMap, fileInfo.st_size);
接收方:
// 使用 mmap 前用使用 ftruncate 來擴大文件大小 ftruncate(fd, fileSize); char *pMap = (char*) mmap (NULL, fileSize, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); recvCycle(sfd, pMap, fileSize); munmap(pMap, fileSize);
小結
mmap充當read的功能,進行一次完整的IO,減少了傳統方式read數據的時候,從內核態CPU拷貝到用戶態的這次拷貝; (發生了4次切換 + 2次DMA拷貝 + 1次CPU拷貝;)
mmap 存在的問題:mmap 對大文件傳輸有一定優勢,但是小文件可能出現碎片,并且在多個進程同時操作文件時可能產生引發 coredump 的 signal。
sendfile
mmap+write 方式有一定改進,但是由系統調用引起的狀態切換并沒有減少,因此在 Linux 內核2.1版本中引入了 sendfile 系統調用。
sendfile 在兩個文件之間通過內核直接傳輸數據,避免了內核緩沖區和用戶緩沖區之間的數據拷貝操作。sendfile 只能用于發送數據,不能用于接收數據。
sendfile 方式只使用一個函數就可以完成之前的 read+write 和 mmap+write 的功能,這樣減少一個系統調用(2次狀態切換),由于數據不經過用戶緩沖區,因此該數據無法被修改。
sendfile 的流程如圖所示, 發生了2次切換 + 2次DMA拷貝+1次CPU拷貝
sendfile + DMA
linux2.4版本后,對 sendfile 系統調用進行優化,配合硬件 DMA,可以直接從內核空間緩沖區中將數據拷貝到網卡,徹底省去了CPU拷貝。
如圖所示,sendfile + DMA 的過程中發生了2次切換 + 2次DMA拷貝 + 0次CPU拷貝
sendfile 函數原型
#include <sys/sendfile.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); /* 參數 - out_fd:待寫入內容的文件描述符 - in_fd:待讀出內容的文件描述符 - offset:文件偏移量 - count:傳輸的字節數 */
例:
發送方
sendfile(clientFd, fd, 0, fileInfo.st_size);
小結
早期sendfile : 2次切換 (sendfile后,數據不用過用戶層了,導致不能修改了,不過也少了兩次狀態切換!)+ 2次DMA拷貝(磁盤到內核,socket緩沖區到網卡)+ 1次CPU拷貝(內核到socket緩沖區)
改良的sendfile + DMA : 發生了2次切換 + 2次DMA拷貝(磁盤到內核,內核直接到網卡) + 0次CPU拷貝
sendfile 存在的問題:無法對數據進行修改(數據沒上到用戶層,也沒必要,webserver一般都不需要修改,返回的本地的資源!),并且需要硬件層面DMA的支持,并且 sendfile 只能將文件數據拷貝到 socketfd,有一定的局限性。
splice
splice 系統調用在 Linux 2.6 版本引入,不需要硬件支持,并且不再限定于 socket 上,實現了兩個普通文件之間的零拷貝。
可以在內核緩沖區和 socket 緩沖區間建立管道來傳輸數據,避免了兩者之間的 CPU 拷貝操作。
函數原型
#define _GNU_SOURCE #include <fcntl.h> ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); /* 返回值;成功返回接收到的字節數,失敗-1 參數 - fd_in:待輸入數據的文件描述符。 - off_in: 輸入流偏移量。若 fd_in 是管道文件描述符,則設置為 NULL,表示從當前偏移讀入。 否則,off_in 表示從輸入數據流的某處開始讀取。 - fd_out:待輸出數據的文件描述符。 - off_out:輸出流偏移量,同上。 - len:單次寫入的數據長度,最多65536 - flags:0 */
例:web服務器端代碼: transFile.c:
int fds[2]; pipe(fds); int recvLen = 0; //當讀到的數據量超過文件大小時,即已經讀取數據完成 while(recvLen < fileInfo.st_size){ //將數據從服務器端本地讀到管道 ret = splice(fd, 0, fds[1], 0, 65536, 0); //將數據從管道讀到客戶端 ret = splice(fds[0], 0, clientFd, 0, ret, 0); //計算已經讀到的數據量 recvLen += ret; }
小結
splice 引入管道機制,實現了普通文件之間的0拷貝,突破了僅限于socket的sendfile0拷貝;
splice 存在的問題:它的兩個文件描述符中有一個必須是管道設備
>