03 · 中断线程化:驱动逻辑离开 hardirq
结论:RT 内核把
force_irqthreads()编译期固定为true,所有没有标记IRQF_NO_THREAD/IRQF_PERCPU/IRQF_ONESHOT的中断都被强制线程化:hardirq 里只剩一个「屏蔽本线 + 唤醒irq/N-name线程」的桩,驱动逻辑在 SCHED_FIFO 50 的内核线程里、在 bh 关闭状态下运行,可以被更高优先级任务抢占。代价是每个中断多一次线程唤醒与上下文切换(~2–5 µs),高中断率设备的吞吐下降;收益是 hardirq 停留时间从「驱动决定」变成「几 µs」,并且中断优先级变得可配置(chrt)。
第一层:从设备中断到任务唤醒
先看结论:hardirq 只剩几 µs 的唤醒桩,驱动逻辑在 SCHED_FIFO 50 的线程里跑,中断从此有了可配置的优先级。
第二层:内核路径逐步对照
先看结论:
force_irqthreads()编译期恒真;非 NO_THREAD/PERCPU/ONESHOT 的中断全部被强制 ONESHOT + 线程化,线程里只关 bh、不关中断。
强制线程化的开关
include/linux/interrupt.h:516-525:
#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
注意 PREEMPT_RT 并不 select IRQ_FORCED_THREADING(kernel/Kconfig.preempt:70-86 里没有),该符号由各架构自己选(arch/x86/Kconfig:297、arch/arm64/Kconfig:249、arch/riscv/Kconfig:187 等);RT 只是把运行时判断改成常量。非 RT 内核用 threadirqs 启动参数打开同样的机制(kernel/irq/manage.c:27-35,Documentation/admin-guide/kernel-parameters.txt:6725-6727),RT 上这个参数不存在。
谁被线程化、谁豁免
kernel/irq/manage.c:1368-1400 irq_setup_forced_threading():
- 豁免:
IRQF_NO_THREAD、IRQF_PERCPU、IRQF_ONESHOT(:1372)。IRQF_TIMER自带IRQF_NO_THREAD(include/linux/interrupt.h:90);NMI 也不线程化(:2341, 2696)。arm64 上:arch timer 的 PPI(drivers/clocksource/arm_arch_timer.c:1251-1269)与 IPI(arch/arm64/kernel/smp.c:1077-1084)都用request_percpu_irq()——flags 为 0,但它们是 percpu-devid 中断,天然不进强制线程化;内存映射 timer 用IRQF_TIMER(arm_arch_timer.c:1326)。因此 arm64 RT 上留在 hardirq 的只有 arch timer、IPI 和显式IRQF_NO_THREAD的设备。 - 已经用
request_threaded_irq()且 primary 为irq_default_primary_handler的驱动不需要再处理(:1375-1380)。 - 其余:
new->flags |= IRQF_ONESHOT(:1382)——线程跑完前保持本中断线屏蔽;如果驱动同时有 primary 和 thread_fn,primary 也被搬进一个 secondary 线程(:1384-1399)。
线程本身
- 创建:
kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name)(manage.c:1457-1468),继承中断亲和性(:1488)。 - 优先级:
irq_thread()第一件事是sched_set_fifo(current)(:1310)→MAX_RT_PRIO / 2 = 50(kernel/sched/syscalls.c:856-861,include/linux/sched/prio.h:16)。内核注释(syscalls.c:836-853)直言 SCHED_FIFO 无法做资源管理,所以内核自己创建的 FIFO 线程一律放在 50,必须由管理员按业务重新排定。 - 主循环(
:1321-1329):irq_wait_for_interrupt()等IRQTF_RUNTHREAD置位 →handler_fn()→wake_threads_waitq()(synchronize_irq()等的就是它)。 - 处理函数(
:1184-1207)irq_forced_thread_fn():
local_bh_disable();
if (!IS_ENABLED(CONFIG_PREEMPT_RT))
local_irq_disable(); /* 非 RT + threadirqs 时还要模拟关中断 */
ret = action->thread_fn(action->irq, action->dev_id);
...
irq_finalize_oneshot(desc, action); /* 解除屏蔽 */
if (!IS_ENABLED(CONFIG_PREEMPT_RT))
local_irq_enable();
local_bh_enable(); /* 期间 raise 的 softirq 在这里就地跑 */
注释(:1184-1189)解释了为什么要关 bh:没有显式申请线程化的驱动依赖 hardirq 隐含的 bh/preempt 关闭。RT 下只关 bh(那是一把 per-CPU 锁,第 04 章),中断保持打开。显式 request_threaded_irq() 的 thread_fn 走 irq_thread_fn()(:1209-1225),什么都不关,完全可抢占。
hardirq 侧
kernel/irq/handle.c:139-177:遍历 action,调用 primary,返回 IRQ_WAKE_THREAD 时 __irq_wake_thread()(:61-76)——test_and_set_bit(IRQTF_RUNTHREAD) 保证多次硬中断合并成一次线程运行。irq_exit() 在 RT 下不跑 softirq,只唤醒 ksoftirqd(kernel/softirq.c:295-299)。
网络驱动的联动
Documentation/networking/napi.rst:145-148:napi_schedule_irqoff() 在中断线程化(如 RT)时退化为 napi_schedule();实现见 net/core/dev.c:6339-6351,因为「hard IRQ 已关」的假设在 RT 上不成立。RT 还拒绝 irqfixup/irqpoll 启动参数(kernel/irq/spurious.c:450-468)。
ARM64:GIC、伪 NMI 与 PMR 屏蔽
- 线程化能力:
arch/arm64/Kconfig:249select IRQ_FORCED_THREADING;GIC SPI(外设中断)全部可线程化,PPI/SGI(arch timer、IPI)不可。IPI 在/proc/interrupts里被IRQ_HIDDEN隐藏(smp.c:1091)。 - 伪 NMI:
CONFIG_ARM64_PSEUDO_NMI(arch/arm64/Kconfig:2292-2305)+ 启动参数irqchip.gicv3_pseudo_nmi=1(定义在arch/arm64/kernel/cpufeature.c:1093-1097,不在 irqchip 驱动里)。GICv3 用中断优先级模拟 NMI:include/linux/irqchip/arm-gic-v3-prio.h:28-32定义GICV3_PRIO_UNMASKED 0xe0、GICV3_PRIO_IRQ 0xc0、GICV3_PRIO_NMI 0x80。只有IPI_CPU_STOP_NMI、IPI_CPU_BACKTRACE、IPI_KGDB_ROUNDUP三种 IPI 以及 PMU 中断会成为伪 NMI(smp.c:1016-1025)。 - 对 RT 的影响:开启后
local_irq_disable()/enable()从msr daifset/daifclr变成写ICC_PMR_EL1+pmr_sync()(arch/arm64/include/asm/irqflags.h:38-39, 67-68),若硬件不支持 relaxed 同步还要加屏障(drivers/irqchip/irq-gic-v3.c:2045-2046打印 "forced ICC_PMR_EL1 synchronisation")——每次关/开中断多几 ns 到几十 ns;伪 NMI 返回不做抢占(第 01 章);NMI 处理函数里只能用raw_spinlock_t。interrupts_enabled(regs)同时检查PSTATE.I与regs->pmr_save(arch/arm64/include/asm/ptrace.h:215-221)。 - 硬死锁检测:arm64 的 perf 硬死锁检测器要求伪 NMI(
arch/arm64/Kconfig:224-225, 232;arch/arm64/kernel/watchdog_hld.c:28-35:没有伪 NMI 时 PMU 中断只是普通 IRQ,检测器名存实亡);6.12.107 上游没有 SDEI 看门狗。采样周期按cpufreq_get_hw_max_freq()的周期数计算(watchdog_hld.c:16-25),受 DVFS 影响。 - PMU 中断:
drivers/perf/arm_pmu.c:636-637用IRQF_NOBALANCING | IRQF_NO_AUTOEN | IRQF_NO_THREAD注册,有伪 NMI 时改request_percpu_nmi()(:652)——perf 采样永远在 hardirq/NMI,采样率高时是自带的抖动源。 - PCIe MSI-X(ITS LPI):GICv3 ITS 的 LPI 支持亲和性(
drivers/irqchip/irq-gic-v3-its.c:1678, 1991its_set_affinity),多队列网卡/NVMe 的 managed 中断用isolcpus=managed_irq排除隔离核。 - 安全岛 IPC:mailbox 驱动多为普通
request_irq(如drivers/mailbox/tegra-hsp.c:711, 858,flags 0)或本就线程化(drivers/mailbox/omap-mailbox.c:233),RT 下都变成 irq 线程——Cortex-R 安全岛到 Linux 的通知延迟由该线程的优先级决定,要显式chrt。 - 优先级:
sched_set_fifo()与 x86 相同(50);Documentation/admin-guide/kernel-parameters.txt:2417-2420明确irqchip.gicv3_pseudo_nmi是[ARM64]专属。
性能影响
净影响:hardirq 停留时间从「驱动决定」变为常数;每个中断多一次线程切换,高中断率设备吞吐下降 5–40%。
| 指标 | 非 RT | RT | 说明 |
|---|---|---|---|
| hardirq 停留时间 | = 驱动 handler 时间(10 µs – 1 ms) | 几 µs(屏蔽 + 唤醒) | 对所有任务的最坏延迟贡献大幅缩小 |
| 中断→驱动逻辑开始 | 0 | 唤醒 + 切换:2–5 µs(若 irq 线程被更高优先级挡住则更久) | 中断响应时间略增 |
| 每中断 CPU 开销 | 基线 | +1 次线程切换 + bh 锁进出 | 100k+ IRQ/s 的网卡/NVMe 可见吞吐损失 |
| 中断优先级 | 硬件决定,无法调整 | 每个线程可 chrt,可低于 RT 应用 |
关键收益之一 |
| 中断合并 | 驱动/硬件 coalescing | 同上 + IRQTF_RUNTHREAD 天然合并;RT 默认不开软 IRQ 合并(net/core/dev.c:11202-11210) |
|
| 中断风暴影响面 | 全 CPU 所有任务 | 仅低于 50 的任务;RT 应用(>50)不受影响 |
源码定位
| 主题 | 位置 |
|---|---|
force_irqthreads() |
include/linux/interrupt.h:516-525 |
threadirqs 参数(非 RT) |
kernel/irq/manage.c:27-35 |
| 线程化决策 | kernel/irq/manage.c:1368-1400;kernel/irq/settings.h:115-118 |
| 标志 | include/linux/interrupt.h:77, 80, 83, 90 |
irq_forced_thread_fn / irq_thread_fn |
kernel/irq/manage.c:1184-1225 |
irq_finalize_oneshot |
kernel/irq/manage.c:1134-1177 |
irq_thread 主循环、优先级 |
kernel/irq/manage.c:1300-1329;kernel/sched/syscalls.c:856-861 |
| 线程创建与命名 | kernel/irq/manage.c:1457-1488 |
| hardirq 侧唤醒 | kernel/irq/handle.c:61-90, 139-177 |
| irqfixup/irqpoll 拒绝 | kernel/irq/spurious.c:450-468 |
| NAPI 联动 | net/core/dev.c:6339-6351;Documentation/networking/napi.rst:145-148 |
| arm64:arch timer / IPI 请求 | drivers/clocksource/arm_arch_timer.c:1251-1269, 1326;arch/arm64/kernel/smp.c:1016-1091;include/linux/interrupt.h:192-193 |
| arm64:伪 NMI | arch/arm64/Kconfig:2292-2315;arch/arm64/kernel/cpufeature.c:1091-1113, 2221-2232;drivers/irqchip/irq-gic-v3.c:515-519, 627-635, 919-922, 966-969, 2045-2053;include/linux/irqchip/arm-gic-v3-prio.h:28-48 |
| arm64:PMR 关中断 | arch/arm64/include/asm/irqflags.h:33-183;arch/arm64/include/asm/ptrace.h:26-29, 215-221 |
| arm64:硬死锁检测 | arch/arm64/kernel/watchdog_hld.c:15-35;kernel/watchdog_perf.c:229-241 |
| arm64:PMU / ITS / mailbox | drivers/perf/arm_pmu.c:636-656;drivers/irqchip/irq-gic-v3-its.c:1678, 1991;drivers/mailbox/tegra-hsp.c:711, 858;drivers/mailbox/omap-mailbox.c:233 |
调优要点
ps -eo pid,cls,rtprio,psr,comm | grep 'irq/'看清每个中断线程的优先级和 CPU;把 RT 应用真正依赖的设备(如 EtherCAT 网卡)提到 RT 应用之上(例如 85),其余保持 50 或降到更低。- 中断亲和性决定线程亲和性:
irqaffinity=启动参数 +/proc/irq/*/smp_affinity,把非关键设备的中断全部赶到 housekeeping CPU。 - 高吞吐网卡在 RT 上建议开启硬件中断合并(
ethtool -C),补偿线程化的每中断开销。 - 驱动作者:不要在 primary handler 里做任何会睡眠的事(RT 下
spin_lock_t会睡);确实需要 hardirq 语义的用IRQF_NO_THREAD+raw_spinlock_t,并接受它进入所有任务的最坏延迟。
小结
- 线程化的开关不在 Kconfig 而在
include/linux/interrupt.h:516-525的一个宏;threadirqs参数在 RT 上不存在。 - irq 线程默认 FIFO 50,内核注释明说「管理员必须自己配置」;关键设备提到 RT 应用之上,其余可以压低。
- 网络驱动的
napi_schedule_irqoff()在 RT 上退化为napi_schedule();irqfixup/irqpoll被拒绝。 - 承上启下:驱动逻辑进了线程,它 raise 的 softirq 也就跟着进了线程——下一章。