想象这样一个场景:一个用户态线程(例如线程A)获得了一个自旋锁(spinlock)。与内核态的自旋锁不同,用户态的 spinlock 通常不具备禁止抢占的能力。这意味着,线程A完全有可能在执行临界区代码时被其他线程(例如线程B)抢占。如果线程B恰好需要运行很长时间,而另一个等待同一把锁的线程C就只能干等着,这种等待可能会变得异常漫长。

那么,有没有办法缓解这个问题呢?如果我们能提供一种机制,当持有锁的线程(线程A)时间片用尽、即将被抢占时,调度器能够“网开一面”,为它短暂地延长一点执行时间,比如50微秒。在这段额外的保护期内,线程A不被抢占,它便有很大概率能完成临界区的执行并释放锁,从而避免让等待者(线程C)苦等,也规避了由此可能引发的一系列连锁问题。
这个机制就是延迟抢占或称为调度器时间片扩展(time slice extension)。在 Linux内核 中,具体扩展时长的上限由一个名为 rseq_slice_extension_nsec 的系统控制参数决定。
用户空间如何启用与使用
用户空间的线程首先需要通过 prctl() 系统调用显式地告知内核,它希望使用时间片扩展功能:
prctl(PR_RSEQ_SLICE_EXTENSION, PR_RSEQ_SLICE_EXTENSION_SET,
PR_RSEQ_SLICE_EXT_ENABLE, 0, 0);
内核与用户态之间通过一种称为“可重启序列”(Restartable Sequences,简称 RSEQ)的每CPU(per-cpu)数据结构进行交互。一个典型的使用时间片扩展的用户态代码逻辑如下(伪代码风格):

rseq->slice_ctrl.request = 1;
barrier(); // 防止编译器重排
critical_section(); // 执行临界区代码
barrier(); // 防止编译器重排
rseq->slice_ctrl.request = 0;
if (rseq->slice_ctrl.granted)
rseq_slice_yield();
内核与用户态的协作流程
一旦线程通过 prctl() 启用了该机制,它就可以通过将 rseq::slice_ctrl::request 设为 1 来主动请求延长当前时间片。
当该线程被中断,并且内核因此产生了重新调度的请求时,内核会检查这个用户态设置的 request 标志。如果发现其值为 1,内核可能会选择不立即将该线程换下 CPU,而是批准一次时长不超过 rseq_slice_extension_nsec 的时间片扩展,然后直接返回用户态让线程继续执行。
当内核同意进行时间片扩展时,它会执行两个操作:
- 将
rseq::slice_ctrl::request 清零。
- 将
rseq::slice_ctrl::granted 置为 1,以通知用户态扩展请求已获批准。
如果在这次扩展之后,线程仍然因为某些原因被重新调度(换下 CPU),内核在切换上下文前,会再将 granted 位清零。这相当于告诉用户态:“之前批准的扩展已经结束或失效了”。用户态代码在临界区结束后,可以检查 granted 标志,如果为真,则可能通过调用 rseq_slice_yield() 等函数主动让出 CPU,以示公平。
背景与现状
这项基于 RSEQ 的时间片扩展功能,主要由内核开发者 Thomas Gleixner 和 Peter Zijlstra 完成,预计将随 Linux 6.13 或更高版本的内核与大家见面。相关的补丁集讨论可以在 Linux 内核邮件列表中找到:
https://lore.kernel.org/lkml/20251215155615.870031952@linutronix.de/
这项改进针对的是并发编程中长期存在的细粒度锁争用痛点,旨在减少不必要的等待和调度开销,对于提升用户态高性能并发编程库和中间件的性能有积极意义。如果你想深入了解计算机基础中关于调度、同步原语等更系统的知识,可以在 云栈社区 的 计算机基础 板块找到丰富的资料。
|