文章目录
前言
所谓零拷贝
并不是不复制数据,而是减少不必要的数据复制,甚至杜绝 CPU 参与的数据复制,从而将 CPU 从数据传输的工作负载中释放出来,提高系统效率。在介绍 Linux 零拷贝的实现以前,还是先介绍下 直接内存访问 MDA(Direct Memory Access)
在系统 IO 中的作用
1. DMA 的引入
1.1 早期的文件 IO 处理过程
计算机系统早期的文件 IO 处理如上图所示,可以看到主要的流程如下:
- 用户进程发起 read 系统调用,请求读取数据到用户内存缓冲区中
- CPU 将对应的指令发送给磁盘控制器,磁盘控制器收到指令后,开始读取数据存入到磁盘控制器的内部缓冲区,完成后发出中断信号通知 CPU
- CPU 收到中断信号后执行中断程序,首先把磁盘控制器缓冲区的数据读取出来写入到内核缓冲区
- 因为数据最终是给用户进程使用的,CPU 还需要把内核缓冲区的数据复制到用户进程缓冲区,完成这一步后 read 系统调用才算结束
以上过程中 CPU 执行了两次数据复制,包括一次磁盘设备的数据复制以及一次相对较快的内存中数据复制。如果只是复制少量数据的话 CPU 被占用的时间比较短,对系统的影响较小,但是如果使用硬盘传输大量数据时也使用 CPU 来复制数据,会导致 CPU 被长时间占用,严重影响系统性能。有鉴于此,直接内存访问 MDA(Direct Memory Access)
被发明出来
DMA
技术允许外部设备和内存存储器之间直接进行 IO 数据传输,而不需要 CPU 的介入,这个过程通过 DMA 控制器实现
1.2 DMA 发挥的作用
引入 DMA 后的文件 IO 流程如上图所示,可以看到关键步骤如下:
- 用户进程发起 read 系统调用,请求读取数据到用户内存缓冲区中
- CPU 将对应的指令发送给 DMA 控制器,由 DMA 控制器将 IO 指令发给磁盘控制器
- 磁盘控制器收到指令后,开始读取数据存入到磁盘控制器的内部缓冲区,完成后发出中断信号通知 DMA 控制器
- DMA 控制器收到中断信号后执行中断程序,把磁盘控制器缓冲区的数据读取出来写入到内核缓冲区,完成后发出中断通知 CPU
- CPU 收到 DMA 的信号,把内核缓冲区的数据复制到用户进程缓冲区,完成这一步 read 系统调用结束
以上过程中与速度比较慢的磁盘设备的交互交给了 DMA 控制器,CPU 只执行了一次数据复制,还是在读写速度较快的内存中进行的数据复制,极大提升了 CPU 的使用效率,提高了系统性能
早期 DMA 只存在在主板上,但是由于 I/O 设备越来越多样,数据传输的需求也不尽相同,所以如今每个 I/O 设备里面都有自己的 DMA 控制器
2. 文件传输场景的技术实现
2.1 传统 read / write 文件传输实现
要实现文件传输的功能,传统 I/O 的工作方式是将磁盘上的文件读取出来,然后通过网络协议发送。这涉及到 2 个系统调用的使用,具体过程如下图所示:
- 用户进程发起 read 系统调用,请求读取数据到用户内存缓冲区,这个过程导致用户空间到内核空间的一次上下文切换
- CPU 将对应的指令发送给 DMA 控制器,由 DMA 控制器负责与磁盘设备交互,把磁盘控制器缓冲区的文件数据读取出来写入到内核缓冲区,完成后发出中断通知 CPU
- CPU 收到 DMA 的信号,把内核缓冲区的数据复制到用户进程缓冲区,
- 以上过程结束 read 系统调用返回,导致一次内核空间到用户空间的上下文切换
- 用户进程发起 write 系统调用,请求写出数据到 Socket 缓冲区,这个过程导致用户空间到内核空间的一次上下文切换
- CPU 收到指令,负责把用户进程缓冲区的数据复制到内核空间的 Socket 缓冲区,
- 以上过程结束 write 系统调用返回,导致一次内核空间到用户空间的上下文切换
- DMA 控制器将内核空间的 Socket 缓冲区数据复制到网卡则是异步的独立过程, write 系统调用返回并不会保证数据被传输到网卡
总结以上过程得出一次文件传输的关键动作消耗如下:
用户态与内核态的切换
:4次
- 用户进程没有权限操作磁盘等外部设备,因此用户进程要使用磁盘时,一般要通过操作系统提供的
系统调用函数
借由权限最高的内核去完成,这就导致了用户态/内核态的切换- 虽然一次用户态与内核态的上下文切换耗时很小,但是在高并发大流量场景下切换次数可能极多,在累加效应下这种耗时很容易被放大,影响到系统性能。因此,优化文件传输性能的一个方面就是减少上下文切换,落地到操作层面就是减少系统调用
CPU数据复制
:2次
2 次 CPU 数据复制的数据流向是内核缓冲区-->用户缓冲区-->内核Socket缓冲区
,实际在文件传输场景中用户进程一般不会对数据进行额外处理,所以数据可以不经过用户空间而只在内核中流动,这样就可以减少 CPU 复制数据的次数和内存占用,优化文件传输性能DMA数据复制
:2次
DMA控制器直接与外设交互,2 次 DMA 数据复制是数据在不同设备间流转的最低要求
2.2 文件传输实现的改进
2.2.1 虚拟内存映射 mmap / write 传输实现
要优化文件传输的性能,一个方式是使用 mmap 系统调用,具体过程如下图所示:
- 用户进程发起 mmap 系统调用,请求读取数据到用户内存缓冲区,这个过程导致用户空间到内核空间的一次上下文切换
- CPU 将对应的指令发送给 DMA 控制器,由 DMA 控制器负责与磁盘设备交互,把磁盘控制器缓冲区的文件数据读取出来写入到内核缓冲区,完成后发出中断通知 CPU
- CPU 收到 DMA 的信号,把内核缓冲区的内存地址等信息交给用户进程,通过虚拟映射实现用户进程缓冲区和内核缓冲区共享同一个物理地址
- 以上过程结束 mmap 系统调用返回,导致一次内核空间到用户空间的上下文切换
- 用户进程发起 write 系统调用,请求写出数据到 Socket 缓冲区,这个过程导致用户空间到内核空间的一次上下文切换
- CPU 收到指令,负责把用户进程缓冲区的数据复制到内核空间的 Socket 缓冲区,
- 以上过程结束 write 系统调用返回,导致一次内核空间到用户空间的上下文切换
- DMA 控制器将内核空间的 Socket 缓冲区数据复制到网卡是异步的独立过程, write 系统调用返回不会保证数据被传输到网卡
总结以上过程可知 mmap / write 文件传输的关键动作成本如下,相比传统 IO 节省了 1 次 CPU 复制的消耗,同时由于用户进程中的内存是虚拟的,只是映射到内核的缓冲区,所以节省了一半的内存空间
用户态与内核态的切换
:4次
CPU数据复制
:1次
DMA数据复制
:2次
2.2.2 sendfile 实现的文件传输
sendfile 是在 Linux 内核 2.1 版本中引入的专门用于发送文件的系统调用,其实现文件传输的过程如下:
- 用户进程发起 sendfile 系统调用,请求发送指定文件到指定 Socket,这个过程导致用户空间到内核空间的一次上下文切换
- CPU 将对应的指令发送给 DMA 控制器,由 DMA 控制器负责与磁盘设备交互,把磁盘控制器缓冲区的文件数据读取出来写入到内核缓冲区,完成后发出中断通知 CPU
- CPU 收到 DMA 的信号,把内核缓冲区数据直接复制到指定的 Socket 缓冲区
- 以上过程结束 sendfile 系统调用返回,导致一次内核空间到用户空间的上下文切换
- DMA 控制器异步地将内核空间的 Socket 缓冲区数据复制到网卡
总结以上过程可知 sendfile 文件传输的动作成本如下,相比传统 IO 节省了 1 次 CPU 复制的消耗,减少了 2 次上下文切换消耗,同时文件数据对用户空间完全不可见,减少了内存空间占用
用户态与内核态的切换
:2次
CPU数据复制
:1次
DMA数据复制
:2次
2.2.3 sendfile 结合 SG-DMA 实现的零拷贝
Linux 内核 2.4 版本开始引入SG-DMA(The Scatter-Gather Direct Memory Access)
技术支持,在网卡支持 SG-DMA 技术的情况下,可以直接从内核空间缓冲区中将数据读取到网卡,具体过程如下:
普通 DMA
一次只能传输物理上连续的一块数据,每传输完一个块发起一次中断,直到传输完成,所以必须要在两个真实的物理缓冲区之间复制数据S/G DMA
借助一个链表维护物理上不连续的块描述符,描述符中包含有数据的起始地址和长度。传输时遍历链表按序传输数据,全部完成后发起一次中断即可,相当于支持逻辑缓冲区与真实的物理物理缓冲区之间复制数据
- 用户进程发起 sendfile 系统调用,请求发送指定文件到指定 Socket,这个过程导致用户空间到内核空间的一次上下文切换
- CPU 将对应的指令发送给 DMA 控制器,DMA 控制器与磁盘设备交互,把磁盘控制器缓冲区的文件数据读取出来写入到内核缓冲区,完成后发出中断通知 CPU
- CPU 收到 DMA 的信号,维护内核缓冲区中数据的描述符信息,并将其复制到 Socket 缓冲区
- 以上过程结束 sendfile 系统调用返回,导致一次内核空间到用户空间的上下文切换
S/G DMA 控制器
读取 Socket 缓冲区中的描述符信息,借助描述符异步地将内核缓冲区的数据复制到网卡
以上过程中 sendfile 文件传输的成本如下,相比传统 IO 减少了 2 次上下文切换消耗,并且所有的数据都是通过 DMA 进行传输,完全没有 CPU 复制消耗,是真正的零拷贝(Zero-copy)技术
用户态与内核态的切换
:2次
CPU数据复制
:0次
DMA数据复制
:2次