4

LWN: 打开文件时避免文件名查找导致的阻塞操作!

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=Mzg2MjE0NDE5OA%3D%3D&%3Bmid=2247485181&%3Bidx=1&%3Bsn=754e70782dfd51975c9743bbbe598269
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

关注了就能看到更多这么棒的文章哦~

Avoiding blocking file-name lookups

By Jonathan Corbet January 21, 2021 DeepL assisted translation https://lwn.net/Articles/843163/ 

一般来说,当人们用 openat2() 这样的系统调用来打开一个文件时,人们的期望是这个系统调用直到完成工作之后才会返回。但有的时候,人们在打开文件是希望能够立即就打开文件,不要被阻塞(blocking)。Linux 一直没能很好支持这种使用模式,但随着 Jens Axboe 新提出的 patch set,这种情况终于可能要改变了。

打开一个文件可能是一个很复杂的操作。光是解析文件名这个简单任务,就可能需要遍历可能位于不同文件系统上的一系列目录,其中每一步还可能需要执行 I/O 操作,以及获取锁来保证操作流程顺序不会乱,否则可能会产生意外。在找到文件之后,可能还需要执行更多的 I/O 操作来打开这个文件本身。这些步骤中的每一个,都有可能会导致这个 open 操作被阻塞住不确定的时长。

Axboe 的 patch set 创建了一个名为 LOOKUP_CACHED 的新的内部 flag,调用 openat2()的函数可以通过设置 RESOLVE_CACHED 来利用这个内部 flag。这个 flag 会要求内核在这次 open 操作中,只是在完全不需要依赖缓存之外的数据的情况下,才正常执行完毕 open 操作。换句话说,就是这次 open 操作中不能引入任何 I/O 操作。如果达不到这个要求,那么 openat2() 直接返回 EAGAIN 错误。然后,调用者就可以在重新进行 openat2()操作,只是不再设置 RESOLVE_CACHED,也就是表明自己可以容忍阻塞,这样就能正常打开文件。

人们可能会好奇这个新选项是为了什么应用场景。程序在通常打开一个文件的时候并不会因为无法快速打开而放弃。实际上,这里的应用场景来自 io_uring 子系统,自从 io_uring 第一次出现以来,这两年时间里它已经有了很大的改进。io_uring 的原本用途是执行异步 I/O,但它越来越多地被用来运行与 I/O 操作混杂在同一个异步队列的其他系统调用,也包括 openat2()。

其他许多系统调用在设计之时就没有考虑到异步使用,所以在必要的情况下,它们会毫不犹豫地进入 block 状态,这是 io_uring 子系统无法允许的,因为这样一来就会阻塞其他操作的处理过程。所以 io_uring 创建了一个单独的内核线程来运行那些可能会在不恰当的时候阻塞的系统调用。这样一来,这些调用就成为异步操作了,但产生的代价是,因为将 ring 操作移到了一个单独的线程中,这就会大大减慢执行速度。对于那些只需要使用 cached data(缓存中的数据)就够了的系统调用来说,如果把他们放到另一个线程中去,所增加的开销会严重影响它的性能。

解决的办法就是使用这个新加的 LOOKUP_CACHED 标志。每当在 io_uring 中调用一个 open 操作时,就会尝试用 LOOKUP_CACHED 来直接执行。如果能成功,则一切正常,操作成功完成。否则的话,就会被迁移到上面说的线程中去,然后像以前一样,不使用 LOOKUP_CACHED 从而重新进行 open 操作。根据 Axboe 的说法,如果所有要用到的数据都已经在文件系统 cache 中了,那么进行大量 open 操作的测试 benchmark 的运行速度将近快三倍。

人们可能会想到另一个问题:为什么不直接使用现有的 O_NONBLOCK flag?这里有很多原因,但其中一个最显眼的原因是,open 操作产生的新的文件描述符的整个生命周期内,这个 O_NONBLOCK flag 都是生效的,因此所有后续对这个文件描述符进行的操作都会是 non-blockg(非阻塞)的。而 RESOLVE_CACHED flag 则只在文件 open 的阶段有影响。

很久以前内核开发者就想让 open()调用真正做到 non-blocking 了,甚至比 Linux 存在的时间还长,这一直是一个挑战。因此,人们看到这个 patch set 这么小的时候都感到惊讶。这里需要做的事情很少。这项工作向十年前加入内核的 RCU walk 机制中学习了不少。当时 RCU walk 的目的是尽可能避免占用锁,从而使文件名查找操作变得更快。这就需要在代码中执行路径查找时,如果会走到一些可能会 block 的操作,都需要能马上退出来。通常情况下,如果 RCU-walk 查找失败,就会用普通的 lookup 流程来重试这个路径查找操作。LOOKUP_CACHED patch set 只需要将 lookup 限制在 RCU-walk path 上,就可以达到自己的目的了。

这组 patch set 已经是第四版了。Linus Torvalds 对它有下面这段评论:

尽管已经对这组 patch 看了这么多次了,但考虑到上面的数据,以及考虑到这个需求在历史上引起了多大的问题(我还回忆得出以前我们关于 nonblocking open 的所有讨论),我仍然完全被它的小巧和整洁给打动了。

所以我的评论是 "this is the RightWay(tm)",依据就在于此。

不出意料的话,这项工作很可能会在 5.12 merge window 来合入 mainline。应用开发者最终是否会找到利用 RESOLVE_CACHED 的地方,这一点还不确定,但 io_uring 用户应该马上就可以从这个功能中受益了。

全文完

LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

rU7JbuJ.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK