引入 在PREEMPT_RT(实时Linux)系统中,所有硬中断都被强制线程化(forced threading),中断处理程序运行在内核线程上下文中而非硬中断上下文。当一个IRQ的亲和性(affinity)发生变化时,中断线程的CPU亲和性也需要相应调整。本文基于Linux 6.18内核源码,分析IRQ亲和性变更如何传播到中断线程,以及一个优化patch。
IRQ亲和性变更机制 触发路径 当用户通过/proc/irq/*/smp_affinity修改IRQ亲和性时,调用路径为:
1 2 3 4 5 6 irq_affinity_write() // kernel/irq/proc.c -> irq_set_affinity() // kernel/irq/manage.c:462 -> __irq_set_affinity() -> irq_set_affinity_locked() -> irq_try_set_affinity() -> irq_do_set_affinity()
硬件层面的亲和性设置 irq_do_set_affinity()是核心函数,负责调用底层irqchip的irq_set_affinity回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int irq_do_set_affinity (struct irq_data *data, const struct cpumask *mask, bool force) { struct cpumask *tmp_mask = this_cpu_ptr(&__tmp_mask); struct irq_desc *desc = irq_data_to_desc(data); struct irq_chip *chip = irq_data_get_irq_chip(data); int ret; switch (ret) { case IRQ_SET_MASK_OK: case IRQ_SET_MASK_OK_DONE: cpumask_copy(desc->irq_common_data.affinity, mask); fallthrough; case IRQ_SET_MASK_OK_NOCOPY: irq_validate_effective_affinity(data); irq_set_thread_affinity(desc); ret = 0 ; } return ret; }
中断线程亲和性同步 irq_set_thread_affinity()负责通知所有关联的中断线程更新亲和性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static void irq_set_thread_affinity (struct irq_desc *desc) { struct irqaction *action ; for_each_action_of_desc(desc, action) { if (action->thread) { set_bit(IRQTF_AFFINITY, &action->thread_flags); wake_up_process(action->thread); } if (action->secondary && action->secondary->thread) { set_bit(IRQTF_AFFINITY, &action->secondary->thread_flags); wake_up_process(action->secondary->thread); } } }
这里使用了一个标志位IRQTF_AFFINITY来通知线程需要更新亲和性,而不是在持有自旋锁的上下文中直接调用set_cpus_allowed_ptr()。
中断线程的亲和性检查与更新 当中断线程被唤醒时(在irq_wait_for_interrupt()中),会检查并更新亲和性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static void irq_thread_check_affinity (struct irq_desc *desc, struct irqaction *action) { cpumask_var_t mask; bool valid = false ; if (!test_and_clear_bit(IRQTF_AFFINITY, &action->thread_flags)) return ; __set_current_state(TASK_RUNNING); if (!alloc_cpumask_var(&mask, GFP_KERNEL)) { set_bit(IRQTF_AFFINITY, &action->thread_flags); return ; } scoped_guard(raw_spinlock_irq, &desc->lock) { if (cpumask_available(desc->irq_common_data.affinity)) { const struct cpumask *m ; m = irq_data_get_effective_affinity_mask(&desc->irq_data); cpumask_copy(mask, m); valid = true ; } } if (valid) set_cpus_allowed_ptr(current, mask); free_cpumask_var(mask); }
线程主循环 中断线程的主循环结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 static int irq_wait_for_interrupt (struct irq_desc *desc, struct irqaction *action) { for (;;) { set_current_state(TASK_INTERRUPTIBLE); irq_thread_check_affinity(desc, action); if (kthread_should_stop()) { } if (test_and_clear_bit(IRQTF_RUNTHREAD, &action->thread_flags)) { __set_current_state(TASK_RUNNING); return 0 ; } schedule(); } } static int irq_thread (void *data) { struct irqaction *action = data; struct irq_desc *desc = irq_to_desc(action->irq); irqreturn_t (*handler_fn)(struct irq_desc *desc, struct irqaction *action); while (!irq_wait_for_interrupt(desc, action)) { irqreturn_t ret; ret = handler_fn(desc, action); } return 0 ; }
关键优化:立即唤醒中断线程 问题描述 在2024年1月,Crystal Wood提交了一个重要的patch (commit c99303a2d2a2 ),解决了CPU隔离(isolation)被破坏的问题。
问题场景 :在使用isolcpus=或nohz_full=进行CPU隔离的RT系统中:
用户将某个IRQ从隔离CPU(如CPU 4)迁移到非隔离CPU(如CPU 0-3)
IRQ的硬件亲和性已更新,但中断线程仍在睡眠
在下一个硬中断到来之前的时间窗口 内,中断线程仍持有旧的亲和性
如果此时线程因其他原因被唤醒,可能会在隔离CPU上运行
破坏了CPU隔离 ,引入不可预测的延迟
Patch内容 Before (只设置标志,不唤醒):
1 2 3 4 5 for_each_action_of_desc(desc, action) { if (action->thread) set_bit(IRQTF_AFFINITY, &action->thread_flags); }
After (设置标志并立即唤醒):
1 2 3 4 5 6 7 for_each_action_of_desc(desc, action) { if (action->thread) { set_bit(IRQTF_AFFINITY, &action->thread_flags); wake_up_process(action->thread); } }
工作流程对比 修改前: 1 2 3 4 5 6 7 8 用户修改affinity -> irq_do_set_affinity()成功 -> irq_set_thread_affinity() -> 只设置IRQTF_AFFINITY标志 -> 线程继续睡眠,等待下一个硬中断 -> [竞争窗口:线程可能带着旧亲和性在错误CPU上运行] -> 下一个硬中断到来 -> 线程唤醒,检查并更新亲和性
修改后: 1 2 3 4 5 6 7 8 9 10 用户修改affinity -> irq_do_set_affinity()成功 -> irq_set_thread_affinity() -> 设置IRQTF_AFFINITY标志 -> 立即wake_up_process() -> 线程立即在irq_wait_for_interrupt()中醒来 -> 检查到IRQTF_AFFINITY标志 -> 立即更新亲和性 -> IRQTF_RUNTHREAD未设置,线程继续睡眠 -> 亲和性已同步,消除了竞争窗口
关键细节 注意irq_thread_check_affinity()开头添加了:
1 __set_current_state(TASK_RUNNING);
这是必要的,因为线程在调用此函数前刚被设置为TASK_INTERRUPTIBLE状态。如果直接调用set_cpus_allowed_ptr()而状态不正确,可能导致问题。
总结
自动同步 :中断线程的亲和性会自动跟随IRQ的亲和性,无需手动设置
异步机制 :使用标志位+唤醒的异步机制,避免在持有自旋锁的上下文中进行可能睡眠的操作
RT系统优化 :通过立即唤醒中断线程,消除了亲和性变更和线程更新之间的时间窗口,防止CPU隔离被破坏
代码位置 :
kernel/irq/manage.c:187 - irq_set_thread_affinity()
kernel/irq/manage.c:1001 - irq_thread_check_affinity()
kernel/irq/manage.c:1042 - irq_wait_for_interrupt()
参考资料
本文由 AI ( GLM-4.7 ) 辅助撰写