从硬中断到 softirq:Linux 软中断机制的全景解剖

从硬中断到 softirq:Linux 软中断机制的全景解剖


1. 概览摘要

在 Linux 里,外设一发中断,CPU 上来的第一反应是"硬中断处理程序"(hardirq),但真正大头的工作,往往被拆包转交给一个更"温和"的机制------软中断(softirq)。软中断是 Linux 内核里用来承接"与中断紧密相关、又不适合在硬中断里耗时太久"的那部分工作的一套框架,它是网络协议栈、块设备 IO、定时器等子系统的核心基础设施。

软中断介于"硬中断上下文"和"普通进程上下文"之间:它不在用户态执行,但也不要求像硬中断那样极短的时延。通过软中断、tasklet、工作队列等多级延后机制,Linux 既保证了中断响应延迟,又能把大量逻辑拆到更合适的上下文中执行,从而在高并发场景下保持良好的吞吐。

下面我们会从基本概念、关键数据结构、执行流程、设计取舍、实际示例、调试方法到整体架构,系统性地把 Linux 软中断机制"剖开"来看,尤其会结合网络收包这一典型场景,帮你真正理解内核为什么要这么设计,而不是停留在"知道有 softirq 这个词"的层面。


2. 核心概念详解

2.1 几个关键术语

先把几个常被混在一起的概念捋清:

名称 所在上下文 是否可抢占 典型用途
硬中断(hardirq) 中断上下文(最高优先级) 基本不可被普通代码抢占 及时响应外设事件
软中断(softirq) 内核上下文(软中断域) 不可被同 CPU 软中断抢占 网络、块 IO 底半部等
tasklet 构建在 softirq 之上 同一 tasklet 不并行 驱动自定义延后函数
工作队列(workqueue) 进程上下文(内核线程) 可睡眠 需要 sleep 的延后工作

可以用一个生活类比:

  • 硬中断 = 急诊室电话:必须立刻接,先做最关键的动作(比如"止血"),绝不能在电话上聊 30 分钟。
  • 软中断 = 急诊分诊台:把病人分类、安排后续处理,但还是不能慢吞吞。
  • 工作队列 = 各个专科门诊:可以慢慢排队、可以检查、等 IO。

2.2 "上下文"到底是什么

在内核里,"上下文"可以理解为"当前这段代码运行时能做什么事"的能力边界:

  • 中断上下文:
    • 没有当前进程的概念
    • 不能睡眠、不能阻塞
    • 调用栈有限、时间必须短
  • 软中断上下文:
    • 仍然不能睡眠
    • 但已经不在最顶级的中断里,可批量处理工作
  • 进程上下文:
    • 有当前 task_struct
    • 可以调度和睡眠

软中断就是在"响应时延"和"可做工作量"之间的一个折中点。

2.3 软中断 vs 早期 bottom half / tasklet

历史上 Linux 早期有"bottom half"(BH)概念,后来演化成 softirq + tasklet:

机制 特点 问题/局限
早期 BH 全局固定少数"底半部"入口 可扩展性差,易产生争用
softirq 静态注册,数量有限,按位图调度 编程模型偏底层,不能睡眠
tasklet 基于 softirq 的高层抽象,每个实例一个函数 不能 SMP 并行(同一 tasklet)
workqueue 使用内核线程执行,可以睡眠 延迟相对更大,切换开销更高

3. 实现机制深度剖析

下面按"数据结构 → 调度流程 → 典型场景"来拆。

3.1 核心数据结构(概念化简版)

注意:下面是"概念化的简化版本",非完整内核源码,避免版权问题同时突出关键字段。

c 复制代码
// 每种 softirq 的描述
struct softirq_action {
    void (*action)(struct softirq_action *); // 处理函数
    void *data;                              // 子系统私有数据(可选)
};

// 每 CPU 状态(简化版)
struct softirq_cpu_state {
    unsigned int pending_mask;  // 哪些 softirq 挂起(按位)
    int in_softirq;             // 嵌套计数,防止重入
};

常见软中断类型(内核里是枚举):

  • HI_SOFTIRQ:高优先级软中断
  • TIMER_SOFTIRQ:定时器
  • NET_TX_SOFTIRQ:网络发送
  • NET_RX_SOFTIRQ:网络接收
  • BLOCK_SOFTIRQ:块设备 IO
  • TASKLET_SOFTIRQ / HI_TASKLET_SOFTIRQ:tasklet

3.2 软中断注册与触发

3.2.1 注册 softirq

内核子系统在初始化时注册自己的处理函数(只在启动阶段做一次):

c 复制代码
// 简化版注册接口(示意)
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

比如网络子系统会把 NET_RX_SOFTIRQ 绑定到自己的收包函数上。

3.2.2 标记"有软中断待处理"

当硬中断里发现"后续要处理"的工作时,只是做两件事:

  1. 把少量关键数据搬出来(比如把包放进队列)
  2. 在当前 CPU 的 pending_mask 上置位
c 复制代码
// 在本 CPU 上触发某个 softirq(简化)
void raise_softirq(unsigned int nr)
{
    struct softirq_cpu_state *sc = this_cpu_ptr(&softirq_state);
    sc->pending_mask |= (1U << nr);
}

工作真正执行的时机,稍后再看。

3.3 软中断执行流程

整个链路"自上而下"画出来大概是这样:
发现 pending softirq
无 pending
外设产生中断
CPU 进入硬中断 handler
处理极少量关键工作
raise_softirq() 挂起软中断
退出硬中断
中断退出路径

或内核线程调度
__do_softirq() 执行
正常返回/调度

3.3.1 __do_softirq 核心逻辑(伪代码)

软中断执行的核心函数可以抽象成:

c 复制代码
void __do_softirq(void)
{
    struct softirq_cpu_state *sc = this_cpu_ptr(&softirq_state);

    if (sc->in_softirq)
        return; // 防止重入(简化)

    sc->in_softirq++;

    unsigned int pending = sc->pending_mask;
    sc->pending_mask = 0;  // 读取并清空

    while (pending) {
        int nr = __ffs(pending);        // 找到最低位的 softirq
        pending &= ~(1U << nr);         // 清掉这一位

        struct softirq_action *sa = &softirq_vec[nr];
        sa->action(sa);                 // 调用处理函数
    }

    sc->in_softirq--;
}

注意几个点:

  • 每 CPU 独立执行 softirq,不需要跨 CPU 的锁来保护 pending_mask
  • 一次执行时会把当前 pending 的全部 softirq 拉出来处理,避免频繁进出软中断上下文。
  • 软中断之间的优先级通过 bit 顺序可以粗略体现(高优先放低 bit)。
3.3.2 在哪里"进"软中断?

主要两个入口:

  1. 中断返回路径 :硬中断处理刚结束、准备回到内核/用户态时,检查是否有 pending softirq,有就直接调用 __do_softirq()
  2. ksoftirqd 线程 :每个 CPU 上有一个 ksoftirqd/N 内核线程,在需要时被唤醒,专门拉起软中断处理,避免硬中断退出一次批太多 softirq 导致长时间关抢占。

简化版条件:

c 复制代码
// 中断返回路径中的伪代码
if (local_softirq_pending())
    do_softirq();

// 如果软中断执行太久,则唤醒 ksoftirqd
if (time_spent_too_long)
    wake_up_process(ksoftirqd);

3.4 典型场景:网络收包

把网络收包这条链路画成流程图,会更直观:
应用进程 网络协议栈 NET_RX softirq CPU 硬中断 网卡 应用进程 网络协议栈 NET_RX softirq CPU 硬中断 网卡 触发中断(收到新包) 硬中断 handler 读取少量寄存器 DMA 缓冲区中摘取 skb 入队 raise_softirq(NET_RX) 退出硬中断,进入 softirq poll 收包队列,处理 L2/L3/L4 把数据放入 socket 缓冲 recv() 从内核拷贝到用户态

其中"真正复杂的事情"------路由查找、TCP 状态机、协议栈处理,基本都在 softirq 上下文完成。

3.5 tasklet:基于 softirq 的高层抽象

tasklet 是建立在 TASKLET_SOFTIRQ / HI_TASKLET_SOFTIRQ 之上的轻量框架:

c 复制代码
struct my_tasklet {
    struct tasklet_struct t;
    int value;
};

void my_tasklet_func(unsigned long data)
{
    struct my_tasklet *mt = (struct my_tasklet *)data;
    // 这里运行在 softirq 上下文,不能睡眠
}

DEFINE_TASKLET(my_tasklet_inst, my_tasklet_func, (unsigned long)&my_tasklet_inst);

调度时只是把该 tasklet 加入 per-CPU 队列,并触发对应软中断:

c 复制代码
void some_irq_handler(void)
{
    // ......处理中断头......
    tasklet_schedule(&my_tasklet_inst);  // 延后到 softirq 上下文执行
}

特点:

  • 同一个 tasklet 在同一时刻只会在一个 CPU 上运行(不并行)。
  • 但不同的 tasklet 可以在不同 CPU 上并行。

3.6 NAPI 与 softirq

在高吞吐网络场景下,NAPI(New API)通过"中断+轮询"结合降低中断风暴:

  1. 初始时,网卡通过硬中断通知有包。
  2. 硬中断中关闭进一步的 RX 中断,然后 napi_schedule()
  3. napi_schedule() 触发 NET_RX_SOFTIRQ
  4. 在 softirq 中轮询收包队列直到达到预算或无包。
  5. 若无包,重新打开中断。

简化代码示意:

c 复制代码
irqreturn_t nic_interrupt(int irq, void *dev_id)
{
    struct my_nic *nic = dev_id;

    disable_nic_rx_irq(nic);   // 关 RX 中断
    napi_schedule(&nic->napi); // 触发 NET_RX softirq
    return IRQ_HANDLED;
}

int my_napi_poll(struct napi_struct *napi, int budget)
{
    int work = 0;

    while (work < budget && nic_has_rx_packet()) {
        struct sk_buff *skb = nic_recv_skb();
        netif_receive_skb(skb);   // 交给协议栈
        work++;
    }

    if (work < budget) {
        napi_complete(napi);      // 本轮结束,打开中断
        enable_nic_rx_irq(nic);
    }
    return work;
}

NAPI 的轮询实际上就是在 NET_RX_SOFTIRQ 中被调度执行的。


3.7 复杂内容扩展:并发与可重入性

softirq 是每 CPU 执行的,因此:

  • 同一种 softirq 类型可以在不同 CPU 上并行执行(如多个 CPU 同时处理 NET_RX)。
  • 同一 CPU 上,软中断是不可抢占的:执行 __do_softirq() 过程中,这个 CPU 不会再进另一次 softirq。

这给并发设计带来几个要求:

  • 多 CPU 共享数据结构需要使用锁(自旋锁为主)或 per-CPU 拆分。
  • tasklet 额外保证"同一实例不在多个 CPU 并行",因此驱动可以少考虑重入。

4. 设计思想与技术权衡

4.1 为什么要 softirq?

硬中断上下文有两个硬约束:

  1. 中断关得越久,系统响应性越差;
  2. 硬中断里不能睡眠、不能调度,能做的事情类型受限。

如果所有逻辑都挤在硬中断里:

  • 网络高流量时,中断风暴会把 CPU 100% 占满,用户进程几乎得不到执行机会。
  • 某个驱动写得不好,硬中断里搞复杂算法/打印日志,就可能导致系统"抖一抖甚至死一死"。

softirq 的设计目标:

  • 把"必须立刻做"的和"稍微可以晚一点做"的部分拆开。
  • 让"可以晚一点"的那部分在仍然较高优先级、但可批处理的环境下运行。
  • 支撑 SMP:每 CPU 独立 pending 位图和队列,可以并行处理。

4.2 与工作队列的分层关系

可以简单理解为:

  • softirq:时间敏感、与中断强相关、不需要睡眠的工作。
  • workqueue:可以容忍更多延迟、可能需要睡眠的工作。

常见用法是:

  • 在硬中断中只做最小工作 + raise_softirq()tasklet_schedule()
  • 在 softirq/tasklet 中完成协议或驱动大部分"计算型"逻辑。
  • 如果需要访问用户空间、等待锁、访问慢速设备,再转交给工作队列。

4.3 一些典型权衡

方案 优点 缺点
全部硬中断完成 延迟最低,路径最短 对系统其他任务极不友好,难以维护
全部工作队列 易编程,可睡眠 延迟大,调度成本高,抖动大
硬中断 + softirq 响应及时 + 吞吐较好 + 可并行 编程模型较复杂,调试难度提升

Linux 内核选择了"硬中断 + softirq + workqueue"多级结构,本质上是"把系统当成一个需要兼顾实时性和吞吐的服务系统",用不同层次的队列来平衡延迟和处理能力。


5. 实践示例(最小可运行示例)

这里给出一个简化的内核模块示例:

  • 在加载时注册一个定时器,每隔一段时间在 softirq 上下文里打印一次信息。
  • 同时用工作队列在进程上下文里再做一次"慢动作"处理。

注意:代码是教学示例,省略了大量健壮性处理,也与实际内核有适当差异避免版权问题。

5.1 示例代码

c 复制代码
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>

static struct timer_list my_timer;
static struct work_struct my_work;

static void my_work_func(struct work_struct *work)
{
    // 进程上下文:可以睡眠(此处不睡眠,只打印)
    pr_info("mysoftirq: workqueue context on CPU %d\n", smp_processor_id());
}

static void my_timer_softirq(struct timer_list *t)
{
    // 软中断上下文:不能睡眠
    pr_info("mysoftirq: timer softirq on CPU %d\n", smp_processor_id());

    // 把后续工作交给工作队列
    schedule_work(&my_work);

    // 重新启动定时器
    mod_timer(&my_timer, jiffies + HZ);
}

static int __init mysoftirq_init(void)
{
    INIT_WORK(&my_work, my_work_func);

    timer_setup(&my_timer, my_timer_softirq, 0);
    mod_timer(&my_timer, jiffies + HZ);

    pr_info("mysoftirq: module loaded\n");
    return 0;
}

static void __exit mysoftirq_exit(void)
{
    del_timer_sync(&my_timer);
    cancel_work_sync(&my_work);
    pr_info("mysoftirq: module unloaded\n");
}

module_init(mysoftirq_init);
module_exit(mysoftirq_exit);
MODULE_LICENSE("GPL");

这个模块展示了:

  • 定时器回调是在 softirq 上下文执行;
  • 回调中再调度工作队列,在进程上下文执行;
  • 你可以用 cat /proc/softirqs 看到 TIMER 软中断计数在增加。

5.2 编译运行命令(示意)

假设放在目录 mysoftirq/

  • 内核树外构建 Makefile 示例:
makefile 复制代码
obj-m := mysoftirq.o
  • 构建与加载(在内核源码树外目录):
bash 复制代码
make -C /lib/modules/$(uname -r)/build M=$PWD modules
sudo insmod mysoftirq.ko
sudo rmmod mysoftirq
dmesg | tail

5.3 预期输出

dmesg 中可以看到类似日志(内容会因 CPU / 配置略有不同):

  • 模块加载信息
  • 每秒一次定时器 softirq 打印
  • 与之对应的 workqueue 打印

这有助于你感性认识"softirq 和工作队列的上下文差异"。


6. 调试与工具

调软中断相关问题时,常用观测点和工具如下:

工具/文件 用途 示例
/proc/interrupts 查看硬中断计数 cat /proc/interrupts
/proc/softirqs 查看软中断计数 cat /proc/softirqs
top/htop 查看 ksoftirqd/N 占用 htop 中按 CPU 查看 ksoftirqd 线程
perf 采样热点函数 perf top -g
ftrace/trace-cmd 跟踪 softirq/irq 事件 trace-cmd record -e irq -e softirq

示例:只看某 CPU 的 ksoftirqd 行为:

bash 复制代码
perf record -a -g -e cycles -- sleep 5
perf report  # 查看是否 __do_softirq 成为热点

也可以通过 echo 配置 ftrace 跟踪软中断:

bash 复制代码
echo function > /sys/kernel/debug/tracing/current_tracer
echo __do_softirq > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace_pipe

在高网络负载场景下,ksoftirqd CPU 100%、/proc/softirqsNET_RX 数疯狂增加,是典型的"软中断打满 CPU"症状。


7. 架构总览

从更高的角度看,Linux 的"中断相关子系统"可以概括成这样一个模块图:
硬件设备
硬中断处理程序
软中断子系统
tasklet 层
NAPI 轮询
块设备软中断
工作队列
网络协议栈
文件系统/页缓存
进程上下文

如果只聚焦某一个 CPU,可以这样理解"执行栈":
用户态进程
系统调用/内核线程
工作队列处理函数
软中断处理(__do_softirq)
硬中断 entry

从"上层往下"看,是普通进程触发 IO,进而产生中断;

从"下层往上"看,是硬件中断拉起硬中断、softirq、工作队列,最终把数据送到进程。


8. 全文总结

  1. Linux 把与中断相关的工作分三层:硬中断(响应)、软中断(批处理)、工作队列/进程上下文(复杂、可睡眠)。
  2. 软中断以"枚举 + 每 CPU pending 位图 + 函数向量表"的形式实现,使用 __do_softirq() 在合适时机批量处理挂起的 softirq。
  3. 网络、块设备、定时器等子系统大量依赖 softirq 来完成性能敏感部分的逻辑,是高吞吐场景的关键机制。
  4. tasklet 建立在 softirq 之上,给驱动提供了"不并行执行同一实例"的延后执行抽象;NAPI 则把中断和轮询结合,减少中断风暴。
  5. 软中断是"每 CPU 并行、单 CPU 内不可抢占"的,这对数据结构的并发设计有重要影响:要么 per-CPU,要么自旋锁。
  6. 调试软中断问题时,/proc/softirqsksoftirqd 行为、perf/ftrace 是最重要的观察入口。
  7. 从架构角度看,软中断是连接"实时性(硬件中断)"和"复杂功能(协议栈、文件系统)"的桥梁,是 Linux 内核高性能 IO 的核心基础设施之一。
  8. 真正理解软中断,需要结合具体子系统(尤其是网络和块 IO)去阅读对应代码和 trace 运行路径,单看 API 名字远远不够。
相关推荐
Dillon Dong2 小时前
【风电控制】TI TMS320F28379D 双CPU架构解析与任务分布设计
嵌入式硬件·算法·变流器·风电控制
为思念酝酿的痛8 小时前
POSIX信号量
linux·运维·服务器·后端
小羊在睡觉8 小时前
力扣84. 柱状图中最大的矩形
后端·算法·leetcode·golang·go
Dfreedom.8 小时前
Windows、虚拟机、开发板组网通信原理及调试通联步骤
人工智能·windows·部署·边缘计算·开发板·模型加速
3DVisionary8 小时前
蓝光三维扫描:医疗制造的精度焦虑怎么解
人工智能·算法·制造·蓝光三维扫描·医疗制造·三维检测·义齿检测
ylscode8 小时前
PureLogs 信息窃取恶意软件惊现高危变种:借道 MsBuild.exe 进程空心化实施无痕攻击
网络·安全·安全威胁分析
IPHWT 零软网络8 小时前
MX60E-A信创级智能语音网关技术实现与架构分析
网络·网络安全·国产自研·技术实现·智能语音网关·政企通信·信创技术
好评笔记8 小时前
机器学习面试八股——常用损失函数
人工智能·深度学习·算法·机器学习·校招
weixin_468466858 小时前
全局与局部注意力机制新手实战指南
人工智能·python·深度学习·算法·自然语言处理·transformer·注意力机制
_日拱一卒9 小时前
LeetCode:994腐烂的橘子
java·数据结构·算法·leetcode·深度优先