在每一次網絡io過程,數據都要通過幾個緩存,再發送出去。以下圖:html
以右側爲瀏覽器,左側爲httpd服務器爲例。nginx
須要注意,對於httpd端來講,若是網速很慢,而httpd子進程/線程須要響應出去的數據又足夠大(比send buffer還大),極可能會致使socket buffer填滿的狀況,這時write()函數會返回EWOULDBLOCK或EAGAIN,子進程/線程會進入等待狀態。web
對於瀏覽器一端來講,若是瀏覽器進程遲遲不將數據從socket buffer(recv buffer)中取走,極可能會致使socket buffer被填滿。瀏覽器
再來講httpd端網絡數據的"經歷"。以下圖:緩存
每次進程/線程須要一段數據時,老是先拷貝到kernel buffer,再拷貝到app buffer,再拷貝到socket buffer,最後再拷貝到網卡上。也就是說,老是會通過4段拷貝經歷。服務器
但想一想,正常狀況下,數據從存儲設備到kernel buffer是必須的,從socket buffer到NIC也是必須的,可是從kernel buffer到app buffer是必須的嗎?進程必定須要訪問、修改這些數據嗎?不必定,甚至對於web服務來講,若是不是要修改http響應報文,數據徹底能夠不用通過用戶空間。也就是不用再從kernel buffer拷貝到app buffer,這就是零複製的概念。網絡
零複製的概念是避免將數據在內核空間和用戶空間進行拷貝。主要目的是減小沒必要要的拷貝,避免讓CPU作大量的數據拷貝任務。app
注:上面只是說正常狀況下,例如某些硬件能夠完成TCP/IP協議棧的工做,數據能夠不通過socket buffer,直接在app buffer和硬件之間傳輸數據,RDMA技術就是在此基礎上實現的。socket
mmap()函數將文件直接映射到用戶程序的內存中,映射成功時返回指向目標區域的指針。這段內存空間能夠用做進程間的共享內存空間,內核也能夠直接操做這段空間。tcp
在映射文件以後,暫時不會拷貝任何數據到內存中,只有當訪問這段內存時,發現沒有數據,因而產生缺頁訪問,使用DMA操做將數據拷貝到這段空間中。能夠直接將這段空間的數據拷貝到socket buffer中。因此也算是零複製技術。如圖:
代碼以下:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
man文檔對此函數的描述:
sendfile() copies data between one file descriptor and another. Because this copying is done within the kernel, sendfile() is more efficient than the combination of read(2) and write(2), which would require transferring data to and from user space.
sendfile()函數借助文件描述符來實現數據拷貝:直接將文件描述in_fd的數據拷貝給文件描述符out_fd,其中in_fd是數據提供方,out_fd是數據接收方。文件描述符的操做都是在內核進行的,不會通過用戶空間,因此數據不用拷貝到app buffer,實現了零複製。以下圖
sendfile()的代碼以下:
#include<sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
可是sendfile的in_fd必須指向支持mmap的文件,也就是真實存在的文件,而不能是socket、管道等文件。在Linux 2.6.33以前,還限制out_fd必須是指向socket文件的描述符,因此人們總認爲它專門用於進行網絡數據拷貝。但從Linux 2.6.33開始,out_fd能夠是任何文件,且若是是一個普通文件,則sendfile()會合理地修改文件的offset。
以nginx開啓了tcp_nopush的sendfile爲例,當開啓了tcp_nopush功能後,nginx先在用戶空間構建響應首部,並放進socket send buffer中,而後再向sender buffer中寫入一個待加載文件的標識(例如,聲明我稍後要讀取a.txt文件中的數據發給你),這兩部分先發送給客戶端,而後再加載磁盤文件(sendfile模式加載),每擠滿一次send buffer就發送一次,直到全部數據都發送完。
man文檔對此函數的描述:
splice() moves data between two file descriptors without copying between kernel address space and user address space.
It transfers up to len bytes of data from the file descriptor fd_in to the file descriptor fd_out, where one of
thedescriptors must refer to a pipe.
splice()函數能夠在兩個文件描述符之間移動數據,且其中一個描述符必須是管道描述符。因爲不須要在kernel buffer和app buffer之間拷貝數據,因此實現了零複製。如圖:
注:因爲必須有一方是管道描述符,因此上圖中,若是是發送給socket文件描述符,那麼是沒有storage-->kernel buffer的DMA操做的。
代碼以下:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#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);
man文檔對此函數的描述:
tee() duplicates up to len bytes of data from the pipe referred to by the file descriptor fd_in to the pipe
referred to by the file descriptor fd_out. It does not consume the data that is duplicated from fd_in;
therefore, that data can be copied by a subsequent splice(2).
tee()函數在兩個管道描述符之間複製數據。因爲從in_fd複製給另外一個管道out_fd時,不認爲數據是來自於in_fd的,因此複製數據後,in_fd仍可以使用splice()函數進行數據移動。因爲沒有通過用戶空間,因此實現了零複製。如圖:
Linux下的tee程序就是使用tee函數結合splice函數實現的,先將數據經過tee()函數拷貝給管道,再使用splice()函數將數據移動給另外一個文件描述符。
代碼以下:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
當父進程fork生成子進程時,會複製它的全部內存頁。這至少會致使兩個問題:消耗大量內存;複製操做消耗時間。特別是fork後使用exec加載新程序時,因爲會初始化內存空間,因此複製操做幾乎是多餘的。
使用copy-on-write技術,使得在fork子進程時不復制內存頁,而是共享內存頁(也就是說,子進程也指向父進程的物理空間),只有在該子進程須要修改某一塊數據,纔會將這一塊數據拷貝到本身的app buffer中並進行修改,那麼這一塊數據就屬於該子進程的私有數據,可隨意訪問、修改、複製。這在必定程度上實現了零複製,即便複製了一些數據塊,也是在逐漸須要的過程進行復制的。
寫時複製內容太多,簡單概述的話大概就是上面所述內容。