一文讀懂零拷貝技術|splice 使用
服務端要向客戶端連接發送一個文件,一般過程如下:
-
服務端首先調用
read()
函數讀取文件內容。 -
服務端通過調用
write()/send()
函數將文件內容發送給客戶端連接。
上面過程如下圖所示:
從上圖可以看出,在發送文件的過程中,首先需要將文件頁緩存(Page Cache)從內核態複製到用戶態緩存中,然後再從用戶態緩存複製到客戶端的 Socket 緩衝區中。
其實在上面的過程中,複製文件數據到用戶態緩存這個操作是多餘的,我們完全可以直接把文件頁緩存的數據複製到 Socket 緩衝區即可,這樣就可以減少一次拷貝數據的操作。
爲了實現這樣的功能,內核提供了一個名爲 splice()
的系統調用,使用 splice()
系統調用可以避免從內核態拷貝數據到用戶態。
不需要將內核態的數據拷貝到用戶態緩存的技術被稱爲:
零拷貝技術
。
下面我們將介紹 splice()
系統調用的原理和實現。
splice 使用實例
如果服務端要發送文件給客戶端,使用 read()/write()
方式來實現的話,代碼如下所示:
/**
* 發送文件給客戶端(read/write版本)
*/
int send_file_to_client(int client_fd, char *file)
{
int fd;
struct stat fstat;
int blocks, remain;
char buf[4096]; // 每次發送4096個字節
fd = open(file, O_RDONLY);
if (fd == -1) {
return -1;
}
stat(file, &fstat); // 用於獲取文件的大小
blocks = fstat.st_size / 4096; // 需要發送的次數
remain = fstat.st_size % 4096; // 如果文件的大小不是4096的倍數,要額外發送這些數據
for (i = 0; i < blocks; i++) {
read(fd, buf, 4096); // 讀取文件內容
write(client_fd, buf, 4096); // 發送文件內容給客戶端
}
if (remain > 0) {
read(fd, buf, remain);
write(client_fd, buf, remain);
}
return 0;
}
上面代碼的流程比較簡單,如下:
-
首先通過調用
stat()
系統調用獲取文件的大小。 -
然後通過調用
read()
系統調用讀取文件內容。 -
最後通過調用
write()
系統調用將文件內容發送給客戶端連接。
從上面的代碼可以看出,使用 read()/write()
方式發送文件給客戶端,首先需要將文件內容讀到用戶態緩存中,然後才能發送給客戶端連接。
然而,將文件內容讀取到用戶態緩存這個過程是多餘的,我們看看怎麼使用 splice()
系統調用來避免將文件內容拷貝到用戶態緩存。
使用 splice()
發送文件時,需要創建一個管道作爲中轉,代碼如下:
/**
* 發送文件給客戶端(splice版本)
*/
int send_file_to_client(int client_fd, char *file)
{
int fd;
struct stat fstat;
int blocks, remain;
int pipefd[2];
fd = open(file, O_RDONLY);
if (fd == -1) {
return -1;
}
stat(file, &fstat);
blocks = fstat.st_size / 4096;
remain = fstat.st_size % 4096;
pipe(pipefd); // 創建管道作爲中轉
for (i = 0; i < blocks; i++) {
// 1. 將文件內容讀取到管道
splice(fd, NULL, pipefd[1], NULL, 4096, SPLICE_F_MOVE|SPLICE_F_MORE);
// 2. 將管道的數據發送給客戶端連接
splice(pipefd[0], NULL, client_fd, NULL, 4096, SPLICE_F_MOVE|SPLICE_F_MORE);
}
if (remain > 0) {
splice(fd, NULL, pipefd[1], NULL, remain, SPLICE_F_MOVE|SPLICE_F_MORE);
splice(pipefd[0], NULL, client_fd, NULL, remain, SPLICE_F_MOVE|SPLICE_F_MORE);
}
return 0;
}
從上面代碼可以看出,使用 splice()
發送文件時,我們並不需要將文件內容讀取到用戶態緩存中,但需要使用管道作爲中轉(關於 管道
的原理可以參考這篇文章:《圖解 | Linux 進程通信 - 管道實現》)。
其實這裏的管道只是作爲一個通道,並不會產生數據拷貝的,如下圖所示:
對比 read()/write()
版本的原理圖,可以看出 splice()
版本省去了拷貝文件內容到用戶態緩存這個步驟。
總結
本文主要介紹了使用 read()/write()
方式傳輸文件與使用 splice()
方式傳輸文件的原理,也提供了這兩種方式的實例代碼。
當然,從原理上看,使用 splice()
方式傳輸文件會比 read()/write()
方式性能要高。但如果真實測試這兩種方式,會發現性能相差並不大。這是由於 splice()
方式雖然減少了數據拷貝過程,但是其處理邏輯比 read()/write()
方式更爲複雜,所以性能提升並不理想,有興趣的讀者可以自己測試一下。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4SFoh_Wmuvq83qVXHCLt4w