
Linux PREEMPT_RT 实时内核运行原理从锁机制改造到中断线程化【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文以 Linux 内核 实时抢占文档theory.rst 为核心骨架系统讲解 PREEMPT_RT 如何通过可睡眠的自旋锁 优先级继承 强制中断线程化三大改造把传统内核中不可抢占的长临界区压缩到极少数底层路径从而显著缩短高优先级任务就绪到真正上 CPU 执行之间的调度延迟。读完本文你将理解 PREEMPT_RT 锁语义与普通内核的差异、优先级继承的运作机制、线程化中断的两阶段模型以及这些设计在源码中的具体落点可直接用于实时系统的内核配置与驱动代码审查。一、总览PREEMPT_RT 如何把内核变成实时内核在非 PREEMPT_RT 的内核中大量执行路径如自旋锁临界区、中断处理函数运行在禁止抢占或禁止中断的上下文里调度器在这些区间内完全失效——即使一个更高优先级的任务已经就绪也只能干等。PREEMPT_RT 的核心目标就是把这些不可抢占的执行路径全部交还给调度器。正如 theory.rst 前言 所概括的PREEMPT_RT 通过两项关键改造实现这一点锁原语替换把spinlock_t等自旋锁替换为可抢占、支持优先级继承的rtmutex实现强制中断线程化把中断处理移入受调度器管理的内核线程上下文。改造完成后内核变得几乎完全可抢占仅剩下少数真正必须原子执行的代码路径仍然关闭抢占包括入口代码entry code、调度器本身、底层中断处理例程。这一点在内核配置中也有对应描述——kernel/Kconfig.preempt中CONFIG_PREEMPT_RT的帮助文本kernel/Kconfig.preempt明确写道该选项通过替换自旋锁/读写锁等锁原语、强制执行中断线程化、引入拆分长非抢占段long non-preemptible sections的机制使内核除极少数底层关键路径外完全可抢占并把大多数执行上下文纳入调度器控制。二、调度基础SCHED_OTHER 与 SCHED_FIFO 的行为差异理解 PREEMPT_RT 之前先要理解实时调度策略与传统调度策略的本质区别。Linux 调度及相关的用户空间 API 在sched(7)手册页中有完整说明本文只摘取与实时性直接相关的核心差异。默认策略 SCHED_OTHERCFS一个任务只有在调度器判定它相对其他可运行任务已经消耗了公平份额的 CPU 时间时才会被抢占。关键在于当一个新的 SCHED_OTHER 任务变为可运行状态时策略并不保证当前任务会被立即抢占——当前正在运行的任务可能继续执行下去就绪的新任务只能排队等待。实时策略 SCHED_FIFO当一个带实时策略的任务变为可运行且其优先级高于当前正在运行的任务时调度器立即选中它执行。该任务会持续运行直到它主动让出 CPU典型情况是阻塞在某个事件上或被更高优先级的实时任务抢占。这一差异正是实时系统的基石PREEMPT_RT 的所有改造本质上都是为了让高优先级任务就绪这个事件能够立刻打断任何非关键路径的执行上下文而不是等待自然调度点。三、睡眠自旋锁spinlock_t 在 PREEMPT_RT 下的语义转变3.1 非 PREEMPT_RT 内核自旋 关抢占在普通内核中spinlock_t的获取分两步先关闭抢占preempt_disable然后自旋等待直到锁可用释放时再恢复抢占。从实时性角度看这种设计有严重缺陷——关闭抢占意味着调度器无法切换到更高优先级的任务即使该任务已经就绪。锁持有时间越长优先级反转和延迟抖动越严重。内核文档 Documentation/locking/locktypes.rst 对锁类型做了系统分类spinlock_t/rwlock_t在非 PREEMPT_RT 内核中属于自旋锁Spinning locks并隐式关闭抢占其带后缀的变体提供额外保护——_bh()关闭/使能下半部软中断、_irq()关闭/使能中断、_irqsave/restore()保存并关闭/恢复中断状态。3.2 PREEMPT_RT 内核基于 rtmutex 的睡眠锁为解决上述问题PREEMPT_RT 把自旋锁替换为可睡眠的自旋锁sleeping spin locks其实现基于rtmutex。当一个任务试图获取一个已被占用的锁时它不再自旋而是执行以下动作禁止 CPU 迁移migrate_disable——这与关闭抢占有相同的效果任务被钉在当前 CPU 上因此对 per-CPU 变量的指针引用保持有效向锁的持有者捐赠自己的优先级即优先级继承详见第四节主动调度出去voluntary schedule out睡眠等待锁变为可用。关键点在于禁止 CPU 迁移 ≠ 禁止抢占。任务在持有睡眠锁期间仍然可以被抢占只是保证不会被调度到其他 CPU。这正是 PREEMPT_RT 的巧妙之处——既保证了 per-CPU 数据结构访问的安全性这是自旋锁原本提供的又不再阻塞调度器切换更高优先级任务。3.3 源码佐证语义变化的深层含义Documentation/locking/locktypes.rst 详细列出了 PREEMPT_RT 下spinlock_t语义的全部变化不关闭抢占PREEMPT_RT 中spinlock_t映射到独立的基于 rt_mutex 的实现获取/释放不再影响 CPU 的抢占状态_irq/_irqsave后缀不再影响 CPU 中断关闭状态因为中断已被线程化见第五节不再需要以关中断方式与硬中断上下文同步但_bh()后缀仍会关闭软中断处理——非 PREEMPT_RT 靠关闭抢占实现这一效果PREEMPT_RT 则使用 per-CPU 锁做串行化同时保持抢占开启持有锁的任务不会迁移非 PREEMPT_RT 靠关闭抢占防迁移PREEMPT_RT 靠关闭迁移migrate_disable因此即使任务被抢占per-CPU 指针依然有效任务状态在锁获取期间被保存与恢复由于任务可能在获取锁时阻塞PREEMPT_RT 会在阻塞前保存task-state到saved_state置为TASK_UNINTERRUPTIBLE后schedule()锁唤醒时恢复saved_state。若在等待期间收到其他唤醒源则该唤醒改写saved_state而非直接置RUNNING从而保证真正的唤醒不丢失。同时locktypes.rst强调了两类重要边界raw_spinlock_t在所有内核中都是严格自旋锁包括 PREEMPT_RT仅在真正的核心代码、底层中断处理、需要关闭抢占/中断访问硬件状态的场景使用其临界区内禁止获取普通spinlock_t/rwlock_t也禁止调用内存分配器PREEMPT_RT 下内存分配器完全可抢占不能在真正原子上下文调用——典型反例是raw_spin_lock()后调用kmalloc(..., GFP_ATOMIC)在 PREEMPT_RT 下会失败位自旋锁bit spinlocks无法被替换单个 bit 放不下一个 rt_mutex因此语义在 PREEMPT_RT 下保持不变raw_spinlock_t的注意事项同样适用。此外锁嵌套规则也随之调整PREEMPT_RT 把spinlock_t/rwlock_t从自旋类别改为睡眠类别、把local_lock替换为 per-CPU 的spinlock_t因此它们不能在被raw_spinlock_t持有的情况下获取。由此形成严格的三级嵌套顺序① 睡眠锁 → ② spinlock_t / rwlock_t / local_lock → ③ raw_spinlock_t 与位自旋锁lockdepCONFIG_PROVE_LOCKING会在违反约束时报错。四、优先级继承Priority Inheritance4.1 机制原理PREEMPT_RT 下spinlock_t、mutex等锁都建立在 rtmutex 之上而 rtmutex 的核心价值就是优先级继承PI当一个任务阻塞在锁上时PI 机制会把它阻塞者的调度参数临时传播给锁的持有者。文档中给出了经典示例场景SCHED_FIFO 任务 A 阻塞在一个当前由 SCHED_OTHER 任务 B 持有的锁上动作A 的调度策略与优先级临时被 B 继承随后 A 进入睡眠等待效果B 事实上变成系统中最高优先级的任务得以继续执行、推进、最终释放锁收尾B 释放锁后恢复原有调度参数A 恢复运行。这解决了经典的优先级反转问题——低优先级任务因持有高优先级任务所需的锁而阻止后者运行如果没有 PI高优先级任务会被无限期饿死有了 PI低优先级持有者被提升到高优先级尽快完成临界区并释放锁。4.2 源码落点在 kernel/locking/rtmutex.c 中可以看到 PI 的完整实现脉络锁的等待者队列pi_waiters以优先级排序task_top_pi_waiter(p)返回优先级最高的等待者其任务即为pi_taskrtmutex.c当pi_task存在时调用rt_mutex_setprio(p, pi_task)把持有者p的优先级提升到最高等待者水平注释中的boost()/deboost()描述rtmutex.c刻画了提权与去权的路径——提权沿持有链逐级传播去权同样逐级回溯确保提升谁、就恢复谁。需要说明的是并非所有锁都支持 PI信号量semaphore在 PREEMPT_RT 下不做替换——计数信号量没有所有者概念无法确定提升对象因此阻塞在信号量上仍可能发生优先级反转详见 locktypes.rstrw_semaphore / rwlock_t 在 PREEMPT_RT 下映射到基于 rt_mutex 的实现但公平性发生变化写者无法把自己的优先级授予多个读者被抢占的低优先级读者继续持有锁可能导致高优先级写者饥饿反之读者可以把优先级授予写者低优先级写者会被提升直到释放锁从而避免写者饿死读者。五、线程化中断Threaded Interrupts5.1 为什么需要线程化中断处理函数是另一类在关闭抢占、脱离调度器控制下执行的代码。为了把中断处理纳入调度器管理PREEMPT_RT 强制执行线程化中断处理。5.2 两阶段模型线程化后中断处理被拆成两个阶段阶段执行上下文职责主处理函数primary handlerIRQ 上下文中断关闭唯一职责是唤醒对应的线程化处理函数线程化处理函数threaded handler进程上下文由内核调度即传给request_irq()的中断处理函数实际完成设备处理从唤醒中断线程到线程化处理完成之间中断源在中断控制器中保持屏蔽masked设备中断保持 pending 状态但不会再次触发 CPU系统得以退出 IRQ 上下文在可调度的线程中从容处理中断。5.3 默认调度参数SCHED_FIFO 优先级 50文档明确指出默认情况下线程化处理函数以SCHED_FIFO 策略、优先级 50MAX_RT_PRIO / 2运行——正好位于最小与最大实时优先级的中点。源码常量可验证这一点include/linux/sched/prio.h定义#define MAX_RT_PRIO 100prio.h实时优先级范围为 0..9950 恰为中点。5.4 软中断的处理如果线程化中断处理函数在执行期间触发了软中断softirq这些软中断例程会在同一线程内、线程化处理函数完成之后被调用且执行软中断处理期间抢占保持开启。这意味着 PREEMPT_RT 下不能假设软中断上下文是不可抢占的——Documentation/core-api/real-time/differences.rst 特别警告不要依赖local_bh_disable()在进程上下文保护 per-CPU 变量因为软中断处理函数在 PREEMPT_RT 下可被抢占这种同步方式不可靠。5.5 源码落点irq_thread 与强制线程化在 kernel/irq/manage.c 中可以印证整个线程化模型irq_thread()manage.c是中断线程主体通过sched_set_fifo(current)把线程设为 SCHED_FIFO循环调用irq_wait_for_interrupt()等待中断、执行处理函数irq_forced_thread_fn()manage.c是强制线程化时的包装函数对非 PREEMPT_RT 构建它需local_bh_disable()local_irq_disable()模拟硬中断上下文而对 PREEMPT_RT 构建则跳过local_irq_disable()——这正是中断已线程化、无需关中断的代码级印证!IS_ENABLED(CONFIG_PREEMPT_RT)条件分支setup_irq_thread()创建名为irq/%d-%s的内核线程manage.c例外情况以IRQF_NO_THREAD、IRQF_PERCPU、IRQF_ONESHOT标志请求的中断不会被强制线程化irq_setup_forced_threading()中的检查manage.c。其中IRQF_ONESHOT用于只提供线程处理函数的request_threaded_irq()场景保证中断线保持屏蔽直到线程处理函数完成若此时还提供了主处理函数该主函数不会被线程化因此绝不能在其中获取睡眠锁且应保持最小化、避免忙等硬件寄存器。六、相关配置与延伸阅读PREEMPT_RT 的行为直接由内核配置选项驱动。除CONFIG_PREEMPT_RT本身外kernel/Kconfig.preempt依赖EXPERT ARCH_SUPPORTS_RTCONFIG_PREEMPT_RT_NEEDS_BH_LOCKkernel/Kconfig.preempt用于在怀疑可抢占软中断出错时强制旧式的软中断同步行为做测试对比。对于构建实时系统的集成者Documentation/core-api/real-time/kernel-configuration.rst 给出了影响最坏延迟的关键选项建议与本文主题直接相关CONFIG_PREEMPT_RT必须启用严重级别 fatal否则内核不具实时能力CONFIG_CPU_FREQ/CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE建议启用high——实时负载期望 CPU 频率在执行期间固定不变performance 调速器是最简单的达成方式非 performance 调速器尤其是ONDEMAND应禁用因其频率变化依赖负载行为会显著破坏确定性CONFIG_CPU_IDLE启用但建议用processor.max_cstate1把最大 C 状态限制在 C1避免深睡眠状态带来的进出延迟与缓存冲刷CONFIG_EFI_DISABLE_RUNTIME建议启用medium——调用 EFI 运行时服务期间系统可能无法响应中断造成延迟尖峰PREEMPT_RT 默认启用也可用efinoruntime启动参数禁用CONFIG_NO_HZ/CONFIG_NO_HZ_FULL建议禁用medium——无 tick 模式会增加内核到用户态切换延迟周期型负载如每 100µs 的控制循环应保持固定 tickCONFIG_TRACING建议启用但生产运行时不激活CONFIG_IRQSOFF_TRACER/CONFIG_PREEMPT_TRACER即使不激活也有可观开销建议禁用high调试选项开发测试期鼓励开启 lockdepCONFIG_PROVE_LOCKING等调试选项以暴露锁错误但生产构建应禁用high——CONFIG_LOCKUP_DETECTOR会周期性在硬 IRQ 上下文执行定时器回调、CONFIG_PROVE_LOCKING显著增加最坏延迟。七、总结PREEMPT_RT 的设计哲学可以浓缩为一句话用可睡眠的锁替换自旋锁、用内核线程承接中断处理把关闭抢占/关闭中断的代码区间压缩到最小把绝大多数执行上下文重新交还给调度器。通过 theory.rst 所述的三大机制PREEMPT_RT 实现了高优先级任务就绪即抢占这一实时内核的基本承诺调度实时策略SCHED_FIFO保证高优先级任务就绪即被选中替代 SCHED_OTHER 的公平但不即时睡眠自旋锁spinlock_t基于 rtmutex获取竞争锁时禁止迁移 优先级继承 主动调度出而非自旋 关抢占线程化中断主处理函数仅唤醒线程实际处理在 SCHED_FIFO 优先级 50 的内核线程中完成软中断随后在同一线程、可抢占状态下执行。最终效果正如文档所总结PREEMPT_RT 显著减少了中断或抢占被关闭的代码段使调度器能够随时抢占当前执行上下文并切换到更高优先级的任务——这正是实时系统可预测延迟bounded latency的根本保障。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考