1 传统方式
Linux 原始的数据拷贝操作是怎样的。假如一个应用需要从某个磁盘文件中读取内容通过网络发出去,像这样:
while((n = read(diskfd, buf, BUF_SIZE)) > 0)
write(sockfd, buf , n);
那么整个过程就需要经历:
- 1)read 将数据从磁盘文件通过 DMA 等方式拷贝到内核开辟的缓冲区;
- 2)数据从内核缓冲区复制到用户态缓冲区;
- 3)write 将数据从用户态缓冲区复制到内核协议栈开辟的 socket 缓冲区;
- 4)数据从 socket 缓冲区通过 DMA 拷贝到网卡上发出去。

2 用户态直接I/O
这种方法可以使应用程序或者运行在用户态下的库函数直接访问硬件设备,数据直接跨过内核进行传输,内核在整个数据传输过程除了会进行必要的虚拟存储配置工作之外,不参与其他任何工作,这种方式能够直接绕过内核,极大提高了性能。
缺陷:
- 1)这种方法只能适用于那些不需要内核缓冲区处理的应用程序,这些应用程序通常在进程地址空间有自己的数据缓存机制,称为自缓存应用程序,如数据库管理系统就是一个代表。
- 2)这种方法直接操作磁盘 I/O,由于 CPU 和磁盘 I/O 之间的执行时间差距,会造成资源的浪费,解决这个问题需要和异步 I/O 结合使用。
3 mmap
这种方法,使用 mmap 来代替 read,可以减少一次拷贝操作,如下:
buf = mmap(diskfd, len);
write(sockfd, buf, len);
应用程序调用 mmap ,磁盘文件中的数据通过 DMA 拷贝到内核缓冲区,接着操作系统会将这个缓冲区与应用程序共享,这样就不用往用户空间拷贝。应用程序调用write ,操作系统直接将数据从内核缓冲区拷贝到 socket 缓冲区,最后再通过 DMA 拷贝到网卡发出去。
缺陷:
- 1)mmap 隐藏着一个陷阱,当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,如果服务器被这样终止了,那损失就可能不小了。
解决这个问题通常使用文件的租借锁:
- 首先为文件申请一个租借锁,当其他进程想要截断这个文件时,内核会发送一个实时的 RT_SIGNAL_LEASE 信号,告诉当前进程有进程在试图破坏文件,这样 write 在被 SIGBUS 杀死之前,会被中断,返回已经写入的字节数,并设置 errno 为 success。
4 sendfile
从Linux 2.1版内核开始,Linux引入了sendfile,也能减少一次拷贝。
#include
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile 是只发生在内核态的数据传输接口,没有用户态的参与,自然避免了用户态数据拷贝。
它指定在 in_fd 和 out_fd 之间传输数据,其中,它规定 in_fd 指向的文件必须是可以 mmap 的,out_fd 必须指向一个套接字,也就是规定数据只能从文件传输到套接字,反之则不行。sendfile 不存在像 mmap 时文件被截获的情况,它自带异常处理机制。
缺陷:
-
5 DMA 辅助的 sendfile
常规 sendfile 还有一次内核态的拷贝操作,能不能也把这次拷贝给去掉呢?
答案就是这种 DMA 辅助的 sendfile。
这种方法借助硬件的帮助,在数据从内核缓冲区到 socket 缓冲区这一步操作上,并不是拷贝数据,而是拷贝缓冲区描述符,待完成后,DMA 引擎直接将数据从内核缓冲区拷贝到协议引擎中去,避免了最后一次拷贝。
缺陷: 1)除了3.4 中的缺陷,还需要硬件以及驱动程序支持。
-
6 缓冲区共享
目前比较成熟的一个方案是最先在 Solaris 上实现的 fbuf (Fast Buffer,快速缓冲区)。Fbuf 的思想是每个进程都维护着一个缓冲区池,这个缓冲区池能被同时映射到程序地址空间和内核地址空间,内核和用户共享这个缓冲区池,这样就避免了拷贝。

缺陷: 1)管理共享缓冲区池需要应用程序、网络软件、以及设备驱动程序之间的紧密合作
- 2)改写 API ,尚处于试验阶段。
