硬中断执行结束在退出之前,都会在irq_exit中判断如果有pending的软中断就触发软中断:

内核代码通过调用raise_softirq_irqoff来触发相应的软中断,将相应的bit位置位,这样在irq_exit中判断就有pending的软中断,进而触发执行相应的软中断(直接执行软中断或触发软中断线程):

raise_softirq_irqoff函数调用__raise_softirq_irqoff设置了pending位后,调用in_interrupt判断当前上下文是不是处于中断中(软中断或硬中断),如果处于中断上下文就什么也不做(因为若是处于硬中断上下文,irq_exit就会执行软中断;如果处于软中断上下文,在软中断退出时也会执行到)。否则唤醒软中断线程执行,避免软中断执行有延迟。

在linux内核中通过 raise_softirq 触发一个软中断,但这个软中断什么时候执行呢?

调用 raise_softirq() 并不会立即执行软中断处理函数。它只是做了一个“标记”动作(在每 CPU 变量的位图上置位)。

软中断真正执行的时机,取决于当前上下文是否允许执行以及系统负载情况。主要有以下三个执行时机:

1. 硬中断返回时(最常见、延迟最低)

这是最典型的场景。当你是在硬中断处理函数(Top Half)中调用 raise_softirq() 时:

  • 流程
    1. 硬中断函数执行 raise_softirq(),标记 pending 位。
    2. 硬中断函数返回。
    3. 内核在退出中断栈之前(汇编代码 ret_from_intr 或 C 代码 irq_exit_rcu 中),会检查是否有 pending 的软中断。
    4. 如果有,且当前不在其他软中断上下文中,立即调用 do_softirq() 执行
  • 特点:延迟极低,几乎紧跟着硬中断执行。此时中断已开启,但处于中断上下文。

2. 显式启用底半部时(local_bh_enable()

如果你是在内核线程、进程上下文或某些禁止底半部的临界区之后调用 raise_softirq()(或者在临界区内标记,出来后检查):

  • 流程
    1. 代码执行 local_bh_enable()
    2. 该宏会检查:当前是否没有屏蔽底半部?是否有 pending 的软中断?
    3. 如果满足条件,立即调用 do_softirq() 执行
  • 典型场景:网络协议栈中,某些处理逻辑在禁用底半部后触发了新的软中断,一旦解除屏蔽就会立刻处理。

3. 由 ksoftirqd 内核线程异步执行(延迟稍高,防饿死)

如果出现以下两种情况之一,软中断不会在当前上下文立即执行,而是被推迟到专门的内核线程 ksoftirqd/x 中执行:

  • 情况 A:当前上下文不允许
    • 例如在持有自旋锁(Spinlock)且禁止调度的情况下触发了软中断,为了防止死锁或长时间占用 CPU,可能推迟执行。
    • 或者在某些特定的原子上下文中。
  • 情况 B:软中断负载过高(保护机制)
    • 如果在第 1 或第 2 种场景中,软中断执行时间过长,或者不断有新的软中断产生导致无法退出循环。
    • 内核检测到软中断处理耗时超过阈值(通常是 SOFTIRQ_TIME_LIMIT,约 2ms)或重试次数过多。
    • 动作:当前上下文停止执行软中断,唤醒对应的 ksoftirqd/x 线程,由该线程在进程上下文中继续处理剩余的软中断。
  • 特点:避免了单个 CPU 因处理软中断过久而导致调度器无法运行(Starvation),保证了系统的响应性。

🕵️‍♂️ 深度源码逻辑解析

让我们看看内核是如何决定“现在执行”还是“稍后执行”的。核心逻辑在 irq_exit_rcu() 和 __do_softirq() 中。

步骤一:标记 (Raise)
// include/linux/interrupt.h
static inline void raise_softirq(unsigned int nr) {
    // 只是设置当前 CPU 的 pending 位图
    __raise_softirq_irqoff(nr); 
}
步骤二:检查与决策 (Check & Dispatch)

当硬中断返回或调用 local_bh_enable() 时,会进入类似以下的逻辑(简化版):

// kernel/softirq.c (irq_exit_rcu 的一部分)
void irq_exit_rcu(void) {
    // ... 其他清理工作 ...

    // 关键判断:
    // 1. !in_interrupt(): 确保我们不是在嵌套的中断里(虽然软中断可重入,但这里指顶层退出)
    // 2. local_softirq_pending(): 检查有没有待处理的软中断
    if (!in_interrupt() && local_softirq_pending()) {
        do_softirq(); // 尝试立即执行
    }
}
步骤三:执行引擎 (do_softirq)
// kernel/softirq.c
void do_softirq(void) {
    __u32 pending = local_softirq_pending();

    if (pending) {
        // 如果当前已经是软中断上下文,或者某些特殊标志,可能直接跳去 __do_softirq
        // 否则,为了安全,可能会通过 stub 进入
        
        // 核心执行函数
        __do_softirq(); 
    }
}
步骤四:核心循环与降级 (__do_softirq)
// kernel/softirq.c
static void __do_softirq(void) {
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME; // 时间限制 (约 2ms)
    int max_restart = MAX_SOFTIRQ_RESTART;          // 重启次数限制
    struct softirq_action *h;
    __u32 pending;

    pending = local_softirq_pending();
    set_softirq_pending(0); // 【重要】先清零,防止死循环

    open_softirq(); // 开启中断 (此时 CPU 中断是开的)

    while (pending) {
        for (h = softirq_vec; h < softirq_vec + NR_SOFTIRQS; h++) {
            if (pending & (1 << h->index)) {
                h->action(h); // 执行具体的软中断函数
            }
        }

        // 【保护机制检查】
        // 如果时间到了,或者重启次数太多了,或者需要调度了
        if (time_after_eq(jiffies, end) || 
            --max_restart <= 0 || 
            need_resched()) {
            
            // 停止当前执行,唤醒 ksoftirqd 线程来处理剩下的
            wakeup_ksoftirqd(); 
            break; 
        }

        pending = local_softirq_pending(); // 检查是否有新产生的软中断
        if (pending) {
            set_softirq_pending(0); // 再次清零,准备下一轮
        }
    }
}

📊 总结:到底什么时候执行?

全屏复制

触发场景 执行时机 执行上下文 备注
硬中断中触发 硬中断函数返回后立即执行 中断上下文 (Interrupt Context) 最快,绝大多数情况。
底半部启用时触发 local_bh_enable() 调用时立即执行 进程上下文 / 内核线程 常见于协议栈内部逻辑。
负载过高/时间片耗尽 当前上下文放弃,稍后由 ksoftirqd 执行 内核线程 (ksoftirqd/x) 防止系统卡死,保证调度。
显式唤醒 用户手动 wake_up_process(ksoftirqd) 内核线程 (ksoftirqd/x) 极少见,通常由内核自动处理。

一句话总结
raise_softirq() 只是举手示意“我有活要干”,内核会优先在当前中断返回或解锁时立刻干;但如果活太多干不完,就会叫专门的后台线程 ksoftirqd 来接着干。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐