一、零拷貝
在前面的文章“深淺拷貝、COW及零拷貝”中對零拷貝進行過分析,但沒有舉例子,也沒有深入進行展開分析。本文將結合實際的例程對零拷貝進行更深入的分析和說明。
在傳統的IO操作中,以文件通過網絡傳輸為例 ,一般會經歷以下幾個數據拷貝的過程:
磁盤緩沖區 ->內核緩沖區->用戶緩沖區->內核網絡緩沖區->網卡緩沖區
也就是數據要經歷從IO到內核空間,再從內核到用戶空間再進入內核空間然后才能通過IO發走,至少要有四次的內在拷貝。
而這就引出了零拷貝的概念:盡最大可能減少CPU參與數據拷貝的過程(直到完全不參與拷貝)。它主要有基于內核緩沖優化的零拷貝和DirectIO的零拷貝。
仍然以上面的鏈路來分析,可不可以直接從硬盤把數據(內核緩沖區)拷貝到網卡緩沖區,可不可以?可不可以不過用戶緩沖區直接在內核內交互數據?這都是直接想到的解決問題的方法和手段。而實際上,零拷貝技術也就是按這種指導思想進行開展的。
零拷貝技術的實現有以下幾種方法:
1、DirectIO
這個好理解,不通過各種中間環節直接和IO打交道。它主要應用于上層應用本身實現了磁盤的數據緩存,比如常見的數據庫系統軟件,那么就不需要再使用PageCache進行緩沖。這樣就可以減少PageCache(內核緩沖區)的消耗(這可略過了計算中最大的中間商CPU)。而諸如下面的sendfile等,其實都基于PageCache優化的零拷貝。
2、新的函數sendfile(win:TransmitFile)
sendfile是Linux系統提供的系統API,它可以解決用戶空間和內核空間的數據拷貝的次數問題;如果其和DMA技術(重點指SG-DMA(The Scatter-Gather Direct Memory Access))共同工作即sendfile+DMA,那么其效率更高,可以直接把數據文件從磁盤拷貝到網絡緩沖區 。
sendfile有其一定的局限性,首先是標準不統一,另外一個就是無法在數據操作中間在用戶空間對數據進行操作,比如從磁盤加載然后加解密等然后再發送,因為得不到具體的數據 ,這需要引起重視。
3、函數splice
splice技術更進一步,它接近于 sendfile和DMA的進一步效率提高,此函數在內核空間和網絡緩沖區間建立管道,避免二者的CPU的拷貝。注意,此函數中的兩個文件操作符必須有一個為管道操作符。
4、mmap
mmap方式大家比較熟悉,這里就簡單說明一下,其實mmap的零拷貝就是通過內存映射提供一個內核和用戶空間直接通信的手段。mmap應用非常多,最典型的是安卓的應用,Framework層的數據通信很多是用mmap為實現的。
5、tee
tee函數用來在兩個管道文件描述符間復制數據。它要求兩個文件描述符都必須為管道描述符;同時,它在復制過程中保持原數據不動直接復制fd,而splice是移動數據從源fd到目的fd。注意二者的區別和不同。
下面就分別對幾類技術實現方式進行舉例分析。在分析之前,先對原來的文章“深淺拷貝、COW及零拷貝”中零拷貝的圖進行一下完善:
主要是補齊了未描述清楚的普通DMA部分的流程。
二、sendfile
先看一下定義:
int main(int argc, char* argv[])
{
......int ffd = open(fname, O_RDONLY);//打開文件struct stat st;fstat(ffd, &st);struct sockaddr_in addr;bzero(&addr, sizeof(addr));addr.sin_family = AF_INET;inet_pton(AF_INET, ip, &addr.sin_addr);addr.sin_port = htons(static_cast<uint16_t>(port));int s = socket(PF_INET, SOCK_STREAM, 0);int reuse = 1;//設置端口重用setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));int ret = bind(s, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));ret = listen(s, 3);struct sockaddr_in client;socklen_t client_addrlen = sizeof(client);int cSocket = accept(s, reinterpret_cast<struct sockaddr*>(&client), &client_addrlen);if (cSocket < 0) {printf("accept err: %d\n", errno);}else {sendfile(cSocket, ffd, NULL, static_cast<size_t>(st.st_size));close(cSocket);}......return 0;
}
注意上面的代碼省略了相關的安全控制和參數賦值,大家可以自行設置,直接寫成固定的就可以,只是一個測試程序么。
三、splice
splice的應用也不復雜,但需要注意其中的一些要求,特別是參數中,在Linux2.6.21以前,splice的flags設置SPLICE_F_MOVE有效,其后就無效了,但SPLICE_F_NONBLOCK 和SPLICE_F_MORE都有效果。看一下例程:
#include <fcntl.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <libgen.h>
#include <assert.h>
#include <stdlib.h>int main(int argc, char* argv[])
{
......struct sockaddr_in addr;bzero(&addr, sizeof(addr));addr.sin_family = AF_INET;inet_pton(AF_INET, ip, &addr.sin_addr);addr.sin_port = htons(static_cast<uint16_t>(port));int sfd = socket(PF_INET, SOCK_STREAM, 0);int reuse = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));int r = bind(sockfd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));r = listen(sockfd, 3);struct sockaddr_in cSocket;socklen_t client_addrlen = sizeof(cSocket);int cfd = accept(sfd, reinterpret_cast<sockaddr*>(&cSocket), &client_addrlen);if (cfd < 0) {printf("accept err: %d\n", errno);}else {int pfd[2];ret = pipe(pfd);while (1) {ssize_t res;res = splice(cfd, NULL, pfd[1], NULL, 1024, SPLICE_F_MORE | SPLICE_F_MOVE);if (res == 0) { // 收到EOFbreak;}res = splice(pfd[0], NULL, cfd, NULL, 1024, SPLICE_F_MORE | SPLICE_F_MOVE);}close(cfd);}close(sfd);return 0;
}
相關的具體參數可以看說明文檔,還是相當清楚的。
四、tee和mmap
mmap的例子非常多,這里只給一個tee相關的例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <libgen.h>
#include <assert.h>int main(int argc, char* argv[])
{
......int ffd = open(argv[1], O_CREAT | O_TRUNC | O_WRONLY, 0666);int pfdout[2];int r = pipe(pfdout);assert(r != -1);int pfdfile[2];r = pipe(pfdfile);while (1) {ssize_t res = splice(STDIN_FILENO, NULL, pfdout[1], NULL, 1024, SPLICE_F_MORE | SPLICE_F_MOVE);if (res == 0) {break;}res = tee(pfdout[0], pfdfile[1], 1024, SPLICE_F_NONBLOCK);res = splice(pfdfile[0], NULL, ffd, NULL, 1024, SPLICE_F_MORE | SPLICE_F_MOVE);assert(res != -1);// 二次調用,因為第一次調用數據已經移動,所以splice函數阻塞//res = splice(pfdfile[0], NULL, STDOUT_FILENO, NULL, 1024, SPLICE_F_MORE | SPLICE_F_MOVE);}.......return 0;
}
這些都沒有什么難度,手冊上也都有相關的例程。
五、DMA技術和零拷貝
在上面的分析過程中可以清晰的知道,DMA技術和零拷貝既有千絲萬縷的聯系,又有所不同:
DMA技術是負責數據的直通,零拷貝重點是CPU不參與數據拷貝,但需要參與數據的管理(比如數據可以使用,開始操作等等),也就是說DMA技術和零拷貝技術中的CPU互相協作,達到數據拷貝的次數最少的目的。
零拷貝其實就是考慮減少從IO到用戶層的整個數據流程的拷貝次數從而提高效率,要始終抓住這條主線。DMA主要是拷貝,CPU重點是管理,即把CPU從既管理又復制中簡化工作任務,只管理即可。DMA技術和硬件關系很密切,所以在具體的開發使用中,要明確硬件是否支持相關具體的操作。
需要注意的另外一點是,在實際場景中,如果是非常大的數據文件處理,基于PageCache零拷貝技術則有些力不從心了,還是得使用Direct IO的零拷貝技術。
六、使用零拷貝的框架
說一些技術和概念可能理解并不深刻,可以參考一下相關的一些開源框架中使用的零拷貝技術:
1、KAFKA
使用sendfile的零拷貝技術
2、Nginx
提供了sendfile和directio的相關零拷貝技術
3、Mysql
使用了directio的零拷貝技術
4、Netty
使用sendfile的零拷貝技術
5、RocketMQ
使用了mmap write的零拷貝技術
七、總結
其實說得更淺顯一些,所謂零拷貝更準確的說不是零次拷貝,是指盡可能的減少拷貝。在DPDK的系列文章中,這種操作被發揮的淋漓盡致。互聯網的口號就是“不讓中間商賺差價”,這個在現實上可能有一些邏輯上的BUG,但在內存操作上確實是非常用益。
當然,萬事萬物不是說是絕對的,有的時候,抽象一下,加一層,如果能達到更好的效果,又不影響實際的使用的情況下,豈不更妙?千頭萬緒又回到始終堅持的原則,應用場景決定應用技術,實踐是檢驗真理的標準。