sendfile()

为了简化用户接口,同时还要继续保留 mmap()/write() 技术的优点:

减少 CPU 的拷贝次数,Linux 在版本 2.1 中引入了 sendfile() 这个系统调用。

sendfile() 不仅减少了数据拷贝操作,它也减少了上下文切换。

首先:sendfile() 系统调用利用 DMA 引擎将文件中的数据拷贝到操作系统内核缓冲区中,然后数据被拷贝到与 socket 相关的内核缓冲区中去。

接下来,DMA 引擎将数据从内核 socket 缓冲区中拷贝到协议引擎中去。

如果在用户调用 sendfile() 系统调用进行数据传输的过程中有其他进程截断了该文件,那么 sendfile() 系统调用会简单地返回给用户应用程序中断前所传输的字节数,errno 会被设置为 success。

如果在调用 sendfile() 之前操作系统对文件加上了租借锁,那么 sendfile() 的操作和返回状态将会和 mmap()/write () 一样。

适用场景

sendfile() 系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,所以 sendfile() 只是适用于应用程序地址空间不需要对所访问数据进行处理的情况。

相对于 mmap() 方法来说,因为 sendfile 传输的数据没有越过用户应用程序/操作系统内核的边界线,所以 sendfile() 也极大地减少了存储管理的开销。

但是,sendfile() 也有很多局限性,如下所列:

sendfile() 局限于基于文件服务的网络应用程序,比如 web 服务器。据说,在 Linux 内核中实现 sendfile() 只是为了在其他平台上使用 sendfile() 的 Apache 程序。

由于网络传输具有异步性,很难在 sendfile() 系统调用的接收端进行配对的实现方式,所以数据传输的接收端一般没有用到这种技术。

基于性能的考虑来说,sendfile() 仍然需要有一次从文件到 socket 缓冲区的 CPU 拷贝操作,这就导致页缓存有可能会被传输的数据所污染。

带有 DMA 收集拷贝功能的 sendfile()

上小节介绍的 sendfile() 技术在进行数据传输仍然还需要一次多余的数据拷贝操作,通过引入一点硬件上的帮助,这仅有的一次数据拷贝操作也可以避免。

为了避免操作系统内核造成的数据副本,需要用到一个支持收集操作的网络接口,这也就是说,待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。这样一来,从文件中读出的数据就根本不需要被拷贝到 socket 缓冲区中去,而只是需要将缓冲区描述符传到网络协议栈中去,之后其在缓冲区中建立起数据包的相关结构,然后通过 DMA 收集拷贝功能将所有的数据结合成一个网络数据包。

网卡的 DMA 引擎会在一次操作中从多个位置读取包头和数据。

Linux 2.4 版本中的 socket 缓冲区就可以满足这种条件,这也就是用于 Linux 中的众所周知的零拷贝技术,这种方法不但减少了因为多次上下文切换所带来开销,同时也减少了处理器造成的数据副本的个数。

对于用户应用程序来说,代码没有任何改变。

首先,sendfile() 系统调用利用 DMA 引擎将文件内容拷贝到内核缓冲区去;然后,将带有文件位置和长度信息的缓冲区描述符添加到 socket 缓冲区中去,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,DMA 引擎会将数据直接从内核缓冲区拷贝到协议引擎中去,这样就避免了最后一次数据拷贝。

通过这种方法,CPU 在数据传输的过程中不但避免了数据拷贝操作,理论上,CPU 也永远不会跟传输的数据有任何关联,这对于 CPU 的性能来说起到了积极的作用:首先,高速缓冲存储器没有受到污染;其次,高速缓冲存储器的一致性不需要维护,高速缓冲存储器在 DMA 进行数据传输前或者传输后不需要被刷新。

然而实际上,后者实现起来非常困难。

源缓冲区有可能是页缓存的一部分,这也就是说一般的读操作可以访问它,而且该访问也可以是通过传统方式进行的。

只要存储区域可以被 CPU 访问到,那么高速缓冲存储器的一致性就需要通过 DMA 传输之前冲刷新高速缓冲存储器来维护。

而且,这种数据收集拷贝功能的实现是需要硬件以及设备驱动程序支持的。

参考资料

https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/