LWN:在可重启序列(rseq)中增加虚拟CPU ID!

Linux News搬运工

共 3212字,需浏览 7分钟

 ·

2022-03-18 15:08

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

Extending restartable sequences with virtual CPU IDs

By Jonathan Corbet
February 28, 2022
DeepL assisted translation
https://lwn.net/Articles/885818/

可重启序列(restartable sequence)是 Linux 内核里的一项功能,用来便于在用户空间代码里使用了无锁的(lockless)、per-CPU 的代码,它已经存在一些年头了,但直到这个月才在 GNU C 库中得到支持。现在既然这个障碍已经被克服了,那么似乎是时候开始增加功能了。Mathieu Desnoyers 提供了一个 patch set 来给出更多功能,用它来增加了一个扩展机制(extension mechanism)和一个新的 "virtual CPU ID" 功能。

关于 restartable sequence 如何工作的概述,请参见 LWN 之前的文章。简单描述一下的话,就是任何使用 restartable sequence 的线程必须首先使用 rseq()系统调用来向内核注册一个特殊的结构。这个结构用来指向描述当前临界区 critical section(如果确实有的话)的 rseq_cs 结构。内核还确保在线程运行时,这个结构里会包含当前 CPU 的 ID 编号。与许多近期加入的系统调用的用法比较类似,rseq() 也要求调用者提供所传入的 rseq 结构的 size。

之所以使用这个 length 参数,是为了能支持未来对这个系统调用进行的扩展。新功能通常需要新的数据,也就会增加 rseq 结构的 size。通过查看用户空间传递的 size,内核可以知道此调用进程期望使用哪个版本的 rseq() API。只要使用得小心得当,那么这种机制就可以让现存的系统调用在后续扩展时仍然能保留与旧程序的兼容性。

对于那些需要确定他们正在使用的 API 版本的程序来说,仍然还有一个未解决的问题,那就是他们需要知道有哪些功能是可用的。有一种可能方案,就是用最新版本的 structure 来发起系统调用,如果调用失败则退回到更早的版本去。另一种方法是让内核公布一下它可以接受的 structure size。rseq() patch 采用了后一种方法,通过 getauxval() 来提供可接受的 structure 的最大 size。

在添加了这个扩展机制之后,该 patch set 继续添加了两个扩展,但并没有实际使用起来。这两个扩展给 rseq 结构添加了两个 32 位的数值,这确实让结构的 size 变长了。但是,由于该结构的定义方式是 32 字节对齐,因此尽管在扩展之前这个 struct 只需要 20 字节,但它实际上就已经分配得到了 32 字节。也就是说,用户空间仍然能够通过查看 getauxval()的返回值来判断是否支持新的值。由于新值(AT_RSEQ_FEATURE_SIZE)在这个补丁集出现之前并不存在,所以 getauxval()在旧内核上会返回 0。

rseq 结构中的第一个新增的值叫做 node_id,它包含的内容正是当前线程所运行的 NUMA 节点的 ID 号。这对于一些内存分配器(memory allocator)来说会很有用处,而且,正如在 patch changelog 中所指出的,它支持(配合早已存在的 CPU ID)完全由用户空间来实现的 getcpu()。

另一个新增的值有点偏题:它被称为 vm_vcpu_id。与同一结构中的 cpu_id 字段一样,它包含了一个整数 ID 编号,用来识别线程运行的 CPU。但是,虽然 cpu_id 包含了内核(和系统的所有其他地方)所知道的 CPU 的 ID 号,但是 vm_vcpu_id 与实际的 CPU 号并没有关系,它只是一个虚拟数字,由内核在一个此进程私有的数字空间中分配和管理。

这个新增的 CPU ID 似乎是为了满足在大系统中的少数几个 CPU 上运行线程的程序。请记住,rseq() 的目的是帮助程序获取到每个 CPU 的数据结构,这种结构通常采用数组的形式,并以当前的 CPU ID 号作为索引。这个数组必须大到足以容纳系统中每个 CPU 都有一个条目,而且每个条目都必须被正确地初始化和维护起来。

这只是处理 per-CPU 数据结构的任务的一部分。但是,想象一下,一个小型程序,只有十几个线程,在一个有 128 个 CPU 的大型服务器上运行。这些线程在运行时可能会在这些 CPU 上迁移,或者它们可能被绑定到一个特定的 CPU 子集上,可是尽管如此,per-CPU 的数据结构还是必须要为所有 128 个 CPU 都进行配置,这个做法并不是效率最佳的。如果能将 "per-CPU" 的数组大小跟程序的 size 匹配起来,而不是根据它所运行的系统的 CPU 数量而来,那就更完美了。

这就是 virtual CPU ID 的目的了。当一个线程被安排到一个(真实的)CPU 上时,这些号码由内核分配;内核努力确保同一进程中所有同时运行的线程有不同的虚拟 CPU ID。不过这些数字是从他们自己的数字编号空间中分配出来的,并尽量靠近 0。这就使得程序可以处理更少的 CPU 数量了,同时又保留了使用 per-CPU 数据结构的好处。

但这确实提出了一个有趣的问题:应用程序开发人员如何知道这个 virtual CPU 数量的可能范围是多少?当被问及这个问题时,Desnoyers 解释说:

我希望用户空间的代码使用一些合理的上限来作为提示,告诉内核大概需要多少个 per-vcpu 数据结构(这些都是需要被预先分配的),但在 vcpu ID 小于系统中处理器数量-1 的情况下,仍会有一个 "lazy initialization" 的后备措施。

人们可能期望 virtual CPU ID 由运行线程数量来限制,但情况并没有这么简单。因此,使用这个功能的话需要在用户空间这里增加一些复杂逻辑。

管理这些 virtual CPU ID 在 API 的内核这一面也有一个潜在的缺点:会有一些计算工作需要在调度器的上下文切换路径中完成,这是内核中最繁忙也是最关键的 performance-critical path 之一。人们都不喜欢在那里增加开销。Desnoyers 已经采取了一些措施来减少开销,这些措施在这个 patch 的更新日志中有所描述。例如,同一程序的两个线程之间的上下文切换只是将 virtual CPU ID 从正在离开的线程移动到正在进入的线程,而不需要原子操作。单线程程序也被特别处理,每个运行队列都有一个特殊的 virtual CPU ID 缓存,也可以用来避免原子操作。

这个 changelog 里面也包括了基准测试,它表明这些改动的性能影响在大多数情况下都是很小的。不过,这是否足以让 patch 通过 scheduler 维护者的 review 还有待观察,毕竟他们还没有对这一系列 patch 给出过评论。不过,如果这种机制最终被合并,那么它将成为另一种工具,可供那些希望在多线程应用程序中达到最佳可扩展性的开发者来使用。

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

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

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



浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报