splice系列系统调用

关注splice系列系统调用(包括splice,tee和vmsplice)已经有一段时间了,开始的时候并未能领会splice的意义所在,致使得出了“splice系列系统调用不怎么实用”的 错误结论 。随着内核研究的深入,才逐渐懂得:splice对于其中一个文件描述符必须是管道的要求并不是阻碍其应用的障碍,并且恰恰相反,它正是splice的本质所在。

splice主要通过去除在内核空间和用户空间之间的内存拷贝的开销来提高系统的性能。它将内核空间和用户空间之间的内存拷贝转变成内核空间和内核空间的内存的拷贝,这样的话,内核就有机会通过底层的一些机制来避免非必要的拷贝,如通过页面的引用来替换拷贝内存页面。这就引出一个问题:内核空间的缓冲区如何在用户空间表示?恰巧管道符合这个要求,所以就“勉强”让它来担此重任了。因此,当管道被用来做splice的时候,它队列的概念就被弱化了,这时它代表的就是内核空间的内存缓冲区。因此有了以下几种对应关系:
  • splice(infd,... pipe,...): 从由infd指向的文件读取数据到由pipe指向的内核缓冲区。
  • splice(pipe,... outfd,...): 把由pipe指向的内核缓冲区中的数据写到由oufd指向的文件。
  • tee(inpipe, outpipe,...): 从由inpipe指向的内核缓冲区“拷贝”数据到由outpipe指向的内核缓冲区。
  • vmsplice(rdpipe, iov...): 从由rdpipe(pipe的读端)指向的内核缓冲区“拷贝”数据到由iov表示的用户缓冲区。
  • vmsplice(wrpipe, iov...): 从由iov表示的用户缓冲区“拷贝”数据到由wrpipe(pipe的写端)指向的内核缓冲区。
可见,splice, tee和vmsplice涵盖了在用户空间控制内核缓冲区的全部情况。应该说,已经很完美了。

不过请稍等一下,如果有需要在两个文件之间“拷贝”数据呢?也许我们不得不这样:
  1. splice(fd, ...pipe, ...)
  2. splice(pipe, ...fd, ...)
真的有必要进行两次系统调用么?事实上两次调用可以合而为一,只在内核内部进行上述操作就行了,因为splice原本就只有在文件和内核缓冲区之间移动数据的意思,并且在两个文件描述符之间移动数据的API原本就有:sendfile。但是,用sendfile也不是尽善尽美的,sendfile多少有些词不达意,我想这也可能是它在2.6.9之后就只能用于通过socket接口发送支持mmap系统调用的文件的原因之一。

有了sendfile的加入,这个世界又朝着完美更近了一步。

我们经常听到的一句话就是:前途是美好的,道路是曲折的。

之于splice系列系统调用的实现,这也是成立的,前进的道路上难免会由一些困难和反复,因为这个世界是如此的复杂。

splice系列系统调用首次亮相于2.6.17版内核,直到现在,其实现离完美还有很大一段距离,还有一些问题:
  • splice的套接字读实现存在潜在的数据污染(data corruption)。
  • vmsplice当标志位SPLICE_F_GIFT被设置的时候,其内存地址和大小必须是按页对齐的,缺乏灵活性。这导致不能发送非整页的数据,即使以mmap, splice, munmap的顺序组织系统调用也不可以。
  • sendfile系统调用因为把数据的移动过程在内部分成了从输入文件读取数据到pipe的内核缓冲和从pipe的内核缓冲写数据到输出文件两步,并且两步因为其实现的缘故,必须在原子操作内完成,所以,输出操作必须是阻塞的,极大地限制了其灵活性。

你可能感兴趣的:(C++,linux~)