CHAPTER 05

硬中断线程化

设备中断的 handler 原本运行在一个"比任何任务都高、谁也打断不了"的上下文里。PREEMPT_RT 把它几乎整个搬进一个名叫 irq/N-name 的 SCHED_FIFO 内核线程,只在硬中断里留下"屏蔽这根线、叫醒线程"这两步。本章从 GICv3 硬件讲到 irq_thread(),说清这件事怎么做、为什么安全、代价是什么。

5.1问题:硬中断是调度器看不见的"超级优先级"

在非 RT 内核里,一个设备中断从 CPU 取异常那一刻起,到 handler 返回、irq_exit_rcu() 跑完软中断为止,整个过程都满足三个条件:

  1. 中断是关着的:ARM64 取 IRQ 异常时硬件自动置位 PSTATE.DAIF.I,entry 代码也明确写 DAIF_PROCCTX_NOIRQ(arch/arm64/kernel/entry-common.c:635)。
  2. 不可抢占:irq_enter_rcu() 给 preempt_count 加上 HARDIRQ_OFFSET,调度器在这期间不会切换任务。
  3. 不嵌套:Linux 给所有普通中断设成同一个 GIC 优先级 GICV3_PRIO_IRQ (0xc0),一个 handler 跑着时同一 CPU 上别的中断进不来(见 第 8 章)。

结果是:哪怕当前 CPU 上正在跑一个 SCHED_FIFO 99 的任务,任何一个设备中断都可以把它挂起,时长完全由驱动作者决定。调度器给任务设的优先级,在硬中断面前全部失效。网卡、存储、USB、GPU 驱动的 handler 动辄做几十微秒的事,再加上紧跟其后的 softirq,一个"低重要性"的中断就能让最高优先级的控制循环错过截止时间。

核心矛盾

中断"何时到来"由硬件决定,这一点谁也改变不了;但中断"处理多久、在什么优先级处理"本应由系统设计者决定。非 RT 内核把这两件事绑在了一起。线程化要做的,就是把它们拆开:硬中断里只做"接住",处理交给调度器排队。

5.2硬件路径:GICv3 怎样把一个中断送到 CPU

线程化动不了硬件这一段,但要理解"硬中断里最少必须做什么",得先看清 GICv3 的状态机。GICv3 由四部分组成:

外设(电平/边沿) SPI 线 · INTID 32–1019 PCIe 设备 (MSI) 写 GITS_TRANSLATER Distributor (GICD) 使能 / 优先级 / 路由 GICD_IROUTER · ICENABLER ITS DeviceID+EventID → LPI INTID ≥ 8192 Redistributor 每 CPU 一个 SGI 0–15(IPI) PPI 16–31(timer) LPI pending 表 CPU interface ICC_* 系统寄存器 ICC_PMR_EL1 ICC_IAR1_EL1 ICC_EOIR1_EL1 ICC_DIR_EL1 ICC_RPR_EL1 ICC_SGI1R_EL1 PE(CPU 核) PSTATE.DAIF.I = 0 才接收 IRQ 取异常 → VBAR_EL1 + 0x280 / 0x480 → el1h_64_irq / el0t_64_irq → gic_handle_irq() IRQ 信号 PREEMPT_RT 对这一侧一行都不改: 判优、路由、ack、优先级下降都是硬件行为。 RT 能做的,是让 CPU 在取异常之后 尽快离开硬中断上下文。
GICv3 的四个组成部分。SPI 经 Distributor 路由到一个 CPU,LPI 经 ITS 翻译,SGI/PPI 属于每个 CPU 的 Redistributor。CPU interface 是一组 EL1 系统寄存器,而不是 MMIO。

一个中断在 GIC 里有四种状态:inactive → pending → active → inactive(电平中断在处理期间仍有效时还会处于 active and pending)。三条指令推动这个状态机:

动作寄存器效果Linux 中的位置
AcknowledgeICC_IAR1_EL1(读)返回 INTID;该中断 pending→active;CPU 运行优先级升到该中断的优先级gic_read_iar() irq-gic-v3.c:909
Priority dropICC_EOIR1_EL1(写)运行优先级回落,同优先级的其他中断可以再次送达gic_complete_ack() irq-gic-v3.c:846
DeactivateICC_DIR_EL1(写)active→inactive,这根线才能再次进入 pendinggic_eoimode1_eoi_irq() irq-gic-v3.c:735
ARM64 / GICv3:EOImode 1(split EOI)

内核从 EL2 启动时(几乎所有服务器和带虚拟化的 SoC),GICv3 驱动启用 supports_deactivate_key,日志里会打印 GIC: Using split EOI/Deactivate mode(irq-gic-v3.c:2066–2070)。这时 __gic_handle_irq() 在读完 IAR 后立刻写 ICC_EOIR1_EL1 做优先级下降,真正的 deactivate 推迟到流处理器调用 chip->irq_eoi() 时写 ICC_DIR_EL1。

这个拆分对线程化很友好:CPU 侧的优先级早早恢复,而"这根线是否允许再来"由 deactivate 和 mask 单独控制。没有从 EL2 启动的系统(!is_hyp_mode_available())退回到 EOImode 0,gic_eoi_irq() 一次写 EOIR 同时完成两件事。

static inline void gic_complete_ack(u32 irqnr)
{
	if (static_branch_likely(&supports_deactivate_key))
		write_gicreg(irqnr, ICC_EOIR1_EL1);   /* EOImode1:先做 priority drop */

	isb();
}

static void __gic_handle_irq(u32 irqnr, struct pt_regs *regs)   /* :867 */
{
	if (gic_irqnr_is_special(irqnr))       /* 1020–1023:伪中断 */
		return;

	gic_complete_ack(irqnr);

	if (generic_handle_domain_irq(gic_data.domain, irqnr)) {
		WARN_ONCE(true, "Unexpected interrupt (irqnr %u)\n", irqnr);
		gic_deactivate_unhandled(irqnr);
	}
}

5.3Linux 通用中断层:从 INTID 到 irqaction

GIC 给出的是硬件号(hwirq / INTID),驱动注册的是 Linux 中断号(virq)。中间三层:

  • irq_domain:hwirq → virq 的映射。GICv3 的 gic_irq_domain_map()(irq-gic-v3.c:1596)在建立映射时就决定了这根中断用哪个流处理器(flow handler)。
  • struct irq_desc:每个 virq 一个,含 raw_spinlock_t lock、状态位、handle_irq 函数指针、action 链表、线程化相关的 threads_oneshot / threads_active。
  • struct irqaction:驱动每调用一次 request_irq() 产生一个,含 handler(硬中断里跑)、thread_fn(线程里跑)、thread(task_struct 指针)、flags。
INTID 范围类型flow handler能否线程化
0–15SGI(IPI)handle_percpu_devid_irq否
16–31PPI(arch timer、PMU、vGIC 维护中断)handle_percpu_devid_irq否
32–1019SPI(大多数外设)handle_fasteoi_irq是
≥ 8192LPI(MSI/MSI-X,经 ITS)handle_fasteoi_irq是

SGI/PPI 之所以不能线程化,原因在 irq_set_percpu_devid() 设置的标志:它会调用 irq_set_percpu_devid_flags(),其中包含 IRQ_NOTHREAD(include/linux/irq.h:801)。__setup_irq() 只在 irq_settings_can_thread(desc) 为真时才尝试强制线程化(kernel/irq/manage.c:1547)。这不是疏忽而是必然:per-CPU 中断必须在收到它的那颗 CPU上立即处理(时钟事件、重调度 IPI),交给一个可迁移、可延后的线程毫无意义,还可能死锁。

5.4强制线程化:request_irq() 被悄悄改写

RT 下 force_irqthreads() 是编译期常量 true:

#ifdef CONFIG_IRQ_FORCED_THREADING
# ifdef CONFIG_PREEMPT_RT
#  define force_irqthreads()	(true)
# else
DECLARE_STATIC_KEY_FALSE(force_irqthreads_key);
#  define force_irqthreads()	(static_branch_unlikely(&force_irqthreads_key))
# endif
#else
#define force_irqthreads()	(false)
#endif

非 RT 内核也能用启动参数 threadirqs 打开同一套机制(那条 static key),这正是它当年进入主线的方式——线程化中断(2.6.30 的 request_threaded_irq,以及之后的 forced threading)早于 RT 合入十几年就在主线里,6.12 只是让 RT 配置下它不可关闭。

改写发生在 irq_setup_forced_threading():

static int irq_setup_forced_threading(struct irqaction *new)
{
	if (!force_irqthreads())
		return 0;
	if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
		return 0;                          /* ① 三类豁免 */

	if (new->handler == irq_default_primary_handler)
		return 0;                          /* ② 本来就是纯线程化请求 */

	new->flags |= IRQF_ONESHOT;            /* ③ 线程跑完前屏蔽这根线 */

	if (new->handler && new->thread_fn) {  /* ④ 驱动自带 primary+thread */
		new->secondary = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
		...
		new->secondary->handler   = irq_forced_secondary_handler;
		new->secondary->thread_fn = new->thread_fn;   /* 原线程函数 → 第二个线程 */
		...
	}
	set_bit(IRQTF_FORCED_THREAD, &new->thread_flags);
	new->thread_fn = new->handler;          /* ⑤ 原硬中断 handler 挪进线程 */
	new->handler   = irq_default_primary_handler;  /* 硬中断里只剩它 */
	return 0;
}

而硬中断里剩下的那个 primary handler 只有一行:

static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
	return IRQ_WAKE_THREAD;
}
情形 A:request_irq(irq, foo_isr, 0, "foo", dev) 非 RT 的 irqaction handler = foo_isr thread_fn = NULL thread = NULL RT 改写 PREEMPT_RT 的 irqaction handler = irq_default_primary_handler thread_fn = foo_isr flags |= IRQF_ONESHOT · FORCED_THREAD irq/45-foo FIFO 50 情形 B:request_threaded_irq(irq, foo_hard, foo_thread, 0, ...) 非 RT handler = foo_hard thread_fn = foo_thread foo_hard 在硬中断里跑 handler = irq_default_primary_handler thread_fn = foo_hard secondary->thread_fn = foo_thread foo_hard 返回 IRQ_WAKE_THREAD 时唤醒它 irq/45-foo FIFO 50 irq/45-s-foo FIFO 50
强制线程化对 irqaction 的改写。情形 B 中驱动原来的 primary 也被挪进线程,原来的线程函数变成"secondary"线程(名字带 -s-,manage.c:1466)。两个线程都在 irq_thread() 开头调用 sched_set_fifo(),6.12 中优先级同为 FIFO 50。

哪些中断不会被线程化

条件典型例子为什么
IRQF_NO_THREAD级联中断控制器的父中断、某些 GPIO chained handlerhandler 只是分发到子中断,线程化会多一跳且子中断的线程化由子中断自己负责
IRQF_TIMER时钟事件设备IRQF_TIMER = __IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD(interrupt.h:90)。时钟中断负责唤醒线程,自己再等线程就成了自举死锁
IRQF_PERCPU / per-CPU devidarch timer PPI、IPI SGI、PMU必须在本 CPU 立即处理;见 5.3 节
IRQF_ONESHOT 已设置驱动已经用 request_threaded_irq(..., IRQF_ONESHOT)驱动自己的 primary 被视为"有意留在硬中断",原样保留——它必须是 RT 安全的(只能用 raw 锁)
嵌套线程中断I²C/SPI 扩展芯片的子中断本来就在父中断的线程里调用(irq_nested_primary_handler)
flowchart TD
  A["request_threaded_irq()"] --> B{"irq_settings_can_thread?"}
  B -- "否:percpu devid / IRQ_NOTHREAD" --> H["保持硬中断"]
  B -- 是 --> C{"flags 含 NO_THREAD / PERCPU / ONESHOT?"}
  C -- 是 --> H
  C -- 否 --> D{"handler 已是 default primary?"}
  D -- 是 --> T["已是纯线程化,不改"]
  D -- 否 --> E["flags 加 IRQF_ONESHOT"]
  E --> F{"驱动同时给了 thread_fn?"}
  F -- 是 --> G["创建 secondary action"]
  F -- 否 --> I["handler 挪到 thread_fn"]
  G --> I
  I --> J["setup_irq_thread: kthread irq/N-name"]
  T --> J
  classDef rt fill:#f6e3dc,stroke:#b5371c,color:#1d1d1f
  classDef hardirq fill:#fde8da,stroke:#c2410c,color:#1d1d1f
  class E,G,I,J rt
  class H hardirq
__setup_irq() 中的线程化决策(kernel/irq/manage.c:1534–1566)。

5.5ONESHOT:线程跑完之前,这根线必须保持屏蔽

把 handler 挪进线程后立刻出现一个问题:电平触发的中断。设备会一直拉着中断线,直到驱动读写设备寄存器把中断源清掉。可是清中断源的代码现在在线程里,线程还没被调度时硬中断返回、GIC deactivate,这根线立刻又进入 pending——CPU 刚出来又被拉回去,形成中断风暴,线程永远跑不上。

ONESHOT 的规则:在流处理器里先 mask,硬中断照常 EOI,线程处理完再 unmask。

void handle_fasteoi_irq(struct irq_desc *desc)
{
	struct irq_chip *chip = desc->irq_data.chip;

	raw_spin_lock(&desc->lock);
	...
	kstat_incr_irqs_this_cpu(desc);
	if (desc->istate & IRQS_ONESHOT)
		mask_irq(desc);          /* GICv3: 写 GICD_ICENABLER,并等待 RWP */

	handle_irq_event(desc);       /* 跑 primary → 返回 IRQ_WAKE_THREAD → 唤醒线程 */

	cond_unmask_eoi_irq(desc, chip);  /* 线程已被唤醒:只 EOI(deactivate),不 unmask */
	...
	raw_spin_unlock(&desc->lock);
}
static void cond_unmask_eoi_irq(struct irq_desc *desc, struct irq_chip *chip)
{
	if (!(desc->istate & IRQS_ONESHOT)) {
		chip->irq_eoi(&desc->irq_data);
		return;
	}
	/* 线程没被唤醒(伪中断或 primary 已处理完)→ 立刻 EOI + unmask */
	if (!irqd_irq_disabled(&desc->irq_data) &&
	    irqd_irq_masked(&desc->irq_data) && !desc->threads_oneshot) {
		chip->irq_eoi(&desc->irq_data);
		unmask_irq(desc);
	} else if (!(chip->flags & IRQCHIP_EOI_THREADED)) {
		chip->irq_eoi(&desc->irq_data);   /* GICv3 走这里:deactivate,保持 mask */
	}
}

线程侧,handler 返回后由 irq_finalize_oneshot()(manage.c:1134)清掉自己在 desc->threads_oneshot 里的那一位;共享这根线的所有线程都清完,才调用 unmask_threaded_irq() 重新使能。threads_oneshot 是位图,每个 irqaction 在 __setup_irq() 时分到一位 thread_mask(manage.c:1709)。

sequenceDiagram
  autonumber
  participant Dev as 设备
  participant GIC as GICv3
  participant HI as CPU 硬中断上下文
  participant T as irq/45-foo 线程
  Dev->>GIC: 拉高电平 (SPI 45)
  GIC->>HI: IRQ 异常
  HI->>GIC: 读 ICC_IAR1_EL1 = 45 (ack)
  HI->>GIC: 写 ICC_EOIR1_EL1 (priority drop)
  Note over HI: handle_fasteoi_irq 持 desc 锁
  HI->>GIC: mask_irq: GICD_ICENABLER 第 45 位
  HI->>HI: irq_default_primary_handler 返回 IRQ_WAKE_THREAD
  HI->>T: __irq_wake_thread: 置 RUNTHREAD, threads_oneshot 或上本位, wake_up_process
  HI->>GIC: irq_eoi: 写 ICC_DIR_EL1 (deactivate)
  Note over HI: irq_exit_rcu, 若线程优先级更高则在中断返回时切换
  T->>Dev: foo_isr: 读状态寄存器, 清中断源
  Dev-->>GIC: 电平撤销
  T->>T: irq_finalize_oneshot: 清 threads_oneshot 本位
  T->>GIC: unmask_threaded_irq: GICD_ISENABLER 第 45 位
一次 ONESHOT 线程化中断在 GICv3(EOImode 1)上的完整握手。注意 deactivate 在硬中断里就完成了;"不让它再来"靠的是 Distributor 里的 enable 位,而不是 active 状态。
代价:两次 MMIO

对 SPI,gic_mask_irq() 写 GICD_ICENABLER 后还要轮询 GICD_CTLR.RWP 等写生效(irq-gic-v3.c:486),unmask 再写一次 GICD_ISENABLER。Distributor 是 SoC 总线上的设备,这两次访问比系统寄存器慢得多。高频中断(例如每秒几十万次的网卡中断)在 RT 下的额外开销有相当一部分来自这里——这也是为什么高性能网卡用 MSI-X(LPI),并依赖 NAPI 关掉设备侧的中断,而不是靠 GIC 屏蔽。

5.6irq_thread():中断处理变成了一个普通线程

setup_irq_thread()(manage.c:1458)用 kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name) 创建线程。线程体:

static int irq_thread(void *data)
{
	struct irqaction *action = data;
	struct irq_desc *desc = irq_to_desc(action->irq);
	...
	irq_thread_set_ready(desc, action);

	sched_set_fifo(current);                 /* SCHED_FIFO, MAX_RT_PRIO/2 = 50 */

	if (force_irqthreads() && test_bit(IRQTF_FORCED_THREAD,
					   &action->thread_flags))
		handler_fn = irq_forced_thread_fn;   /* 被强制线程化的 */
	else
		handler_fn = irq_thread_fn;          /* 驱动主动请求的线程 */
	...
	while (!irq_wait_for_interrupt(desc, action)) {
		irqreturn_t action_ret;

		action_ret = handler_fn(desc, action);
		if (action_ret == IRQ_WAKE_THREAD)
			irq_wake_secondary(desc, action);

		wake_threads_waitq(desc);           /* 让 synchronize_irq() 的等待者醒来 */
	}
	...
}

几个细节决定了它在系统里的行为:

  • 优先级 50:sched_set_fifo() 写死 MAX_RT_PRIO / 2(kernel/sched/syscalls.c:856)。所有设备中断线程默认同一个优先级——RT 不知道哪个中断对你重要,这个排序是系统集成者的工作(chrt -f -p)。6.12 里 secondary 线程同样是 50(后续版本才给 secondary 单独降一级,以实际版本为准)。
  • 等待:irq_wait_for_interrupt()(manage.c:1102)是标准的 set_current_state(TASK_INTERRUPTIBLE) + 检查 IRQTF_RUNTHREAD + schedule() 循环。硬中断侧 test_and_set_bit(IRQTF_RUNTHREAD) 后 wake_up_process(),所以线程还没跑时又来的中断会被合并——这和硬件 pending 位的合并语义一致,但驱动必须"一次把所有待处理事件处理完"。
  • 亲和性跟随:每次等待前调用 irq_thread_check_affinity()(manage.c:1061),若中断亲和性变过(IRQTF_AFFINITY),用 set_cpus_allowed_ptr() 把线程迁到中断的有效亲和 CPU 上。GICv3 的 SPI 只路由到一个 CPU(gic_set_affinity() 写单个 GICD_IROUTER,irq-gic-v3.c:1477),所以线程通常就跟那颗 CPU 绑定,保住缓存局部性。

强制线程化的 handler 外面包了一层 BH 禁用

/*
 * Interrupts which are not explicitly requested as threaded
 * interrupts rely on the implicit bh/preempt disable of the hard irq
 * context. So we need to disable bh here to avoid deadlocks and other
 * side effects.
 */
static irqreturn_t
irq_forced_thread_fn(struct irq_desc *desc, struct irqaction *action)
{
	irqreturn_t ret;

	local_bh_disable();
	if (!IS_ENABLED(CONFIG_PREEMPT_RT))
		local_irq_disable();             /* threadirqs 模式下模拟原环境 */
	ret = action->thread_fn(action->irq, action->dev_id);
	if (ret == IRQ_HANDLED)
		atomic_inc(&desc->threads_handled);

	irq_finalize_oneshot(desc, action);
	if (!IS_ENABLED(CONFIG_PREEMPT_RT))
		local_irq_enable();
	local_bh_enable();                       /* ← 这里会把 handler 里 raise 的软中断跑掉 */
	return ret;
}

为什么要 local_bh_disable()?原本写在硬中断里的驱动代码,隐含假设"我执行时软中断不会插进来"(硬中断里软中断确实不会跑)。挪进线程后这个假设要显式补上,否则驱动与自己的 tasklet/NAPI poll 之间可能出现原来不存在的并发。

RT 下 local_bh_disable() 是一把 per-CPU 的睡眠锁,不关抢占;而 local_bh_enable() 发现有 pending 软中断时会在当前线程里就地执行它们。于是网卡中断 handler 里 napi_schedule() raise 的 NET_RX_SOFTIRQ,会在 irq/N-eth0 线程里、以 FIFO 50 的优先级跑掉,而不是交给 ksoftirqd。这是 RT 下"谁 raise 谁执行"的关键来源,细节见 第 6 章。

RT 下不关中断——原 handler 里的 spin_lock_irqsave() 在 RT 下本来就不关中断(第 3 章),线程里也不需要。

5.7调用栈对照:同一个网卡中断,两种内核

硬件 / 异常入口硬中断上下文软中断内核线程 / 任务用户态锁
非 RT:从 EL1 被打断到 handler 与软中断全部跑完 PREEMPT · ARM64 · GICv3
  1. VBAR_EL1 + 0x280 → el1h_64_irqarch/arm64/kernel/entry.S:526硬件保存 ELR/SPSR、置 DAIF;kernel_entry 压 pt_regs
  2. el1h_64_irq_handler()entry-common.c:643
  3. el1_interrupt() → __el1_irq()entry-common.c:632 / 619enter_from_kernel_mode;irq_enter_rcu: preempt_count += HARDIRQ_OFFSET
  4. do_interrupt_handler() → call_on_irq_stack()entry-common.c:304切到 per-CPU IRQ 栈
  5. gic_handle_irq() → __gic_handle_irq_from_irqson()irq-gic-v3.c:964 / 904读 ICC_IAR1_EL1
  6. __gic_handle_irq() → generic_handle_domain_irq()irq-gic-v3.c:867 · irqdesc.c:746
  7. handle_fasteoi_irq() → handle_irq_event()chip.c:687 · handle.c:202
  8. __handle_irq_event_percpu() → irq_int_handler()(mlx5 驱动)handle.c:139驱动 handler 直接在硬中断里跑,经 EQ notifier 最终 napi_schedule() raise NET_RX
  9. …… handler 返回,GIC deactivate ……
  10. irq_exit_rcu() → __irq_exit_rcu() → invoke_softirq()kernel/softirq.c:672 / 652 / 444
  11. do_softirq_own_stack() → handle_softirqs()arch/arm64/kernel/irq.c:84开中断执行,但仍不可抢占
  12. net_rx_action() → napi_poll() → … → ip_rcv() → tcp_v4_rcv()协议栈处理可能持续数百微秒,最长约 2 ms 后才交给 ksoftirqd
  13. …… 软中断结束 ……
  14. arm64_preempt_schedule_irq()entry-common.c:270这时被唤醒的 RT 任务才有机会上 CPU
PREEMPT_RT:硬中断侧只剩"屏蔽 + 唤醒" PREEMPT_RT · ARM64 · GICv3
  1. el1h_64_irq → el1h_64_irq_handler() → __el1_irq()entry.S:526 · entry-common.c:643 / 619
  2. gic_handle_irq() → __gic_handle_irq()irq-gic-v3.c:964 / 867IAR ack + EOIR priority drop
  3. handle_fasteoi_irq()chip.c:687
  4. mask_irq() → gic_eoimode1_mask_irq()irq-gic-v3.c:495IRQS_ONESHOT:写 GICD_ICENABLER
  5. handle_irq_event() → __handle_irq_event_percpu()handle.c:202 / 139
  6. irq_default_primary_handler()manage.c:1036return IRQ_WAKE_THREAD
  7. __irq_wake_thread() → wake_up_process(action->thread)handle.c:61try_to_wake_up:FIFO 50 的线程入 rt 运行队列;若它比 current 优先级高则 resched_curr
  8. cond_unmask_eoi_irq() → gic_eoimode1_eoi_irq()chip.c:657 · irq-gic-v3.c:735写 ICC_DIR_EL1 deactivate,保持 mask
  9. irq_exit_rcu() → invoke_softirq()kernel/softirq.c:295RT 版:最多唤醒 ksoftirqd,不就地执行
  10. arm64_preempt_schedule_irq() → preempt_schedule_irq()entry-common.c:270 · sched/core.c:7040need_resched 已置位 → __schedule(SM_PREEMPT),切到 irq 线程
PREEMPT_RT:线程侧 irq/45-mlx5_comp0 · SCHED_FIFO 50
  1. ret_from_fork → kthread() → irq_thread()manage.c:1300
  2. irq_wait_for_interrupt()manage.c:1102从 schedule() 返回,test_and_clear RUNTHREAD
  3. irq_forced_thread_fn()manage.c:1191
  4. local_bh_disable()RT:拿 per-CPU softirq_ctrl.lock(可睡眠,带 PI)
  5. irq_int_handler()(mlx5,原硬中断 handler)drivers/net/ethernet/mellanox/mlx5/core/pci_irq.c:227可被更高优先级任务抢占;内部 spin_lock 是 rtmutex
  6. irq_finalize_oneshot() → unmask_threaded_irq()manage.c:1134 · chip.c:443写 GICD_ISENABLER
  7. local_bh_enable() → __local_bh_enable_ip()发现 NET_RX pending
  8. handle_softirqs() → net_rx_action() → …协议栈在本线程、FIFO 50 下执行,可被抢占
  9. wake_threads_waitq()回到 irq_wait_for_interrupt 睡眠

对比两份栈:非 RT 版本里"红色"部分(驱动 handler + 协议栈)全在硬中断/软中断上下文、preempt_count != 0;RT 版本里它们整体下移到一个 FIFO 50 的线程,硬中断上下文只剩约十几个函数调用、两次 GIC MMIO 和一次唤醒。

场景:CPU2 上 FIFO 80 的控制任务在运行,此时网卡中断到达 非 RT FIFO 80 任务 驱动 handler NET_RX softirq(不可抢占) FIFO 80 任务继续 任务被挡住:handler + 软中断全部时长,由驱动和流量决定 RT FIFO 80 任务 FIFO 80 任务继续(80 > 50,不切换) irq/45 线程 + NET_RX 只剩 primary:屏蔽 + 唤醒 反过来,如果任务优先级低于 50,RT 下 irq 线程会立即抢占它——那是系统设计者通过优先级明确表达的意图,而不是驱动强加的。 两种情况下硬件那一段(取异常 → IAR → 唤醒 → ERET)都还在,这是 RT 无法消除的最小中断开销。
线程化把"中断打扰任务多久"从驱动的属性变成了调度策略的属性。

5.8仍留在硬中断里的东西

RT 内核里并非所有中断都线程化。在 ARM64 + GICv3 上,硬中断上下文里仍然会执行:

来源INTID 类型handlerRT 下做什么
arch timerPPI(通常虚拟定时器 27 / 物理非安全 30,由 DT/ACPI 给出)arch_timer_handler_virt() arm_arch_timer.c:681调 hrtimer_interrupt(),只跑 _HARD 的 hrtimer(含 RT 任务的 nanosleep 唤醒、tick),其余 raise HRTIMER_SOFTIRQ
IPISGIipi_handler() → do_handle_IPI() arch/arm64/kernel/smp.c:945重调度、跨核函数调用、irq_work、CPU stop
irq_work借用 IPI_IRQ_WORK 或 tickirq_work_run()只跑标了 IRQ_WORK_HARD_IRQ 的;其余交给 irq_work 线程
PMU、vGIC 维护中断PPI各自驱动per-CPU,不可线程化
pseudo-NMI优先级 0x80 的 SGI/PPIperf、hardlockup、backtrace在 NMI 上下文执行,连 local_irq_disable() 也挡不住
驱动显式 IRQF_NO_THREAD / IRQF_ONESHOT 的 primarySPI/LPI驱动必须只用 raw 锁、不分配内存——是 RT 审计的重点

这些路径的长度构成了 RT 内核"硬中断延迟"的下限,第 8 章从 ARM64 架构角度展开。

5.9解决后的变化:收益与代价

非 RT(默认)
  • 中断处理优先级 = 无穷大,无法配置
  • handler 不能睡眠、不能拿 mutex
  • 处理时间计入"被打断任务"的头上,top 里看不见
  • 一次中断:1 次异常进出,0 次上下文切换
  • 电平中断:handler 里清源,无需 mask/unmask
PREEMPT_RT
  • 每根中断一个 FIFO 线程,可 chrt、可 taskset
  • handler 里的 spinlock_t 变成可睡眠锁,带 PI
  • 处理时间计入 irq/N-name 线程,可观测、可统计
  • 一次中断:1 次异常进出 + 2 次上下文切换(进出线程)
  • ONESHOT:额外 2 次 GIC Distributor MMIO(SPI)

需要知道的副作用

  • 吞吐与平均延迟变差:多出的上下文切换在高中断率负载(小包网络、NVMe 小 IO)上可测量。RT 优化的是最坏延迟,不是平均延迟。
  • 共享中断线:同一根线上多个设备各自一个线程,只有全部线程完成才 unmask;一个慢驱动会拖住同线的所有设备。
  • 优先级扁平:默认全是 50,"鼠标中断"和"运动控制编码器中断"一样高。需要按业务重新排序(第 9 章、第 10 章)。
  • 用户态 RT 线程可以饿死中断线程:一个 FIFO 90 的死循环会让同 CPU 上所有 FIFO 50 的 irq 线程永远跑不上,如果这个用户线程恰好在等这些中断带来的数据,就是死锁。RT throttling(默认 95%)是最后的保险。
  • 驱动兼容性:在 IRQF_ONESHOT/IRQF_NO_THREAD 的 primary 里调用 spin_lock()(RT 下可睡眠)会触发 "sleeping function called from invalid context"。lockdep 的 lockdep_hardirq_threaded()(handle.c:155)专门标记"这个 handler 在 RT 下会被线程化",让非 RT 内核上的 lockdep 也能正确判断 wait-type。
  • synchronize_irq() 语义变强:它不仅等硬中断 handler 结束,还等 threads_active 归零,即线程处理也结束(__synchronize_irq(),manage.c:111),驱动卸载/挂起路径因此可能等得更久。

5.10本章小结

一句话

RT 用 forced threading + ONESHOT 把设备中断拆成两半:硬中断里只读 IAR、屏蔽这根线、唤醒线程、deactivate;真正的处理在 irq/N-name(FIFO 50)线程里、在 local_bh_disable() 保护下执行,并顺带跑掉自己 raise 的软中断。GICv3 的硬件握手一步没少,少的是"不可调度的那部分"。

  • 不能线程化的是 per-CPU 中断(SGI/PPI:IPI、arch timer)和显式豁免的中断——它们构成 RT 的硬中断延迟下限。
  • ONESHOT 解决电平中断风暴,代价是 GIC Distributor 的 mask/unmask MMIO。
  • 线程化后,"中断处理有多重要"成了一个可以由系统设计者通过优先级表达的决定。