在每一次网络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中并进行修改,那么这一块数据就属于该子进程的私有数据,可随意访问、修改、复制。这在必定程度上实现了零复制,即便复制了一些数据块,也是在逐渐须要的过程进行复制的。
写时复制内容太多,简单概述的话大概就是上面所述内容。