以应用程序将磁盘上的文件发送到网络中为例
传统的文件拷贝
(1)应用程序在上层调用read( )函数
(2)从用户态切换到内核态(第一次状态切换)
(3)CPU命令DMA将数据从磁盘传输到内核内存(第一次拷贝)
(4)CPU将数据从内核内存拷贝到用户内存(第二次拷贝)
(5)从内核态切换到用户态(第二次状态切换)
至此这个文件被加载到了用户空间中
(6)应用程序在上层调用write( )函数
(7)从用户态切换到内核态(第三次状态切换)
(8)CPU将数据从用户空间拷贝到socket缓冲区(第三次拷贝)
(9)CPU命令DMA将数据从socket缓冲区拷贝到网卡(第四次拷贝)
(10)从内核态切换到用户态(第四次状态切换)
在传统的文件拷贝中,数据从磁盘发送到网卡,需要四次状态的切换,需要四次数据的拷贝。
这种情况下,存在冗余的状态切换和数据拷贝,在高并发场景下,这会大大加大系统的负担。
优化的思路
减少状态的切换
状态切换发生的原因是发生了系统调用,系统调用结束之后要切换回用户态。如果我们可以减少系统调用的次数,那么就可以减少状态的切换。
减少数据拷贝的次数
在发送文件的这个例子中,进程在用户态时并没有对数据进行增删改查的操作,只是把他又传入了socket缓冲区。因此数据其实没有必要到用户空间走一圈,那么从内核缓冲区拷贝到用户空间再拷贝到socket缓冲区的过程,我们是否可以优化为直接将数据从内核缓冲区拷贝到socket缓冲区。
零拷贝
mmap() + write()
mmap()函数的作用:mmap() 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
执行流程
(1)调用mmap()函数
(2)从用户态切换到内核态(第一次状态切换)
(3)CPU命令DMA把数据从磁盘的缓冲区读取到内核缓冲区中(第一次拷贝)
(4)从内核态切换到用户态,应用进程和内核共享共享这个缓冲区(用户空间的虚拟内存地址和内核空间的虚拟内存地址映射到同一块物理内存)。(第二次状态切换)
(5)调用write()函数
(6)从内核态切换到用户态(第三次状态切换)
(8)CPU将数据从内核缓冲区拷贝到socket缓冲区(第二次拷贝)
(9)CPU命令DMA将数据从socket缓冲区拷贝到网卡(第三次拷贝)
(10)从内核态切换到用户态(第四次状态切换)
四次状态切换,三次拷贝
sendfile()
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile()
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度
这个函数既包含了将数据读入到内存的含义也包含了将数据发送到网卡的含义,因此sendfile只发生一次系统调用,替换了read和write两次系统调用
这样就只会发生两次状态切换和三次数据拷贝
(1)调用sendfile函数
(2)从用户态切换到内核态(第一次状态切换)
(3)CPU命令DMA把数据从磁盘的缓冲区读取到内核缓冲区中(第一次拷贝)
(4)CPU将数据从内核缓冲区拷贝到socket缓冲区(第二次拷贝)
(5)CPU命令DMA将数据从socket缓冲区拷贝到网卡(第三次拷贝)
(6)从内核态切换到用户态(第二次状态切换)
sendfile + SG-DMA
从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了变化
(1)调用sendfile函数
(2)从用户态切换到内核态
(3)CPU命令DMA将数据从磁盘传输到内核缓冲区
(4)缓冲区描述符和数据长度传到 socket 缓冲区,网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
(5)从内核态切换到用户态
只发生了两次状态切换,和两次数据拷贝,且CPU没有参与拷贝数据的过程,这就是真正的零拷贝技术
零拷贝技术:数据在网络传输过程中无需CPU参与拷贝