RT Linux中的IRQ亲和性变更与中断线程同步机制

引入

在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
// kernel/irq/manage.c:220
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;

// ... 省略mask处理逻辑 ...

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
// kernel/irq/manage.c:187
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
// kernel/irq/manage.c:1001
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); // 必须先设置为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
// kernel/irq/manage.c:1042
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系统中:

  1. 用户将某个IRQ从隔离CPU(如CPU 4)迁移到非隔离CPU(如CPU 0-3)
  2. IRQ的硬件亲和性已更新,但中断线程仍在睡眠
  3. 在下一个硬中断到来之前的时间窗口内,中断线程仍持有旧的亲和性
  4. 如果此时线程因其他原因被唤醒,可能会在隔离CPU上运行
  5. 破坏了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); // 没有wake_up_process
// ...
}

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()而状态不正确,可能导致问题。

总结

  1. 自动同步:中断线程的亲和性会自动跟随IRQ的亲和性,无需手动设置
  2. 异步机制:使用标志位+唤醒的异步机制,避免在持有自旋锁的上下文中进行可能睡眠的操作
  3. RT系统优化:通过立即唤醒中断线程,消除了亲和性变更和线程更新之间的时间窗口,防止CPU隔离被破坏
  4. 代码位置
    • 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 ) 辅助撰写