LWN:重新思考splice() 调用
共 2868字,需浏览 6分钟
·
2023-03-03 19:45
关注了就能看到更多这么棒的文章哦~
Rethinking splice()
By Jonathan Corbet
February 17, 2023
DeepL assisted translation
https://lwn.net/Articles/923237/
splice()系统调用的出发点是很吸引人的:直接将两个文件描述符(fd, file descriptor)连在一起让数据从一个文件直接移动到另一个文件,而不需要经过用户空间,最好也不需要在内核中进行 copy 动作。多年以来,splice() 已经帮助实现了一些明显的性能优化,但也让人意识到它很难用,偶尔会产生出人意料的效果。最近一次的 linux-kernel 讨论中显示了 splice() 是如何引起麻烦的,以至于一些开发者现在怀疑当初增加这个功能是否真的是一个好主意。
Stefan Metzmacher 是一位 Samba 开发者,他想使用 splice()来实现 Samba 服务器的 zero-copy I/O。不过他遇到了一个问题。如果把一个文件通过网络发送到远程客户端,那么可以使用 splice()将文件数据送入一个 socket;网络层将直接从 page cache 中读取该数据,不需要在内核中进行 copy 了。这正是他所希望的结果。但是,如果文件在网络传输完成之前被写入过,那么新写入的数据也可能会被发送出去,即使这个写入操作是发生在 splice()调用之后的,哪怕是在同一进程中进行也可能会有这种结果。这可能会导致一些不好的后果(以及让 Samba 用户感到不满),也就是在远端收到的数据不是预期中的那个样子。
这里的问题其实不止这么简单。首先,不可能将一个文件直接用 splice 操作关联到一个网络 socket 中;splice() 操作要求至少其中一个文件描述符是管道(pipe)才行。所以实际的操作顺序是将文件 splice 到一个管道中,然后用第二个 splice()调用把管道再 splice 到 socket。两个 splice()调用都不知道其中的数据会在什么时候到达最终目的地;因为在两个 splice() 调用完成后,网络层可能仍然还在处理这个文件数据的传输。没有简单的方法可以知道数据已经完成传输,从而可以开始对文件进行后续改动了。
在他最初的电子邮件中,Metzmacher 询问是否可以在 file-cache page 传递给 splice() 时将其标记为 copy-on-write,从而防止出现这个问题。然后,如果文件在传输过程中被写入的话,这个传输动作就可以继续从较早的数据中读取,而对文件的写入操作会独立进行,不会产生干扰。Linus Torvalds 很快就拒绝了这个想法,他说, "splice 的全部意义" 就在于能够共用存放这些数据的 buffer。让这些 page 采用 copy-on-write 的做法的话,会破坏数据共享。他后来补充说,splice() 调用应该被看作是 mmap() 的一种特殊形式,它们具有类似的语义。
他还说 "你可以说'我不喜欢 splice()'。这没关系。我曾经认为 splice 是一个非常酷的概念,但现在我有点讨厌它。不喜欢 splice() 的观点是很合理的。" 不管你喜不喜欢,目前 splice()的行为不能改变了,因为那会破坏现存的应用程序;即使 Torvalds 不喜欢,也没法推翻这个结论。
Samba 的开发者 Jeremy Allison 建议说,可以通过这种方法来解决 Metzmacher 问题,也就是让 Samba 只在客户端持有相关文件的租约(lease)时才会尝试 zero-copy I/O,这就能确保不存在并发访问的情况。但他后来不得不收回这个想法;因为 Samba 服务器端并不能知道网络传输能在什么时候完成,即使有租约存在,仍然可能有意外情况。因此,他的结论是:"即使在这个文件有租约保证的情况下,samba 也无法使用 splice()"。
Dave Chinner 看下来认为这个问题与以前在文件系统层解决的问题很相似。跟很多情况类似,例如 RAID 5 或文件系统压缩的数据之类的,这时要写入的数据必须在这些操作期间保持不变;这就是近十二年前一直在面对的 stable-pages 问题。他说,也许在这里可以实现一个类似的解决方案,也就是对目前正在使用 splice()的 page 进行写入的操作直接就阻塞掉,直到操作完成再说。
Torvalds 和 Matthew Wilcox 都指出了这个想法的缺陷:splice() 操作会花费的时间并不确定,所以可能可以被人利用(意外地或故意地)来无限期地阻止对文件的访问。因此这个想法并没有继续下去。
Andy Lutomirski 认为,对于应用程序想要做的事情来说,splice() 并不是一个正确的接口;splice() 没有办法将状态信息确定地传回给调用者。他说,相反,io_uring 可能是实现这一功能的更好方式。它允许多个操作排队进行,关键是,它有一个 completion 机制,可以让用户空间知道什么时候一个特定 buffer 是不再被使用的。io_uring 的维护者 Jens Axboe 最初对这一想法也不是很确定,但在 Lutomirski 建议将管道从这个方案中移除并允许一个非管道的文件描述符直接跟另一个文件描述副连接起来之后,他也对这一想法产生了兴趣。Axboe 说,管道有时 "确实碍手碍脚"。
Axboe 认为,可以新增一个 "send file" io_uring 操作来很好地解决这个问题,可以在设计之初就考虑到异步操作,而不使用管道。因此,这可能是这次讨论中产生的解决方案。当然还是需要有人去实际上先实现出来。
还有一些讨论是关于 splice() 是否应该被废弃的讨论;Torvalds 就认为这个系统调用没有什么价值:
这种 "一切都是多个进程的流水线操作(everything is a pipeline of processes)" 的观念是来自 Unix 的历史情况,对 shell 脚本非常有用,但这种关键实际上来说对更大规模的场景来说通常不是很有用,splice() 也是类似的情况,它确实没有达到它想设计达到的目标,而且在开发实践中真的非常令人讨厌。
但我们不得不继续支持它。
如果内核里没有一个更好的替代方案,那么就没法要求人们别用 splice();Torvalds 有些怀疑 io_uring 方案最终是否会被证明是更好的方案。唯一的证明办法可能就是去尝试一下,看看它到底有多好用。在这之前,splice() 将是内核所能提供出来的最好的方案了,尽管它确实有缺陷。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~