2.8.1 零拷贝的基本原理
Linux包含内核态和用户态。如果学习过内核的相关知识就会了解到内核态的内存和用户态的内存是隔离的。当用户态的程序向文件写入数据时,需要将用户态的数据拷贝到内核态的内存中;当用户态的程序读取数据时,需要将内核态的内容拷贝到用户态的内存中。
读取文件的过程分为两个步骤,首先从磁盘中读取数据并将其保存到内核内存中,然后从内核内存中将数据拷贝到用户分配的data_buf中。在写入数据时,先将data_buf中的数据拷贝到内核内存中,然后写入磁盘。这种数据的拷贝过程其实是非常消耗内存资源的,如果能减少内存拷贝过程,则一方面可以提高性能,另一方面可以减少延时。
不仅文件系统存在类似的问题,网络也存在类似的文件。如果想要将一个文件通过网络发送到某个节点,则要经过两次用户态与内核态的内存拷贝。第一次将文件系统缓存中的数据拷贝到用户态缓冲区,第二次将用户缓冲区的内容拷贝到传输协议的缓冲区,如图2-19所示。除了用户态与内核态之间的内存拷贝,还有硬件与系统内存之间的数据传输(通常为DMA方式)。
图2-19 数据访问的内存拷贝
我们观察一下会发现,对于单纯地将文件数据发送到网络的场景(如Web服务端发送照片),其实没必要经过用户态缓冲区转发,完全可以直接将文件系统缓存的数据从内核中拷贝到传输协议的内核缓存中。这样本质上就减少了一次内核态与用户态之间的内存拷贝,如图2-20所示。其实如果在内核实现两个模块的内存拷贝不仅会减少内存拷贝的开销,而且也会减少内核态与用户态上下文切换的开销。
图2-20 避免用户态拷贝的示意图
虽然使用上述方式消除了内核态与用户态之间的内存拷贝过程,但是在内核内部还是有一次拷贝的。后来Linux内核又做了进一步的优化,消除了内核内部的内存拷贝。在该优化中,当执行2时,并不是进行全内存拷贝,而是将一个描述数据位置的信息拷贝到套接字缓存中(图2-21中步骤2),通过传输协议发送数据时根据描述信息利用DMA机制直接将数据发送出去(图2-21中的粗线)。
图2-21 避免磁盘缓存与网络缓存拷贝的示意图
通过上面描述我们发现,其实所谓零拷贝技术并非没有任何内存拷贝,它主要是消除数据的拷贝,描述数据的拷贝是不可缺少的。