1. 引言
上一篇文章中,我们介绍了在计算机系统中,CPU 是如何与外围硬件交互的:
CPU 是如何与外围硬件交互的
我们看到,通过 DMA 芯片进行的硬盘读写过程需要进行四次特权级切换和四次拷贝操作。
那么,是否可以对上述过程进行优化,实现对 CPU 性能的提升呢?
2. 零拷贝
如果能够减少这些特权级切换和拷贝操作,系统性能必然会大幅提升。
从这一思路出发,“零拷贝”技术就这样诞生了,主要有以下三个思路:
- 用户态可以直接操作读写,从而避免特权级切换;
- 减少交互过程的拷贝次数;
- 写时复制,需要写操作的时候再执行拷贝操作,读数据过程不拷贝。
3. 用户态直接 IO
如上所述,用户态直接 IO 可以避免特权级切换,最常见的用户态 IO 的例子就是异步 IO 的实现,让用户态进程无需进行特权级切换就可以完成 IO 操作。
可以参看异步 IO 两种实现的介绍:
POSIX AIO -- glibc 版本异步 IO 简介
linux AIO -- libaio 实现的异步 IO 简介及实现原理
在追求高性能的数据库、web 服务器等组件通常都会使用异步 IO 来实现性能的提升。
4. 内存映射 IO -- mmap
此前我们对 linux 下的内存映射 IO 的用法做过详细的介绍。
那么,内存映射 IO 究竟是如何实现的呢?
4.1 mmap 的执行流程
- 用户进程执行 mmap 函数后,会在内存虚拟地址空间中分配一段连续的地址空间;
- 当用户进行文件操作时,会触发一个特殊的缺页中断;
- 内核发起调页请求,将数据从磁盘写入到内存中;
- 如果脏页有过修改,经过一段时间后,或执行 msync 函数后,会触发将内存中的脏页写入到文件中。
4.2 mmap 的性能
内存映射 IO 并没有减少每次磁盘读写过程中的 DMA 拷贝,但却让 CPU 的拷贝减少了,因为 CPU 无需再将数据从内核缓冲区拷贝到用户缓冲区。
虚拟地址空间中分配的共享空间成为了一层磁盘的缓存,从而有效提升 IO 性能,尽管会导致一部分碎片空间的浪费,与文件写入的不及时,但在此之后,对所有已被载入到内存的文件内容的读取,都再也无需进行拷贝操作,可以有效提升 IO 效率。
5. sendfile 函数相关的 IO 操作
5.1 sendfile 零拷贝技术
另一种零拷贝技术就是 sendfile 函数,它通过直接从内核缓冲区向 socket 缓冲区拷贝数据,减少了 CPU 将数据从内核缓冲区拷贝到用户缓冲区的过程,也无需进行系统特权级的切换,从而有效提升 IO 效率。
但 sendfile 函数的缺点也显而易见,那就是用户态程序无法对文件数据进行任何修改,对于数据库、消息队列等直接读取文件的组件来说,他们并不需要对文件进行任何修改,采用 sendfile 函数来提升 IO 效率是非常合适的。
5.2 sendfile DMA gather copy
sendfile 函数的优势在于对 CPU 拷贝的去除,从而有效提升 IO 性能,但 CPU 仍然要进行从内核缓冲区到 socket 缓冲区的拷贝操作,既然在整个过程中,文件都没有被修改,是否可以进一步省去这一步的拷贝操作呢?
答案是可以的,那就是 DMA gather copy 操作。
在硬件实现上,DMA 可以直接读取内存的数据。在这样的硬件系统中,操作系统可以将一部分内核缓冲区暴露给 DMA 芯片,从而在 sendfile 实现上让 DMA 芯片直接将内核缓冲区的数据拷贝到网卡缓冲区中,从而让 CPU 在 sendfile 的实现中得以彻底解放,从而实现性能的大幅提升。
5.3 sendfile 的内部实现 -- splice 系统调用
由于 DMA gather copy 依赖于硬件实现,这就限制了 sendfile 在不同硬件环境下的表现,那么,是否有什么办法,能够让不支持 DMA gather copy 的硬件实现中也支持这样高性能的零拷贝呢?
答案当然也是有的,那就是 splice 系统调用。
我们知道,进程间通信的一个高效的方法就是通过管道 pipe,所谓的“管道”实际上是一个 FIFO 缓冲区,这个缓冲区的存在实现了位于管道两端的两个进程之间的高效通信。
splice 借鉴了管道的设计思想,它在通信的两端之间创建了一个中间缓冲区,让两端在这个 FIFO 缓冲区中直接进行读写,从而实现对性能的提升。
在 linux 内核中,sendfile 在不支持 DMA gather 的硬件环境下,便是通过 splice 系统调用来实现的,它通过一个中间的 FIFO 缓冲连通了内核缓冲区与 socket 缓冲区,从而无需再进行内核缓冲区到 socket 缓冲区的数据拷贝,实现对 CPU 拷贝过程的解放。