一、传统IO传输

传统的数据传输,不论是文件,还是写文件,都是会经过从 磁盘-》内核缓冲区 -》用户应用缓冲区 -》Socket 缓冲区 -》 网卡,这些步骤。这其中,会涉及到 4次用户态到内核态的上下文切换、4次用户态和内核态之间的数据拷贝。
origin

以读文件为例:
线程在用户空间发起read()读文件,线程从用户态切换为内核态
DMA将磁盘数据拷贝到内核缓存后,CPU又将数据从内核缓存拷贝至用户缓存,这时线程又从内核态切换为用户态
CPU将数据从用户缓存拷贝至socket缓存,线程又从用户态切换到内核态
DMA将数据从内核缓存拷贝到网卡,read()调用结束返回,线程又从内核态切换到用户态

方式一:mmap + write实现零拷贝

mmap
mmap,Memory Mapped Files:简称 mmap,也有叫 MMFile 的,使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程。它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上。
使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销,在用户态对映射区域的写操作数据会同时在内核态缓存中存在
用户空间发起mmap系统调用
DMA将数据从文件系统拷贝到内核缓存中,并和用户空间堆外缓存建立映射关系
CPU从用户缓存读取到数据后,将数据拷贝到socket缓存中,线程从用户态切换到内核态
DMA将数据从socket缓存拷贝到网卡,调用write()结束后返回,线程从内核态切换到用户态
整个过程发生了3次拷贝,2次DMA,1次CPU;4次线程切换

方式二:sendfile

sendfile
sendfile,在Linux内核2.1版本之后,提供了sendfile()函数
sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

// out_fd 参数代表待写入的文件描述符
// in_fd 参数代表待读取的文件描述符
// *offset 参数代表指定从读取文件流的哪个位置开始读,为空时则使用读入文件流默认的起始位置
// count 参数代表传输的字节数
sendfile函数实现了内核态和用户态之间的“零拷贝”:就是将数据的拷贝全部控制在内核态中,省去传统读取文件方式中 2次上下文切换 和 2次数据拷贝(伴随着上下文切换时,发生在内核态到用户态之间的拷贝),具体步骤如下:
用户空间发起sendfile()函数调用,设置读取数据和写入输入的文件描述符、读取字节的偏移量、读取的字节长度,进程从用户态切换到内核态
DMA 把磁盘的数据拷贝到内核缓存(Page Cache)中
CPU 从内核缓存中,将数据拷贝至socket缓存中
DMA 将socket缓存中的数据拷贝至网卡,sendfile调用完成,进程由内核态切换至用户态

整个过程发生了 2次上下文切换 和 3次数据拷贝,和上文说的貌似不一致。没错,在 sendfile()的 man page 中有这样的定义:In Linux kernels before 2.6.33, out_fd must refer to a socket. Since Linux 2.6.33 it can be any file. 因此,在Linux 2.6.33 之前的内核版本,写入的 fd 必须为 socket,因此数据在内核缓冲区后,需要再次拷贝到socket缓冲区,而在 2.6.33 版本之后的实现略有不同
sendfile_optimize

如图,当数据被拷贝至内核缓冲区时,通过DMA Gather控制器,直接拷贝至网卡。因为全程没有cpu参与数据的搬运,所有的数据都是通过 DMA 来进行传输的,实现了真正意义上的“零拷贝”。

Reference
https://www.cnblogs.com/xiaolincoding/p/13719610.html
https://www.cnblogs.com/ericli-ericli/articles/12923420.html