硬中断线程化
设备中断的 handler 原本运行在一个"比任何任务都高、谁也打断不了"的上下文里。PREEMPT_RT 把它几乎整个搬进一个名叫 irq/N-name 的 SCHED_FIFO 内核线程,只在硬中断里留下"屏蔽这根线、叫醒线程"这两步。本章从 GICv3 硬件讲到 irq_thread(),说清这件事怎么做、为什么安全、代价是什么。
5.1问题:硬中断是调度器看不见的"超级优先级"
在非 RT 内核里,一个设备中断从 CPU 取异常那一刻起,到 handler 返回、irq_exit_rcu() 跑完软中断为止,整个过程都满足三个条件:
- 中断是关着的:ARM64 取 IRQ 异常时硬件自动置位
PSTATE.DAIF.I,entry 代码也明确写DAIF_PROCCTX_NOIRQ(arch/arm64/kernel/entry-common.c:635)。 - 不可抢占:
irq_enter_rcu()给preempt_count加上HARDIRQ_OFFSET,调度器在这期间不会切换任务。 - 不嵌套:Linux 给所有普通中断设成同一个 GIC 优先级
GICV3_PRIO_IRQ (0xc0),一个 handler 跑着时同一 CPU 上别的中断进不来(见 第 8 章)。
结果是:哪怕当前 CPU 上正在跑一个 SCHED_FIFO 99 的任务,任何一个设备中断都可以把它挂起,时长完全由驱动作者决定。调度器给任务设的优先级,在硬中断面前全部失效。网卡、存储、USB、GPU 驱动的 handler 动辄做几十微秒的事,再加上紧跟其后的 softirq,一个"低重要性"的中断就能让最高优先级的控制循环错过截止时间。
中断"何时到来"由硬件决定,这一点谁也改变不了;但中断"处理多久、在什么优先级处理"本应由系统设计者决定。非 RT 内核把这两件事绑在了一起。线程化要做的,就是把它们拆开:硬中断里只做"接住",处理交给调度器排队。
5.2硬件路径:GICv3 怎样把一个中断送到 CPU
线程化动不了硬件这一段,但要理解"硬中断里最少必须做什么",得先看清 GICv3 的状态机。GICv3 由四部分组成:
一个中断在 GIC 里有四种状态:inactive → pending → active → inactive(电平中断在处理期间仍有效时还会处于 active and pending)。三条指令推动这个状态机:
| 动作 | 寄存器 | 效果 | Linux 中的位置 |
|---|---|---|---|
| Acknowledge | ICC_IAR1_EL1(读) | 返回 INTID;该中断 pending→active;CPU 运行优先级升到该中断的优先级 | gic_read_iar() irq-gic-v3.c:909 |
| Priority drop | ICC_EOIR1_EL1(写) | 运行优先级回落,同优先级的其他中断可以再次送达 | gic_complete_ack() irq-gic-v3.c:846 |
| Deactivate | ICC_DIR_EL1(写) | active→inactive,这根线才能再次进入 pending | gic_eoimode1_eoi_irq() irq-gic-v3.c:735 |
内核从 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–15 | SGI(IPI) | handle_percpu_devid_irq | 否 |
| 16–31 | PPI(arch timer、PMU、vGIC 维护中断) | handle_percpu_devid_irq | 否 |
| 32–1019 | SPI(大多数外设) | handle_fasteoi_irq | 是 |
| ≥ 8192 | LPI(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;
}
-s-,manage.c:1466)。两个线程都在 irq_thread() 开头调用 sched_set_fifo(),6.12 中优先级同为 FIFO 50。哪些中断不会被线程化
| 条件 | 典型例子 | 为什么 |
|---|---|---|
IRQF_NO_THREAD | 级联中断控制器的父中断、某些 GPIO chained handler | handler 只是分发到子中断,线程化会多一跳且子中断的线程化由子中断自己负责 |
IRQF_TIMER | 时钟事件设备 | IRQF_TIMER = __IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD(interrupt.h:90)。时钟中断负责唤醒线程,自己再等线程就成了自举死锁 |
IRQF_PERCPU / per-CPU devid | arch 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 位
对 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调用栈对照:同一个网卡中断,两种内核
- VBAR_EL1 + 0x280 → el1h_64_irqarch/arm64/kernel/entry.S:526硬件保存 ELR/SPSR、置 DAIF;kernel_entry 压 pt_regs
- el1h_64_irq_handler()entry-common.c:643
- el1_interrupt() → __el1_irq()entry-common.c:632 / 619enter_from_kernel_mode;irq_enter_rcu: preempt_count += HARDIRQ_OFFSET
- do_interrupt_handler() → call_on_irq_stack()entry-common.c:304切到 per-CPU IRQ 栈
- gic_handle_irq() → __gic_handle_irq_from_irqson()irq-gic-v3.c:964 / 904读 ICC_IAR1_EL1
- __gic_handle_irq() → generic_handle_domain_irq()irq-gic-v3.c:867 · irqdesc.c:746
- handle_fasteoi_irq() → handle_irq_event()chip.c:687 · handle.c:202
- __handle_irq_event_percpu() → irq_int_handler()(mlx5 驱动)handle.c:139驱动 handler 直接在硬中断里跑,经 EQ notifier 最终 napi_schedule() raise NET_RX
- …… handler 返回,GIC deactivate ……
- irq_exit_rcu() → __irq_exit_rcu() → invoke_softirq()kernel/softirq.c:672 / 652 / 444
- do_softirq_own_stack() → handle_softirqs()arch/arm64/kernel/irq.c:84开中断执行,但仍不可抢占
- net_rx_action() → napi_poll() → … → ip_rcv() → tcp_v4_rcv()协议栈处理可能持续数百微秒,最长约 2 ms 后才交给 ksoftirqd
- …… 软中断结束 ……
- arm64_preempt_schedule_irq()entry-common.c:270这时被唤醒的 RT 任务才有机会上 CPU
- el1h_64_irq → el1h_64_irq_handler() → __el1_irq()entry.S:526 · entry-common.c:643 / 619
- gic_handle_irq() → __gic_handle_irq()irq-gic-v3.c:964 / 867IAR ack + EOIR priority drop
- handle_fasteoi_irq()chip.c:687
- mask_irq() → gic_eoimode1_mask_irq()irq-gic-v3.c:495IRQS_ONESHOT:写 GICD_ICENABLER
- handle_irq_event() → __handle_irq_event_percpu()handle.c:202 / 139
- irq_default_primary_handler()manage.c:1036return IRQ_WAKE_THREAD
- __irq_wake_thread() → wake_up_process(action->thread)handle.c:61try_to_wake_up:FIFO 50 的线程入 rt 运行队列;若它比 current 优先级高则 resched_curr
- cond_unmask_eoi_irq() → gic_eoimode1_eoi_irq()chip.c:657 · irq-gic-v3.c:735写 ICC_DIR_EL1 deactivate,保持 mask
- irq_exit_rcu() → invoke_softirq()kernel/softirq.c:295RT 版:最多唤醒 ksoftirqd,不就地执行
- arm64_preempt_schedule_irq() → preempt_schedule_irq()entry-common.c:270 · sched/core.c:7040need_resched 已置位 → __schedule(SM_PREEMPT),切到 irq 线程
- ret_from_fork → kthread() → irq_thread()manage.c:1300
- irq_wait_for_interrupt()manage.c:1102从 schedule() 返回,test_and_clear RUNTHREAD
- irq_forced_thread_fn()manage.c:1191
- local_bh_disable()RT:拿 per-CPU softirq_ctrl.lock(可睡眠,带 PI)
- irq_int_handler()(mlx5,原硬中断 handler)drivers/net/ethernet/mellanox/mlx5/core/pci_irq.c:227可被更高优先级任务抢占;内部 spin_lock 是 rtmutex
- irq_finalize_oneshot() → unmask_threaded_irq()manage.c:1134 · chip.c:443写 GICD_ISENABLER
- local_bh_enable() → __local_bh_enable_ip()发现 NET_RX pending
- handle_softirqs() → net_rx_action() → …协议栈在本线程、FIFO 50 下执行,可被抢占
- wake_threads_waitq()回到 irq_wait_for_interrupt 睡眠
对比两份栈:非 RT 版本里"红色"部分(驱动 handler + 协议栈)全在硬中断/软中断上下文、preempt_count != 0;RT 版本里它们整体下移到一个 FIFO 50 的线程,硬中断上下文只剩约十几个函数调用、两次 GIC MMIO 和一次唤醒。
5.8仍留在硬中断里的东西
RT 内核里并非所有中断都线程化。在 ARM64 + GICv3 上,硬中断上下文里仍然会执行:
| 来源 | INTID 类型 | handler | RT 下做什么 |
|---|---|---|---|
| arch timer | PPI(通常虚拟定时器 27 / 物理非安全 30,由 DT/ACPI 给出) | arch_timer_handler_virt() arm_arch_timer.c:681 | 调 hrtimer_interrupt(),只跑 _HARD 的 hrtimer(含 RT 任务的 nanosleep 唤醒、tick),其余 raise HRTIMER_SOFTIRQ |
| IPI | SGI | ipi_handler() → do_handle_IPI() arch/arm64/kernel/smp.c:945 | 重调度、跨核函数调用、irq_work、CPU stop |
| irq_work | 借用 IPI_IRQ_WORK 或 tick | irq_work_run() | 只跑标了 IRQ_WORK_HARD_IRQ 的;其余交给 irq_work 线程 |
| PMU、vGIC 维护中断 | PPI | 各自驱动 | per-CPU,不可线程化 |
| pseudo-NMI | 优先级 0x80 的 SGI/PPI | perf、hardlockup、backtrace | 在 NMI 上下文执行,连 local_irq_disable() 也挡不住 |
驱动显式 IRQF_NO_THREAD / IRQF_ONESHOT 的 primary | SPI/LPI | 驱动 | 必须只用 raw 锁、不分配内存——是 RT 审计的重点 |
这些路径的长度构成了 RT 内核"硬中断延迟"的下限,第 8 章从 ARM64 架构角度展开。
5.9解决后的变化:收益与代价
- 中断处理优先级 = 无穷大,无法配置
- handler 不能睡眠、不能拿 mutex
- 处理时间计入"被打断任务"的头上,
top里看不见 - 一次中断:1 次异常进出,0 次上下文切换
- 电平中断:handler 里清源,无需 mask/unmask
- 每根中断一个 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。
- 线程化后,"中断处理有多重要"成了一个可以由系统设计者通过优先级表达的决定。