上一篇,学习了copy_file_range()的历史,以及内部原理,同时还提到了和sendfile()的差异。
本篇学习另外2个实现零拷贝的系统调用sendfile()和splice()。
sendfile()的函数原型【1】
ssize_t sendfile(int out_fd, int in_fd, off_t *_Nullable offset, size_t count);
要求in_fd是支持mmap类似操作的fd,2.6.33以前out_fd必须是一个套接字,2.6.33以后,可以是任何文件。
从5.12内核开始,如果out_fd是一个pipe,那么sendfile()内部使用splice()。
splice()的函数原型【2】
ssize_t splice(int fd_in, off_t *_Nullable off_in, int fd_out, off_t *_Nullable off_out, size_t size, unsigned int flags);
splice要求其中一个fd是管道。
第一次了解splice的人,可能会觉得非常奇怪,为什么splice需要一个管道,如果要把文件发送到套接字,那就需要调用两次splice。
Linus在2006年的时候详细解释了,如参考[3]。linus给出了双重的原因:
1 The pipe is the buffer
sendfile()烂透了的原因是,sendfile()不能工作在不同的缓存类型,sendfile()只能工作在一种缓存类型,也就是文件的page cache。
直接使用page cache,sendfile()不需要额外的缓存,这也限制了sendfile()不能用在两个套接字,也不能从streaming device发送数据。
管道是站在任意两点之间的标准内核缓存,可以将它当做拥有wait queue的scatter-gather list。这是pipe的本质。
试图摆脱管道,那就完全丢失了splice()的真正意义。
2 使用pipe可以达成其它目的
比如给pipe写入一个头,然后再splice(),这样可以实现readv/writev的scatter-gather效果。如果是一个web server,可以:
write(pipefd, header, header_len);
splice(file, pipefd, file_len);
splice(pipefd, socket, total_len);
后来linus又补充了2点
3 pipe buffer is a stream with no position
这让管道很纯粹,一个简单的fd,不用担心偏移,不用担心长度,不用担心类型。
4 可以实现tee
后来又补充到
如果失败,数据仍然保存在pipe中,而sendfile做不到这一点。而且可以很好的处理信号量。
参考
【1】https://man7.org/linux/man-pages/man2/sendfile.2.html
【2】https://man7.org/linux/man-pages/man2/splice.2.html
【3】https://yarchive.net/comp/linux/splice.html