PREEMPT_RT 技术细节

PREEMPT_RT 补丁的主要目的是最大限度地减少不可抢占的内核代码量。因此,实施了几种替代机制和新机制。这些机制在主线 Linux 中部分可用。从版本 5.15 开始,PREEMPT_RT 补丁已部分合并到主线 Linux 内核中,并通过 CONFIG_PREEMPT_RT 启用。


高精度计时器

高精度计时器允许精确的定时调度,并消除了计时器对定期调度器 tick (jiffies) 的依赖性。

除了最近的 glibc 外,使用高分辨率计时器没有特殊要求。当在 Linux 内核中启用高分辨率定时器时,nanosleep、itimers 和 posix 定时器将提供高分辨率模式,而无需更改源代码。对高分辨率计时器的动态优先级支持 (在实时抢占补丁中提供扩展)

启用实时抢占后,在高分辨率定时器中断的硬中断上下文中,无法在 itimer 和 posix 间隔定时器到期时传送信号。由于锁定约束,信号传递必须在线程上下文中发生。为了避免长时间延迟,softirq 线程在不久前的实时抢占补丁中被分离。虽然这种分离显着增强了行为,但仍然存在一个问题。hrtimers softirq 线程可以被更高优先级的任务任意长时间延迟。一个可能的解决方案是提高 hrtimer softirq 线程的优先级,但这会产生所有与计时器相关的信号都以高优先级传递的效果,因此会给高优先级任务带来延迟影响。实时抢占补丁的早期版本已经包含了此问题的解决方案:根据必须传递信号的任务的优先级动态调整 softirq 优先级。

随着高分辨率计时器补丁的返工,以及优先级继承代码的微妙争用条件,此功能被删除。RT-Mutexes 的新设计和调度进程中的核心 PI 支持消除了此争用条件,并允许重新实现此功能。在 PentiumIII 400 MHz 测试机器上,此更改将等待周期性信号传送的线程的最大用户空间延迟从满系统负载下的 ~400 秒显着降低到 ~90 微秒。

(clock_) nanosleep 函数不会遇到此问题,因为定时器到期时的唤醒函数是在高分辨率定时器中断的上下文中执行的。如果应用进程未使用异步信号处理进程,则建议使用设置了 TIMER_ABSTIME 标志的 clock_nanosleep () 函数,而不是等待定期计时器信号传递。应用进程必须保持下一个间隔本身的绝对到期时间,但这是添加和规范化两个结构 timespec 值的轻量级操作。这样做的好处是显着降低了最大延迟 (~50us) 和一般的操作系统开销。

线程中断处理

PREEMPT_RT 补丁进程强制使用线程中断处理进程的机制。因此,所有中断处理进程都在线程上下文中运行,除非它们标有 IRQF_NO_THREAD 标志。这种机制也可以在 Linux 主线内核中强制执行 PREEMPT_RT 无需通过内核命令行选项 threadirqs 进行修补。但是结果行为有很小的差异。

在主线 Linux 中,中断服务例程在禁用硬件中断的情况下在硬中断上下文中处理。禁用硬件中断意味着抢占和软中断也被禁用。中断处理进程在中断服务例程的上下文中进行处理,同时禁用硬中断。内核命令行选项『threadirqs』将中断处理进程更改为在线程上下文中运行。线程的调度进程策略设置为 SCHED_FIFO,默认优先级为 50.

所有标有 IRQF_NO_THREAD 标志的中断处理进程都不会被线程化,并在禁用硬中断的情况下运行。例如,处理器间中断 (IPI) 使用此标志。使用 IRQF_TIMER 或 IRQF_PER_CPU 标志设置的中断处理进程被隐式标记为 IRQF_NO_THREAD.

PREEMPT_RT 补丁强制执行『threadirqs』命令行选项。主线实现和 PREEMPT_RT 实现中线程中断的行为之间存在细微差异,如下表所示:

  Mainline PREEMPT_RT
hard interrupts disabled    
soft interrupts disabled
preemption disabled  

RCU

主线 Linux 中的 RCU 机制只有在设置了 CONFIG_PREEMPT 时才具有抢占性 (抢占模型:『Low-Latency Desktop』)。PREEMPT_RT 抢占模型都使用抢占式 RCU 机制。此外,PREEMPT_RT 补丁消除了所有中间状态的 RCU 处理,并且仅在自己的线程中对其进行处理。

在主线 Linux 中,RCU 在 softirq 上下文中执行大量处理,在此期间禁用抢占。需要配置 RCU 以避免此处理,从而避免由此产生的延迟降低。

RCU Callback Offloading

默认情况下,在主线 Linux 中,RCU 回调是在 softirq 上下文中调用的。这些回调通常会释放内存,因此内存分配器在采用慢速路径时可能会施加较大的延迟。尽管这些延迟无法避免,但可以通过使用 RCU 回调卸载将它们定向到选择的 CPU.

要卸载回调,使用 CONFIG_RCU_NOCB_CPU=y 构建内核。要在所有 CPU 上启用回调卸载,使用 CONFIG_RCU_NOCB_CPU_ALL=y 进行构建。如果希望更具选择性,使用 rcu_nocbs 内核启动参数指定要卸载的 CPU 列表。例如,rcu_nocbs=1, 3-4 将在 CPU 1、3 和 4 上启用回调卸载。卸载只能在启动时指定,不能在运行时更改。

每个具有卸载回调的 CPU 都会有一组 rcuo kthread。例如,CPU 1 将具有 rcuob/1(用于 RCU-bh)、rcuop/1 (用于 RCU-preempt) 和 rcuos/1 (用于 RCU-sched)。这些 kthread 可以分配给特定的 CPU,并可以根据需要分配调度优先级。

当然,天下没有免费的午餐。使用 RCU 回调卸载意味着 call_rcu () 由于原子操作、缓存未命中和唤醒而产生更大的开销。仅唤醒开销就可能导致某些工作负载的吞吐量下降数十个百分点,这就是 Linux 发行版默认为不回调卸载的原因。可以使用 rcu_nocb_poll 内核引导参数将此唤醒开销从调用 call_rcu () 的任务转移到 rcuo kthreads,但代价是轮询唤醒导致的能源效率下降。将 rcuo kthread 分配给特定 CPU 时需要小心,例如,将所有这些 kthread 放在单个 CPU 上可能会使该 CPU 过载,这可能会限制回调调用,甚至可能对系统造成 OOMing.

任何 nohz_full CPU 的 RCU 回调也将卸载。此操作模式还可以正常处理受 CPU 限制的实时用户空间线程。

RCU 优先级提升

抢占式 RCU 的一个潜在缺点是,低优先级任务可能会在 RCU 读取端关键部分的中间被抢占。如果系统的高优先级任务消耗了所有可用的 CPU,则该低优先级任务可能永远不会恢复,因此可能永远不会离开其关键部分。这反过来又会阻止 RCU 宽限期完成,最终对系统进行 OOMing.

这通常表示存在设计或配置错误:事件驱动的实时应用进程应留出大量空闲时间,以避免调度进程中的排队延迟等。这种空闲时间将允许低优先级任务继续进行,从而允许宽限期完成,从而避免 OOM。

但是,可能会发生错误,包括涉及高优先级实时线程中的无限循环的错误。如果系统由于 OOM 而一直挂起,则调试这些问题会更加困难。简化调试的一种方法是使用 CONFIG_RCU_BOOST=y 进行构建,默认情况下,这会将阻止当前宽限期超过半秒的任务提升到实时优先级 1。其他 Kconfig 选项 CONFIG_RCU_KTHREAD_PRIO 和 CONFIG_RCU_BOOST_DELAY 提供对 RCU 优先级提升的额外控制。有关详细信息,请参阅 Kconfig 帮助文本。