一、普通的文件拷贝
零拷贝的使用场景是从某台机器将一份数据(比如一个文件)通过网络传输到另外一台机器。
现在设想一下,我们要把设备A 上的一个文件通过socket发送到设备B,期间涉及到调用一次read()函数和write()函数。
当我们调用read()函数时:
- 系统调用read导致了从用户空间到内核空间的上下文切换。接着,切换到内核态后,DMA(直接存储访问器)模块从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制。
- 数据从内核空间缓冲区复制到用户空间缓冲区(这次的拷贝是通过CPU进行的),之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf)。
当我们调用write()函数时:
- 系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间中的socket缓冲区中(这次的拷贝是通过CPU进行的)。
- 系统调用write返回,导致了从内核空间向用户空间的上下文切换。并且此时DMA模块将数据从内核中的socket缓冲区传递至协议引擎。
可以看到,这一过程总共发生了4次上下文切换,以及2次涉及到CPU的数据拷贝(这意味了在拷贝数据的期间,CPU无法处理其他数据,十分的浪费CPU资源)。
二、零拷贝的实现
为了实现零拷贝,我们要从两方面来切入,即减少上下文切换的次数和避免使用CPU来进行数据的拷贝。
(1)mmap
mmap的核心思想就是将内核缓存区、用户空间缓存区映射到同一个物理地址上,可以减少用户缓存区与内核缓存区之间的数据拷贝。
这样一来就变成了:
- 文件的内容通过DMA模块被复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样就内核缓冲区与用户缓冲区之间的复制就不会发生。
- write系统调用导致内核将数据从内核缓冲区复制到与socket相关联的内核缓冲区中。
- DMA模块将数据由socket的缓冲区传递给协议引擎时。
(2)sendfile
sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中。
此时数据并未被复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。
此时全部流程变为了,调用sendfile:
- 由用户态切换到内核态。
- 系统将记录数据位置和长度的描述符写入到socket缓冲区中。
- 由DMA模块将数据直接从内核缓冲区传递给协议引擎。
- sendfile返回,由内核态切换到用户态。
这时我们发现,整个过程只涉及了两次上下文的切换,并且整个数据的拷贝过程都没有涉及到CPU,即这就是零拷贝。