从硬中断到 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 名字远远不够。
相关推荐
生信碱移2 小时前
单细胞空转CNV分析工具:比 inferCNV 快10倍?!兼容单细胞与空转的 CNV 分析与聚类,竟然还支持肿瘤的亚克隆树构建!
算法·机器学习·数据挖掘·数据分析·聚类
lsp84ch802 小时前
MacBookPro运行飞牛Nas,解决合盖亮屏
linux·网络·macbook·nas·飞牛
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]mnt_idmapping
linux·笔记·学习
Brduino脑机接口技术答疑3 小时前
TDCA 算法在 SSVEP 场景中:Padding 的应用对象与工程实践指南
人工智能·python·算法·数据分析·脑机接口·eeg
optimistic_chen3 小时前
【Redis 系列】常用数据结构---Hash类型
linux·数据结构·redis·分布式·哈希算法
我就是你毛毛哥3 小时前
Linux 定时备份 MySQL 并推送 Gitee
linux·mysql
儒道易行3 小时前
平凡的2025年终总结
网络·安全·web安全
旖旎夜光3 小时前
Linux(7)(下)
linux·学习
keep_learning1113 小时前
Z-Image模型架构全解析
人工智能·算法·计算机视觉·大模型·多模态