LWN: copy_file_range()应该处理哪些情况?
共 4323字,需浏览 9分钟
·
2021-03-07 08:13
关注了就能看到更多这么棒的文章哦~
How useful should copy_file_range() be?
By Jonathan Corbet
February 18, 2021
DeepL assisted translation
https://lwn.net/Articles/846403/
copy_file_range()系统调用看起来很容易理解:允许用户空间要求内核将一系列的数据从一个文件复制到另一个文件,希望在这一过程中能以比较优化的方式来完成。实际上,这个调用看起来会很常用,但实际上并不是,这还是在 5.3 版本时对可用性方面进行了一些改进之后的结果。Go 语言的开发者在使用 copy_file_range() 时就遇到了问题,随后进行了长时间的讨论,讨论这个系统调用应该如何应用、内核是否应该多做些工作来使它真正有用。
copy_file_range()的定义是:
ssize_t copy_file_range(int fd_in, loff_t *off_in,
int fd_out, loff_t *off_out,
size_t len, unsigned int flags);
ssize_t copy_file_range(int fd_in, loff_t *off_in,
int fd_out, loff_t *off_out,
size_t len, unsigned int flags)。
它的任务是将 fd_in 所代表的文件中的 len 长度的数据复制到 fd_out 中,监测两边的 offsett。flags 参数必须为 0。这个调用最早出现在 4.5 版本中。随着时间的推移,人们发现它有许多令人不快的问题,导致了很多长期修复工作,也引起了很多抱怨。
在 2019 年,Amir Goldstein 修复了许多问题,并在此过程中删除了一个重要的限制:在此之前,copy_file_range()不允许在不位于同一文件系统的文件之间进行数据复制。这个 patch 被合并之后(5.3),它可以在任何两个文件之间进行复制了。对于跨文件系统的情况,它会回退到使用 splice() 来完成。这样一来,似乎 copy_file_range() 终于成为了一个可靠而有用的系统调用。
它确实成功吸引到了人们的注意,Go 的开发者就决定将它用于他们标准库中的 io.Copy() 函数。然后他们遇到了一个问题:当使用内核生成的文件来作为 input 文件时,copy_file_range()会复制 0 byte 数据并返回成功。/proc、tracefs 和其他大量的虚拟文件系统中的文件都是这种情况,如果直接使用像 stat()这样的系统调用对其查询文件长度,一般都会显示长度为 0。copy_file_range()看到这个信息之后,就会认为没有数据需要复制,工作已经完成,就返回成功值。
但其实这种文件是有数据要读的,只是在文件长度上没有体现出来,原因也很简单在实际读取文件之前往往无法知道其真实长度。在内核 5.3 之前,因为禁止了对跨文件系统的 copy 操作,所以大多数这样的操作都会返回一个错误代码,但这种情况也被被干净地处理掉了。内核很高兴,但用户可能感到出乎意料,他们毕竟是真的想要复制他们所要求获取的数据,于是感到很不满意。
Marking virtual filesytems
于是 Nicolas Boichat 修改了 copy_file_range() 希望能让这些用户满意。patch 中为虚拟文件系统的 file_system_type 结构增加了一个 flag (FS_GENERATED_CONTENT),设置了这个 flag 就表示这个文件系统文件的长度是无法提前获取的。copy_file_range() 会根据这个标志来返回一个错误代码,这会进而导致 io.Copy() 退而求其次继续使用主动 copy 动作。这个改动似乎解决了眼前的问题,但它肯定无法被合入 mainline。
人们对这个 patch 有一些反对意见,首先,它要求对所有的虚拟文件系统进行特殊标记,这个 patch set 并未全部标记好,并且开发者在今后添加新文件系统时一定要将它们全部标记正确。正如 Greg Kroah-Hartman 所说,"这样就意味着不断的审查工作,我不认为有人愿意在接下来的 20 年内保持做这个工作"。
但更大的问题是,这种行为是否应该被看作是一个 bug。Boichat 认为这是一个质量倒退。在 5.3 之前代码逻辑会自动回到正常 copy,而在这个改动之后就会无法 copy 了,并且也不报错。不过 Kroah-Hartman 并不认为这是个质量回退,他继续说道:
首先,人们为什么要在简单的/proc 和/sys 文件上使用 copy_file_range 呢?这些文件无法进行 seek 操作(至少大多数是不行的),所以这种感觉就像 "快看,这有一个新的 syscall,我们可以把所有地方都改成用它了!" 的问题,user space 不应该这样改。
Dave Chinner 讲得就更加直接了:
这是一个针对性的解决方案,只针对那些存储 persistent data 的文件系统中的“常规文件”,用来加速数据复制(如 clone、server side offload、hardware offload)。这不是作为一种 copy 机制(也就是将数据从随便某个文件描述符复制到另一个文件描述符)。
在 Go system library 中把它当做普通的文件复制机制来用,这不对。这是一个 user space bug。用户空间做错了事,用户空间自己才需要修正。
Go 开发者 Ian Lance Taylor 认为,不是很容易搞清楚什么时候可以使用 copy_file_range()。他指出,在 copy_file_range()的 man 页面中没有提到这些限制,并且按这种做法的话,这个系统调用的实用性就大大降低了:
从我的角度来说,也就是作为内核的一个用户而不是内核的一个开发者,像这样的系统调用会对于某些文件直接无效并且没有提醒,而且也没有提供任何方法来帮助我们:1)要么能提前确定系统调用会失败,或者 2)在调用后确定系统调用确实失败。那么这种系统调用就没有用了。我永远不能使用那个系统调用,因为我不知道它是否会成功。
Chinner 说,可以看看是不是除了 read()以外没有办法判定这个文件是否有数据,如果没法判定,那就不要使用这个系统调用。但文件系统专家 Darrick Wong 也说:"我不知道怎么做到这一点,Dave:)" 还有一个有趣的转折,正如 Boichat 所说的:sysfs 中的文件,并没有报告自己长度为零,而是声称长度为 4,096 字节,而它们的真实长度可能比这个大,也可能比这个小。Chinner 的判定方法就算能实现,在这些文件上也是无效的。
Toward a real fix
Wong 继续说,他赞同 Go 开发者的观点:copy_file_range()要么能正常按照预期来完成任务,要么就返回一个错误,这样用户空间就可以知道应该回到老路上来进行普通的 copy。他还提出了几种可能解决这个问题的方法,其中第一种是回到以前的样子,也就是明确禁止跨文件系统 copy。如果做不到这一点,我们可以将这种 copy 限制在那些明确宣称支持这个操作的那种文件系统中。Luis Henriques 针对这一想法稍作修改之后实现了 patch,也就是如果两个文件系统的类型相同,并且所涉及的文件系统明确地实现了 copy_file_range() 操作,那么跨文件系统的 copy 仍然是被允许的。
不过,Trond Myklebust 指出内核的 NFS daemon 使用 copy_file_range()机制在不同类型的文件系统之间复制文件,这个 patch 也只好中止了,因为按这种实现的话会破坏这些基本功能。这种使用模式在其他文件系统中也存在,比如 Ceph 和 FUSE。对此,Henriques 添加了一个新的标志(COPY_FILE_SPLICE),可以在内核中使用这个标志来表示应该进行跨文件系统种类的 copy。有人提出,这个标志是否应该被提供给用户空间,这样用户空间能够有某种方式能知道操作会不会成功,但最终决定不会这么做。
在写这篇文章的时候,patch 的最终版本还没有发布,但修复的方式很清晰了。当从用户空间调用时,copy_file_range()只有在两个文件系统类型相同、并且该文件系统对这个系统调用显式地声明支持的情况下,才会尝试跨文件系统复制一个文件(这样写应该是考虑到了所有可能的情况了)。否则,调用将会显式地返回错误码报错,这样用户空间就知道必须以其他方式来 copy 数据。所以,copy_file_range() 永远不会是一个通用的文件复制机制,但至少可以在那些准备好面对出错情况的代码中可靠地使用。
不过在 copy_file_range()中还潜伏着一个陷阱。同大多数与 I/O 相关的系统调用一样,copy_file_range()可以允许复制的字节数少于请求的字节数的情况。用户空间需要检查返回值,来看看实际复制了多少字节。目前没有办法区分是因为在被读的这一端被缩短的(比如是到了文件的末尾)还是在写端被停止(这很可能表明 write error)。目前还没有人想出能真正解决这个问题的方法。
上面这些内容应该让大家了解了一个看起来非常简单的接口是如何变得复杂的。copy_file_range() 存在的时间还不长,就已经暴露出了不少此前意想不到的问题。后续可能还会有新问题暴露出来。因此,难怪内核开发者在时不时被责备之后,会强烈希望要尽可能地保持 kernel 实现尽量简单了。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~