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