LWN:让用户态来管理一组线程的调度!

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

User-managed concurrency groups

By Jonathan Corbet
December 28, 2021
DeepL assisted translation
https://lwn.net/Articles/879398/

内核的线程模型(thread model) 还是比较简单直接的,使用效果也很不错。但这还不足以满足所有用户的需求。具体来说,有一些使用场景会需要使用更轻量级的线程模型(lightweight threading model),也就是让用户空间来控制相关的调度决策。早在 2021 年 5 月的时候,Peter Oskolkov 就发布了一组 patch set,实现了一个抽象层,名为为用户管理的并发组(user-managed concurrency groups),简称 UMCG。不过在发布了几个改进版本之后,很多旁观者仍然不太确定这个 patch 应该怎么做,更不知道它是不是一个内核应该实现的功能。不过,随着 Peter Zijlstra 对 UMCG 进行了重新实现之后,事情有了转机。

这种由一个开发者重新实现了另一个开发者的 patch set 的情况,通常会引起人们的注意。Zijlstra 做这件事的动机也许可以从 https://lwn.net/ml/linux-kernel/20211215222524.GH16608%40worktop.programming.kicks-ass.net/ 这个讨论中看出来,他指出 UMCG 的代码看起来跟 scheduler 中的其他代码并没有什么区别。他还说需要做 "逆向工程(reverse engineering)" 来弄清楚 UMCG 是如何被使用的。在弄清楚这一点之后,也许他会更加清楚该如何来实现这部分代码。

事实上,UMCG 的文档并不比以前好多少,这对于一个希望增加进来的系统调用 API 来说是一个很大的缺点。但是我们可以通过代码(以及 Zijlstra 发布的 "相当粗糙" 的测试程序)来了解它。简而言之,UMCG 要求一个多线程的应用程序把自己划分为 "server" 和 "worker" 线程,针对系统中的每个 CPU 都可能有一个 server 线程。server 线程做出调度决策,而 worker 则根据这些决策来运行,从而完成实际工作。UMCG 这类系统的优点是,调度可以快速完成,而且内核的开销很小。当然,这一切的前提是 worker 线程实现正确。

Setting up

UMCG 引入了三个新的系统调用以及一个用来处理与内核的大部分通信工作的数据结构。每个参与 UMCG 的线程都必须有一个 umcg_task 结构,看起来像这样:

struct umcg_task {
  __u32 state;
  __u32 next_tid;
  __u32 server_tid;
  __u64 runnable_workers_ptr;
  /* ... */
};

有些字段在本文中省略了。请注意,因为这个结构最终会是由 C 库所提供的,因此可能会看起来与此不太相同。具体的字段会在后面用到的时候介绍。

第一个新增系统调用是 umcg_ctl(),用来在 UMCG 子系统中注册和取消注册(register and unregister)线程:

int umcg_ctl(unsigned int flags, struct umcg_task *self, clockid_t which_clock);

flags 参数用来指定要执行的操作,self 是当前线程相应的 umcg_task 结构,which_clock 控制用哪个 clock 来给此线程提供时间戳。

如果 flags 包含 UMCG_CTL_REGISTER,那就是向子系统注册一个新线程。这里有两种选择,取决于被注册的线程类型是什么:

  • 如果 flags 包含 UMCG_CTL_WORKER,那么这就是一个新的 worker task。在这种情况下,self->state 必须是 UMCG_TASK_BLOCKED,确保该 worker 最开始不会运行。还必须要在 server_tid 中指定后续会处理这个 worker 的 server 线程 ID。

  • 不包含的话,这就是一个 server task。其初始状态必须是 UMCG_TASK_RUNNING(因为此线程此时确实正在运行),server_tid 必须是 calling thread 的 ID。

worker 和 server 都必须是属于同一个进程的线程(更确切地说,它们必须共享同一个地址空间)。一切顺利的话,此系统调用会返回 0。不过对于 worker 来说会延后再返回,因为此时调用线程会被阻塞,直到服务器安排它开始运行为止。注册一个新的 worker 将会唤醒相关的 server 线程。

注册 worker 的同时,它的状态会被设置为 UMCG_TASK_RUNNABLE,然后添加到 server 的可用 worker 单向链列表之中。这个链表是通过位于每个 task 的 umcg_task 结构中的 runnable_workers_ptr 字段来实现的。内核将会使用 compare-and-exchange 的操作来把新 task 推到链表的头部。服务器通常会使用相同操作来把任务从链表中移除。

Scheduling

大多数调度操作是通过调用 umcg_wait() 完成的:

int umcg_wait(unsigned int flags, unsigned long timeout);

在当前 patch 中,flags 字段必须设置为 0。调用线程(calling thread)必须已经被注册为 UMCG 线程,否则的话此系统调用会失败。如果调用者是一个 worker 线程,那么 timeout 也必须是 0。这就会暂停 worker 当前的运行,来唤醒相关的 server 进程从而进行下一次调度决策。如果 worker 的状态是 UMCG_TASK_RUNNING(当前 task 正常来说应该是正在运行,否则的话是无法进行这个调用的),那么就改成 UMCG_TASK_RUNNABLE 状态,并且把该任务添加到 server 的 runnable_workers_ptr 列表中。因此,任何一个 worker task 都可以调用 umcg_wait() 从而在保持后续仍可以运行的情况下来让出 CPU 给另一个线程的方法。

如果调用者是一个 server,那么调用 umcg_wait() 通常是因为要安排一个新的 worker 来运行。具体实现方式是在调用之前先在 server 的 umcg_task 结构中的 next_tid 字段里设置上相关 worker 的线程 ID。在这样做之后,并且相关线程是处于 UMCG_TASK_RUNNABLE 状态的 UMCG worker 的话,那么就会放到队列里来继续运行。反之的话,server 就会被阻塞住,直到后续发生某个 wakeup 事件发生,或设定的 timeout 时间超时为止(如果 timeout 不为零的话)。

这里的一个重要细节是,内核一旦成功唤醒了新的 worker 线程,那么就会把 server 的 next_tid 字段设置为 0。这就可以允许 server 在从 umcg_wait() 返回时来快速地检查一下该线程是否真的被调度到了。

有几种 event 会导致 server 唤醒。比如当前运行的 worker 在系统调用中阻塞的话,其状态就会变为 UMCG_TASK_BLOCKED。server 可以通过查看(之前)运行的 worker 的 umcg_task 结构来判断是否是这种情况。如上所述,一个新的 task 变成 runnable 的话就会导致一个 wakeup 事件。如果编者对代码的理解正确的话,目前似乎没有办法在某个 worker task 已经完全退出的情况下来通知到 server 的。

Preemption

umcg_wait()的 timeout 参数会被 server 利用来在 worker 运行一段时间后进行强制抢占(forced preemption)。如果 umcg_wait() 返回 ETIMEDOUT,那么 server 就知道当前的 worker 运行的时间已经超过了 timeout 值,然后 server 就可以选择让它让出 CPU。具体来说分两步进行,首先是将 UMCG_TF_PREEMPT flag 添加到正在运行的 worker 的 state 字段中(同样要使用 compare-and-exchange 操作)。然后应该要调用第三个新增系统调用:

int umcg_kick(unsigned int flags, pid_t tid);

其中的 flags 必须为 0,tid 则是要被抢占的 worker 的线程 ID。这个调用会导致 worker 重新进入 scheduler,此时 UMCG_TF_PREEMPT flag 会被看到,然后就会暂停 worker 的执行,并将其放回 server 的 runnable_workers_ptr 链表中。在完成之后,server 会再次被唤醒,从而可以安排一个新的线程来执行。

这几乎就是目前的新 API 的全部内容了。不过,这项工作显然仍处于早期状态,估计在合并之前很可能还会有非常大的改动。UMCG 发源于谷歌的内部系统之中,反映的是谷歌特有的使用场景,但几乎可以确定这个功能还会有其他一些场景是有价值的,只是这些用户还没有提出他们的需求而已。随着对这项工作的进一步认识和了解,这种情况有望得到改变。

同时,正如人们所期望的那样,也需要说服 Oskolkov 他原来的成果确实有必要由其他人来重新实现,或者说新的实现方式更加优越一些。他对其中一些改动表示了不满,尤其是 Zijlstra 将 runnable worker 的单一队列改为了每个 server 都有一个队列。不过,最后他说:"如果所有需要的功能都已经实现了的话,我可以接受你的实现方案"。因此,似乎可以认为 Zijlstra 的 patch 已经指明了这项工作未来的样子。让时间告诉我们它的后续发展吧。

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

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

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

cca4244d048074189f114765df4d4738.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值