LWN:vfs锁带来的一个意外!
共 2774字,需浏览 6分钟
·
2023-08-19 10:30
关注了就能看到更多这么棒的文章哦~
A virtual filesystem locking surprise
By Jonathan Corbet
July 31, 2023
ChatGPT assisted translation
https://lwn.net/Articles/939389/
人们都知道,并发情况会使编程变得更加困难;内核开发中固有的高并发性就是内核开发工作具有挑战性的原因之一。然而,如果并发访问发生在代码中完全没有预料到的地方,情况可能会变得更糟。Christian Brauner 的这个短 patch 附带的长篇说明就是一个关于并发性的假设被证明不正确时可能出现的问题。
在内核内部,struct file 用于表示打开的文件。它包含与该文件一起工作所需的信息,包括一个非常复杂的 operation 指针列表、引用计数、指向相关 inode 的指针、当前的读/写位置等等。由于打开的文件可以有多个引用,必须有一种方法来把对这个结构的访问串行起来。在大多数情况下使用 f_lock 这个 spinlock,但还有一个名为 f_pos_lock 的互斥体是用来访问文件位置的。
获取和释放锁这个动作本身就是有开销的。许多 I/O 操作都会影响文件位置,因此 I/O 密集型的工作负载可能会导致重复获取和释放 f_pos_lock,增加内核的开销。然而,事实是,对打开的文件有多个引用的情况相对较少。如果对给定文件只有一个引用,就不可能发生对文件中多个位置的并发访问,而这个锁引入开销就会被浪费掉。为了避免出现这种浪费,获取 f_pos_lock(__fdget_pos())的函数中包含了一种优化:
if (file_count(file) > 1)
mutex_lock(&file->f_pos_lock);
这里的想法很简单:如果文件只有一个引用,那么并发访问是不可能发生的,因此没有必要获取锁,可以直接跳过 mutex_lock()调用。
io_uring 子系统自 2019 年引入以来一直在进行密集开发;它正在迅速成为许多内核功能的独立接口。目前正在开展的许多工作是添加与 waitid()、futex 和 getdents()相对应的 io_uring operation。最后一个 patch 将 getdents()系统调用添加到 io_uring 中,就跟这个主题有关了,因为 getdents() 在多次调用中需要很多次获取文件的位置(以及可能由文件系统底层实现来保存的状态),以允许进程通过多个调用读取一个内容非常多的目录。
io_uring 的"fixed files"特性也与碰到的问题有关;它允许一个文件在 io_uring 操作中被多次使用,而不需要承担使用常规系统调用时必需付出的每次调用的开销。这些开销包括要获取对文件的引用,并验证进程对其的访问权限,在 I/O 密集型应用程序中这种开销可能会很大;将文件 fix (固定住)就使得人们只需支付一次这个成本就好,从而提高性能。当一个文件被固定到 io_uring 中时,会创建一个新的引用,因此引用计数将增加。然而,进程可以在将文件固定到 io_uring 后关闭自己的文件描述符,从而只留下这个固定文件的引用。结果,引用计数将降至 1。在 io_uring 中进行的文件上的 I/O 操作正在进行时,这个引用计数也就一直保持不变。固定文件的目的就是避免重复获取和释放引用的开销。
Brauner 指出了 getdents() patch 中的一个问题:如果一个文件在 io_uring 中被固定下来,并且其引用计数为 1,那么在 io_uring 中可能同时运行多个 getdents()操作,每个操作都将在不获取锁的情况下访问 f_pos。这种并发操作的结果极有可能不是开发者所期望的。有人可能会争辩说,这是一种非常简单的"直接禁止这样做"的情况,但正如 Brauner 在他的修复该问题的 patch 中所描述的那样,io_uring 并不是唯一会遇到麻烦的情况。
在 2020 年,内核引入了一个有趣的系统调用,名为 pidfd_getfd(),允许有相应权限的进程从正在运行的进程中提取一个打开的文件描述符。这个操作在某些情况下非常有用,比如允许有特权的监管进程来执行其他进程无法自己执行的操作;例如,在容器之外打开文件。为了能正常工作,pidfd_getfd() 创建的文件描述符必须引用与目标进程中描述符相同的已打开的 file 结构。因此事实上是给该结构创建了第二个引用,也就相应地增加了引用计数。
然而,当目标进程正在进行 getdents()系统调用时如果它的文件描述符被 pidfd_getfd()抓取时会出现问题。由于在调用 getdents()时,文件的引用计数为 1,目标进程不会获取 f_pos_lock。如果获取了由 pidfd_getfd()获得的文件描述符的进程还将其传递给 getdents(),问题可能会发生。第二个调用将看到增加的引用计数并获取 f_pos_lock,但由于第一个调用没有获取该锁,获取将立即成功,导致两个 getdents()调用并发运行,再次产生设计之外的情况。
解决方案相当简单:只需无条件地删除对 f_count 的检查,并无条件获取 f_pos_lock。这将引入性能开销,但似乎没有人真正担心其影响,以至于根本没有人去实际测量这个开销。Linus Torvalds 在编辑了 changelog 后将此 patch 合入了 6.5-rc4 版本,他对 pidfd_getfd() 共享文件结构的方式提出了担忧。他表示最好是直接重新打开文件(创建新的文件结构),但这将违背 pidfd_getfd()的目的,因为新的文件描述符将不再可以用来代表其他进程执行操作。
尽管 Torvalds 对 pidfd_getfd() 引入的共享访问 struct file 仍然感到不满,但这种行为似乎已经成为一种即成事实了。无论如何,这个问题已经得到解决,为在 io_uring 中对固定文件(eventual)使用 getdents()清除了道路。但这个事件提供了一个关于如何在意想不到的方式中出现错误的示例,强调了对并发性的一些很小的假设可能会引出的意外问题。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~