Linux 基础体系 · 第 30/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux 中断与 Softirq:IRQ、Bottom Half、ksoftirqd 和延迟
在 Linux 中,硬件事件通常不能直接由普通用户线程处理。网卡收到数据、块设备完成 DMA、定时器到期或 PCIe 设备报告状态时,硬件会通过中断通知处理器。内核随后需要在两件事之间取得平衡:
- 尽快响应硬件,避免设备丢失状态或队列溢出;
- 不要让中断处理长期占用 CPU,从而阻塞调度器和普通任务。
Linux 为此把处理路径拆成不同上下文。最上层是 IRQ 的快速处理,随后是 Bottom Half,也就是“下半部”机制;Softirq 是其中的一种实现,ksoftirqd 则是在 Softirq 不能及时完成时承担处理工作的内核线程。它们共同决定了网络、存储、定时器等高频事件对系统延迟的影响。
一、先建立几个边界:中断、异常、系统调用和调度
1. IRQ 是什么
IRQ 通常指 Interrupt Request,即中断请求。硬件设备通过中断控制器向 CPU 发送请求,CPU 保存当前执行状态,切换到内核预先注册的中断入口。
“IRQ”在不同语境中有两层含义:
- 硬件中断请求:设备向 CPU 或中断控制器提出请求;
- Linux 中的 IRQ 编号或 IRQ 描述对象:内核用来标识和管理某条中断线或某个 MSI/MSI-X 向量。
传统设备可能共享一条物理中断线,例如多个设备都连接到 PCI INTx。现代 PCIe 设备通常使用 MSI 或 MSI-X,把中断表示为写入特定内存地址的消息,并且可以拥有多个向量。多队列网卡通常为每个 RX/TX 队列分配 MSI-X 向量,从而把不同队列分布到不同 CPU。
IRQ 不等于所有进入内核的事件:
- 系统调用:用户程序主动请求内核服务,例如
read()、write(); - 异常:当前指令执行导致的同步事件,例如缺页异常、除零、非法指令;
- 硬件中断:与当前执行指令通常没有直接因果关系,例如网卡收到数据。
这三类入口最终都可能影响调度,但触发原因和执行上下文不同。中断处理尤其不能假设自己像普通进程一样拥有完整的进程上下文。
2. 中断上下文和进程上下文
Linux 内核代码大致运行在两种重要上下文中。
进程上下文具有当前进程,并且在允许睡眠的情况下可以阻塞等待。例如系统调用路径、内核工作线程和 workqueue 回调通常运行在进程上下文。
中断上下文由硬件中断处理或 Softirq 处理产生,通常没有可供当前逻辑依赖的用户进程上下文。它不能执行可能睡眠的操作,例如:
mutex_lock()在锁不可用时等待;schedule();- 可能阻塞的内存分配;
- 等待 I/O 完成;
- 访问可能睡眠的用户态拷贝路径。
在内核代码中,in_interrupt()、in_atomic() 等检查可以帮助诊断上下文问题,但它们不是设计接口的替代品。判断一个 API 是否能在中断或 Softirq 中使用,应查看该 API 的语义和注释,例如是否标记为 atomic-safe、是否可能 sleep。
一个典型错误是:
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
mutex_lock(&my_lock); /* 锁竞争时可能睡眠 */
...
mutex_unlock(&my_lock);
return IRQ_HANDLED;
}
这段代码在普通进程上下文中可能合法,但在硬件 IRQ handler 中,如果 mutex_lock() 需要等待,就会触发类似:
BUG: sleeping function called from invalid context
中断处理需要使用不会睡眠的同步方式,例如短临界区中的自旋锁;如果后续工作必须睡眠,则应交给 threaded IRQ 或 workqueue。
二、IRQ 的两阶段处理:Top Half 和 Bottom Half
1. Top Half:先确认事件,再快速退出
硬件 IRQ handler 通常被称为 Top Half。它的目标不是完成所有工作,而是完成必须立即完成的部分:
- 读取或确认设备状态;
- 判断是否确实由本设备产生中断;
- 保存必要的状态;
- 清除或屏蔽设备中断源,防止中断风暴;
- 安排后续处理;
- 尽快返回。
例如一个设备收到数据时,Top Half 可能只读取 DMA 完成队列的指针,确认设备状态,并安排 Softirq 或唤醒 IRQ 线程;真正解析大量数据、构造网络包或完成复杂协议处理通常不应全部放在硬件中断入口中。
如果 Top Half 执行时间为 ,中断到达频率为 ,那么仅从 CPU 占用角度看,平均占用比例近似为:
其中:
- :每秒中断次数;
- :每次硬中断处理时间;
- :该 CPU 用于 Top Half 的时间比例。
例如每秒 50,000 次中断,每次 Top Half 平均耗时 2 微秒:
也就是一个 CPU 大约 10% 的时间仅用于硬中断。若设备产生 500,000 次中断且处理时间不变,理论占用就达到 100%,还没有计算 Softirq、调度和普通任务。
这只是平均值。突发到达时,真正影响延迟的是队列长度、最长连续执行时间和中断是否被屏蔽,而不只是平均 CPU 占用。
2. Bottom Half:把延后的工作移出硬中断
Bottom Half 是一个历史上的总称,不是单一 API。它表示“中断后续工作”的机制。现代 Linux 中主要包括:
- Softirq;
- tasklet;
- threaded IRQ;
- workqueue;
- 某些子系统自己的轮询或延后执行机制。
这些机制的共同目标是:把不必在硬中断入口立即完成的工作延后处理,但它们的上下文和调度能力并不相同。
| 机制 | 执行上下文 | 能否睡眠 | 并行特性 | 典型用途 |
|---|---|---|---|---|
| 硬 IRQ handler | 中断上下文 | 否 | 受 IRQ/CPU 约束 | 确认设备、应答中断 |
| Softirq | 原子/中断相关上下文 | 否 | 同一 Softirq 可在多个 CPU 并行 | 网络、定时器、RCU 等 |
| tasklet | 建立在 Softirq 上 | 否 | 同一 tasklet 通常避免并发执行 | 旧驱动中的延后处理 |
| threaded IRQ | 内核线程上下文 | 是 | 由线程调度 | 需要较复杂或可睡眠的设备处理 |
| workqueue | 内核线程上下文 | 是 | 由 worker pool 调度 | 低频、复杂、可阻塞工作 |
“延后”不等于“一定由另一个线程执行”。Softirq 可能直接在硬中断返回路径上执行,因此它的延迟可能很低,但也会延长中断退出路径。
三、Softirq 的定义和执行路径
1. Softirq 是什么
Softirq 是 Linux 内核的一组静态定义的软中断类型。代码通过设置某个 CPU 上的 Softirq pending 位,表示“该 CPU 有一类延后工作待处理”。
常见 Softirq 类型包括:
NET_RX_SOFTIRQ:网络接收;NET_TX_SOFTIRQ:网络发送;TIMER_SOFTIRQ:传统定时器相关处理;HRTIMER_SOFTIRQ:高精度定时器相关处理;BLOCK_SOFTIRQ:块层相关处理,具体使用取决于内核版本和路径;RCU_SOFTIRQ:RCU 回调推进;TASKLET_SOFTIRQ:tasklet 使用的 Softirq 类型。
具体类型和实现属于内核内部接口,可能随版本变化。用户态程序不应依赖 Softirq 编号的固定数值。
Softirq 的一个重要性质是:同一种 Softirq 可以在多个 CPU 上同时运行。因此,Softirq 处理函数不能依赖“同一类型永远串行”的假设,必须正确使用每 CPU 数据、原子操作、锁或其他同步机制。
2. 典型路径:硬件中断设置 pending 位
以网络接收为例,抽象流程如下:
sequenceDiagram
participant Dev as 设备
participant CPU as 目标 CPU
participant IRQ as 硬 IRQ handler
participant S as Softirq 执行点
participant K as ksoftirqd/CPU
participant Net as 网络协议栈
participant App as 用户进程
Dev->>CPU: DMA 写入接收环
Dev->>CPU: MSI-X/IRQ
CPU->>IRQ: 进入硬件中断处理
IRQ->>IRQ: 确认状态、屏蔽/应答、安排后续处理
IRQ->>S: 设置 NET_RX pending
CPU->>S: 中断返回路径尝试处理 Softirq
S->>Net: NAPI poll/协议栈处理
alt 工作量未超出本次处理预算
Net-->>App: 唤醒 socket 等待者或排队数据
else 仍有 pending 工作
S->>K: 唤醒当前 CPU 的 ksoftirqd
K->>Net: 在线程上下文继续处理 Softirq
Net-->>App: 唤醒 socket 等待者或排队数据
end
实际网络路径通常还包含 NAPI。NAPI 的思想是:在低负载时可以通过中断快速响应;当中断过于频繁时,驱动在确认一次中断后暂时关闭或抑制接收中断,改为由网络 Softirq 批量轮询接收队列。这样可以减少“每个包一次硬中断”的开销。
3. Softirq 何时运行
Softirq 可能在以下位置运行:
-
硬中断返回路径
IRQ handler 设置了 Softirq pending,硬中断退出前,内核检查并执行 Softirq。 -
进程上下文主动触发的路径
例如内核在启用 bottom half 或完成某些系统调用路径时检查 pending Softirq。 -
ksoftirqd/N内核线程
如果直接处理 Softirq 不合适,内核唤醒对应 CPU 的ksoftirqd/N,由它继续处理。
不同架构和内核配置的具体调用链会变化,但“设置 pending—在安全点处理—必要时转交 ksoftirqd”的模型是理解问题的关键。
四、为什么需要 ksoftirqd
1. 直接处理 Softirq 的两种风险
如果每次硬中断返回时都无限处理 Softirq,设备持续产生事件时,CPU 可能长时间停留在中断相关路径中:
硬 IRQ
-> Softirq
-> 新的 Softirq 又产生
-> 继续 Softirq
-> 普通任务迟迟得不到运行机会
这会造成调度延迟,甚至让用户进程表现为“系统没有死机,但几乎不响应”。
反过来,如果 Softirq 一律立即交给线程,低负载下又会引入不必要的线程唤醒和调度开销。Linux 的常见实现采用折中策略:
- 先在当前返回路径尽快处理一部分;
- 达到重启次数或时间限制后停止继续处理;
- 如果仍有 pending Softirq,唤醒每 CPU 的
ksoftirqd/N。
这里的具体限制是实现细节,内核版本、配置和架构可能不同,不能把某个版本的常数当作稳定 ABI。稳定的语义是:内核不会让一次 Softirq 处理无限制地占据当前路径,而会在剩余工作较多时转交线程继续处理。
2. ksoftirqd 是什么
ksoftirqd/N 是与 CPU 关联的内核线程,N 表示 CPU 编号。例如:
ksoftirqd/0
ksoftirqd/1
它不是每一种 Softirq 一个线程,而通常是每个逻辑 CPU 一个线程。它的职责是处理该 CPU 上积压、没有在立即路径中完成的 Softirq。
查看这些线程:
ps -eLo pid,tid,psr,cls,rtprio,pri,stat,comm | grep '\[ksoftirqd/'
可能看到类似:
42 42 0 TS - 20 S [ksoftirqd/0]
47 47 1 TS - 20 S [ksoftirqd/1]
字段含义受 ps 版本影响,但通常可以看到:
psr:当前或最近运行的 CPU;cls:调度类;stat:线程状态;comm:线程名。
普通内核配置下,ksoftirqd 通常是普通调度类中的内核线程,而不是实时线程。它会受到 CPU 亲和性、CPU 隔离、调度策略和系统负载影响。不要简单地认为“名字中有 kernel,所以它一定优先于用户线程”。
3. ksoftirqd CPU 占用高意味着什么
ksoftirqd/N 长时间占用 CPU,通常说明该 CPU 上的 Softirq 工作量持续超过立即处理路径能够消化的速度。常见原因包括:
- 网络包速率过高;
- 单包处理成本高;
- 网卡中断和队列集中在一个 CPU;
- RPS/RFS 把处理流量集中到某些 CPU;
- 块设备完成事件或定时器事件突发;
- 某个驱动反复产生中断;
- CPU 被限频、虚拟化调度或其他任务抢占;
- Softirq 处理本身存在异常重试或大量回调。
但单次看到 ksoftirqd 有 CPU 占用并不代表故障。低延迟、短时间的占用可能只是正常的突发工作。应同时观察增长率、Softirq 类型、丢包、队列积压和业务延迟。
五、延迟到底是哪一种延迟
“中断延迟”经常被混用。至少应区分以下时间段:
-
硬件到 CPU 的入口延迟
设备发出 IRQ 后,CPU 真正进入入口代码之前等待了多久。 -
IRQ handler 延迟
从进入 Top Half 到返回需要多久。 -
Softirq 调度延迟
Softirq pending 被设置后,到 Softirq 开始执行之间等待多久。 -
Softirq 服务延迟
Softirq 从开始处理到完成特定事件需要多久。 -
调度延迟
数据已进入可交付状态,但目标用户线程何时获得 CPU。 -
应用可见延迟
从硬件事件发生到应用完成read()、收到事件或处理数据的完整时间。
可以写成一个简化模型:
其中:
- :进入中断入口的等待;
- :Top Half 执行时间;
- :Softirq 开始前的等待;
- :相关 Softirq 工作时间;
- :目标任务等待调度的时间;
- :应用实际处理时间。
这个分解解释了两个看似矛盾的现象:
- IRQ handler 很短,但应用仍然延迟很高:可能积压在 Softirq 或调度阶段;
- Softirq 总 CPU 占用不高,但尾延迟很差:可能是偶发的长临界区、CPU 亲和性冲突、锁竞争或调度抖动。
1. 一个队列算例
假设一个 CPU 上有任务到达,每个任务由 Softirq 处理,平均服务时间为 ,到达率为 。
CPU 利用率近似为:
若:
- 个事件/秒;
- 微秒;
则:
平均上只需要 40% CPU,系统可能运行良好。
如果突发期间到达率变为 300,000 个事件/秒:
服务能力不足,队列必然增长。即使突发结束后平均速率又降下来,系统也要先清空 backlog,因此应用会看到一段额外延迟。
如果通过把事件分布到两个 CPU,使每个 CPU 获得约一半事件,则每个 CPU 的平均利用率降为约 60%。这就是 IRQ affinity、RSS、RPS 等机制可能改善延迟的原因。但如果两个 CPU 共享同一把锁或频繁访问同一 NUMA 节点上的数据,理论上的并行度会被同步和内存访问成本抵消。
六、网络接收为什么经常与 Softirq 绑定
1. 中断逐包处理的成本
最简单的网络处理方式是每收到一个包就产生一次硬中断:
包 1 -> IRQ -> 处理
包 2 -> IRQ -> 处理
包 3 -> IRQ -> 处理
高包速率下,CPU 会花费大量时间进入和退出中断,上下文切换和缓存扰动也会增加。
NAPI 将路径改为:
第一次包到达 -> IRQ -> 关闭/抑制接收中断 -> 设置 NET_RX
NET_RX Softirq -> 批量 poll 多个包
队列为空 -> 重新允许接收中断
NAPI poll 通常有处理预算。预算的存在是为了限制一次批量处理对其他工作造成的影响。预算耗尽但队列仍非空时,后续处理会继续进行,而不是无限处理到队列为空。
因此,看到网络 Softirq 高并不一定意味着 Top Half 慢;可能是:
- 每次 IRQ 很短,但 NAPI 每轮处理很多包;
- 包已经进入内核,但协议栈、iptables、桥接、隧道或 socket 分发很重;
- CPU 上 Softirq 被限制在预算内反复执行;
- IRQ 分布不均,某个 CPU 过载而其他 CPU 空闲。
2. 中断合并的取舍
网卡常提供 interrupt coalescing:累积若干包或等待一个很短时间后再发中断。
它减少了中断次数和每包固定开销,但会增加等待时间。设:
- 中断合并等待时间为 ;
- 批量处理节省的固定开销为 。
如果 带来的业务损失,吞吐量通常受益;如果应用要求极低延迟, 可能成为主要延迟来源。适合吞吐的配置不一定适合交易、工业控制或实时音视频。
修改网卡参数时应先查看当前配置:
sudo ethtool -c eth0
可能输出:
Coalesce parameters for eth0:
...
rx-usecs: 0
rx-frames: 64
字段和是否支持取决于驱动。尝试修改前应记录原值:
sudo ethtool -c eth0 > /tmp/eth0-coalesce.before
例如:
sudo ethtool -C eth0 rx-usecs 0 rx-frames 32
这不是通用必然有效的优化:
- 设备可能不支持该参数;
- 参数可能在网卡重置或重启后恢复;
- 更低延迟可能换来更高 IRQ/Softirq CPU 占用;
- 生产环境必须用业务延迟、丢包和 CPU 数据验证,而不是只看吞吐。
七、IRQ affinity、RSS、RPS 和 NUMA
1. IRQ affinity
IRQ affinity 决定某个 IRQ 可以在哪些 CPU 上处理。查看某个 IRQ:
irq=123
cat /proc/irq/$irq/smp_affinity_list
cat /proc/irq/$irq/effective_affinity_list 2>/dev/null || true
也可以查看所有中断的分布:
cat /proc/interrupts
输出示意:
CPU0 CPU1
123: 1200 845000 PCI-MSI eth0-TxRx-0
124: 1100 832000 PCI-MSI eth0-TxRx-1
/proc/interrupts 中的数值是自启动以来的累计计数,不是每秒速率。用以下方式观察增长:
watch -n 1 'grep -E "eth0|mlx|nvme" /proc/interrupts'
如果某一个队列对应的 IRQ 只在 CPU0 增长,而业务流量很大,可能存在分布不均。但手工移动 IRQ 前必须确认:
- 网卡队列与 CPU 的映射;
- IRQ 是否由
irqbalance管理; - CPU 是否属于同一 NUMA 节点;
- 应用线程和内存是否也在同一节点;
- 是否使用 CPU isolation 或实时任务。
临时修改 affinity 的示例:
echo 2 | sudo tee /proc/irq/123/smp_affinity
这里的 2 是十六进制 CPU mask,表示允许 CPU1 处理。不同系统暴露的 mask 文件可能还包括 smp_affinity_list;写入错误可能导致性能下降,甚至让设备中断集中到不合适的 CPU。生产修改前应保存原值并准备恢复。
2. RSS 与 RPS 不是一回事
- RSS(Receive Side Scaling):通常由网卡硬件根据流哈希把接收流分配到多个硬件队列,再通过 MSI-X 分配到 CPU。
- RPS(Receive Packet Steering):包进入内核后,由软件决定把处理工作安排到其他 CPU。
RSS 主要影响硬件队列和 IRQ 分布;RPS 可能减少单个 IRQ CPU 的压力,但会增加跨 CPU 唤醒、缓存失效和数据移动成本。
查看 RPS 配置:
for f in /sys/class/net/eth0/queues/rx-*/rps_cpus; do
printf '%s: ' "$f"
cat "$f"
done
RPS 不是“CPU 越多越好”。对于同一连接,如果处理频繁跨 CPU,缓存局部性可能变差;如果目标 CPU 位于另一个 NUMA 节点,内存访问成本也可能增加。
八、Softirq 的执行约束和同步问题
1. Softirq 不能睡眠
Softirq 虽然比硬 IRQ 晚执行,但仍然不等于普通线程。Softirq handler 中通常不能:
mutex_lock(...);
msleep(...);
wait_event(...);
copy_to_user(...);
如果工作需要:
- 分配可能阻塞的内存;
- 等待另一个设备;
- 获取 mutex;
- 执行较长的协议或文件系统操作;
就应考虑 workqueue、threaded IRQ 或专用内核线程。
2. Softirq 可以并行执行
假设一个 Softirq handler 使用全局链表:
static struct list_head pending;
void my_softirq_action(struct softirq_action *a)
{
while (!list_empty(&pending)) {
...
}
}
如果该 Softirq 可在多个 CPU 上同时运行,这段代码就存在并发访问风险。应根据上下文选择合适的同步:
- 只在 Softirq/硬 IRQ 间共享:可考虑
spin_lock_bh()、spin_lock_irqsave()等; - 进程上下文也会访问:需要让进程路径禁用 bottom half,或使用正确的锁组合;
- 适合生产者/消费者模式:可以使用每 CPU 队列,减少全局锁;
- 数据读多写少:可能使用 RCU,但回收和生命周期必须完整设计。
锁后缀本身表达了上下文关系:
spin_lock_irqsave():保存并关闭本地硬中断,适合保护可能被本地 IRQ 访问的数据;spin_lock_bh():禁止本地 Softirq,适合进程上下文与 Softirq 共享数据;- 普通
spin_lock():只解决互斥,不自动阻止本地中断或 Softirq 重入。
具体使用方式取决于数据在哪些上下文访问。错误地扩大 IRQ 关闭范围会降低系统响应能力;锁范围过大则会增加 Softirq 和 IRQ 延迟。
九、tasklet、threaded IRQ 和 workqueue 的边界
1. tasklet
tasklet 是建立在 Softirq 之上的历史机制。它提供了比直接注册 Softirq 更方便的接口,并且同一个 tasklet 通常不会在多个 CPU 上同时执行。
但 tasklet 仍然:
- 不能睡眠;
- 可能延长 Softirq 处理;
- 不适合需要严格调度和优先级控制的复杂工作。
在现代内核开发中,新代码通常应优先考虑更明确的机制,尤其是 threaded IRQ 或 workqueue。tasklet 在许多现有驱动中仍能看到,但“代码能编译”不等于它适合新的设计。
2. threaded IRQ
threaded IRQ 把中断处理分为快速 handler 和线程 handler。典型接口是:
request_threaded_irq(irq,
my_irq_top,
my_irq_thread,
flags,
"my-device",
dev);
概念上:
irqreturn_t my_irq_top(int irq, void *data)
{
if (!device_asserted_irq(data))
return IRQ_NONE;
mask_device_irq(data);
return IRQ_WAKE_THREAD;
}
static irqreturn_t my_irq_thread(int irq, void *data)
{
struct my_device *dev = data;
mutex_lock(&dev->lock);
drain_device_events(dev);
mutex_unlock(&dev->lock);
unmask_device_irq(dev);
return IRQ_HANDLED;
}
关键路径是:
- Top handler 判断 IRQ 是否属于本设备;
- 屏蔽或确认设备状态;
- 返回
IRQ_WAKE_THREAD; - 内核唤醒对应的 IRQ 线程;
- 线程 handler 在进程上下文中处理复杂逻辑;
- 完成后重新允许设备中断。
IRQ 线程可以睡眠,因此适合需要 mutex 或较复杂设备访问的场景。但它不是免费优化:
- 线程唤醒和调度会增加路径延迟;
- 如果 IRQ 线程优先级或 affinity 配置不当,设备事件仍会积压;
- 不应在 Top handler 中做大量工作后再“形式上”使用线程。
3. workqueue
workqueue 把工作排入内核 worker 线程。示例:
static void my_work_fn(struct work_struct *work)
{
struct my_device *dev =
container_of(work, struct my_device, work);
mutex_lock(&dev->lock);
process_deferred_events(dev);
mutex_unlock(&dev->lock);
}
Top Half 或 Softirq 中可以:
schedule_work(&dev->work);
workqueue 适用于可睡眠、非实时、可以由 worker 调度的任务。若工作必须在特定 CPU 执行或需要隔离,可以使用专用 workqueue,但这会增加线程和管理成本。
十、Softirq 的状态和数据流
可以把一次事件抽象为以下状态转换:
stateDiagram-v2
[*] --> DevicePending: 设备产生事件
DevicePending --> IRQRunning: CPU 进入 IRQ handler
IRQRunning --> NoDeferredWork: 仅需应答/确认
IRQRunning --> SoftirqPending: 设置 Softirq pending
SoftirqPending --> SoftirqRunning: 返回路径或其他安全点开始处理
SoftirqRunning --> Completed: 队列在本轮处理完
SoftirqRunning --> KsoftirqdRunnable: 达到本轮限制仍有工作
KsoftirqdRunnable --> KsoftirqdRunning: ksoftirqd 获得 CPU
KsoftirqdRunning --> Completed: backlog 被清空
KsoftirqdRunning --> KsoftirqdRunnable: 仍有新工作
NoDeferredWork --> [*]
Completed --> [*]
需要注意三个容易误解的地方:
SoftirqPending不是一个用户态可观察的稳定状态对象,而是内核内部 pending 位和队列状态的抽象;ksoftirqd被唤醒不代表它立即运行,中间仍可能等待调度;- Softirq 处理过程中可能再次产生 Softirq,因此“开始执行一次”不等于“所有工作已经完成”。
十一、如何观察 IRQ、Softirq 和 ksoftirqd
1. 查看 IRQ 计数
cat /proc/interrupts
重点看:
- 各 CPU 的计数是否均衡;
- 某个设备 IRQ 是否异常快速增长;
- 是否有单个 CPU 承担几乎全部网卡或 NVMe 队列;
- 是否出现设备中断计数停止增长。
/proc/interrupts 是累计值。判断速率需要连续采样:
for i in 1 2 3 4 5; do
date +%T
grep -E 'eth0|nvme|mlx|virtio' /proc/interrupts
sleep 1
done
2. 查看 Softirq 类型
cat /proc/softirqs
输出示意:
CPU0 CPU1
HI: 0 0
TIMER: 12345 23456
NET_TX: 800 1200
NET_RX: 98765 345678
BLOCK: 100 240
IRQ_POLL: 0 0
TASKLET: 120 300
SCHED: 4567 6789
HRTIMER: 3 4
RCU: 7890 11234
这同样是累计计数。字段名称和 Softirq 类型会因内核版本而变化。若 NET_RX 增长很快,应结合网卡队列、NAPI、协议栈和应用接收情况分析;不能仅凭某个数字判断性能问题。
3. 查看网络 backlog
cat /proc/net/softnet_stat
该文件每行通常对应一个 CPU,每行是十六进制字段。字段布局不是适合普通脚本长期依赖的稳定用户态 ABI,不同内核版本可能增加或调整字段。常见实现中,字段包含处理次数、因 backlog 限制而丢弃的次数、处理时间预算耗尽次数等信息。
安全做法是:
- 记录同一内核版本的字段解释;
- 比较一段时间内的增量;
- 与
ethtool -S eth0、应用丢包和业务延迟对照; - 不把某一列在所有版本中硬编码成同一个含义。
4. 查看 ksoftirqd 的实际 CPU
pidstat -t -p ALL 1
或:
top -H
观察:
ksoftirqd/N是否持续占用 CPU;- 是否只集中在少数 CPU;
- 是否与 IRQ 计数和
NET_RX增长同步; - 是否存在高优先级实时任务长期占用同一 CPU。
如果系统没有 pidstat,可以使用:
ps -eLo pid,tid,psr,stat,pcpu,comm | grep '\[ksoftirqd/'
这些工具给出的是线程级 CPU 使用情况,不直接给出每个 Softirq 的处理时延。
5. 使用 ftrace 或 trace-cmd 看入口和退出
如果系统安装了 trace-cmd,可以尝试:
sudo trace-cmd record \
-e irq:irq_handler_entry \
-e irq:irq_handler_exit \
-e irq:softirq_entry \
-e irq:softirq_exit \
-e sched:sched_switch \
sleep 10
sudo trace-cmd report
这些 tracepoint 的可用性应先检查:
sudo trace-cmd list -e | grep -E 'irq|softirq|sched_switch'
如果事件名或工具支持不同,应以本机列出的事件为准。分析时关注:
- IRQ handler 的持续时间;
- Softirq 从 entry 到 exit 的持续时间;
- Softirq 是否频繁转入
ksoftirqd; ksoftirqd是否被其他任务延迟调度;- IRQ、Softirq 和目标应用是否位于同一个 CPU。
也可以直接查看 tracefs:
mount | grep trace
ls /sys/kernel/tracing/events/irq/
生产环境使用 tracing 需要控制采样时间和事件数量。过度开启函数级 tracing 或高频事件会改变被测系统,尤其会放大中断和调度开销。
十二、常见故障路径
1. 硬中断风暴
可能的路径是:
设备状态未清除
-> IRQ handler 返回
-> 同一 IRQ 立即再次进入
-> CPU 长时间执行 IRQ handler
-> 普通任务和 Softirq 得不到运行机会
表现包括:
/proc/interrupts某个 IRQ 极快增长;- 某个 CPU 的硬中断占用异常高;
- 应用延迟和系统响应急剧恶化;
- 驱动日志可能出现 repeated interrupt、timeout 或 reset。
原因可能是错误的设备应答顺序、共享 IRQ 判断错误、硬件故障或驱动 bug。直接屏蔽 IRQ 可能暂时止住 CPU 消耗,但也会导致设备不再工作,不能作为无条件修复。
2. Softirq backlog 持续增长
典型路径:
事件到达率 > Softirq 服务率
-> NAPI/协议栈队列增长
-> 本轮预算耗尽
-> pending 保持
-> ksoftirqd 被反复唤醒
-> 应用读取延迟、丢包或超时
这里要区分两种情况:
ksoftirqd高 CPU 且 backlog 计数增长:处理能力不足或事件突发;ksoftirqdCPU 不高但 backlog 增长:它可能没有获得 CPU,或者工作被其他瓶颈阻塞;- Softirq 高但 backlog 不增长:可能只是高吞吐下正常完成工作。
3. 一个 CPU 过载,其他 CPU 空闲
这通常与以下因素有关:
- 所有 IRQ affinity 指向一个 CPU;
- 网卡只有一个 RX 队列;
- RSS 哈希配置不合理;
- RPS mask 过窄;
- 应用线程、IRQ 和 Softirq 被错误地绑定;
- NUMA 跨节点访问导致迁移策略收益不明显。
此时盲目把所有 IRQ 分散到更多 CPU 可能产生反效果。需要先确认硬件队列、流量模式和应用线程的实际亲和性。
4. 高优先级任务反而引起设备 backlog
在实时或高优先级调度环境中,Softirq 仍然需要 CPU。若一个 SCHED_FIFO 线程长时间运行并禁止同一 CPU 上的其他工作,可能出现:
实时线程长期占用 CPU
-> ksoftirqd 得不到运行机会
-> 网卡 RX backlog 增长
-> 丢包或设备环溢出
这说明“提高应用优先级”不是单向优化。实时设计必须为 IRQ、threaded IRQ、Softirq 和内核 housekeeping 工作保留可运行的 CPU,并正确配置 affinity。
十三、降低延迟时应先判断问题发生在哪一层
情况一:Top Half 太长
检查驱动是否在硬 IRQ handler 中:
- 扫描过大的设备队列;
- 做复杂协议解析;
- 获取可能长时间竞争的锁;
- 执行不必要的日志输出;
- 没有正确关闭或确认中断源。
修复方向是缩短硬中断路径,把可延后工作交给 NAPI、threaded IRQ、Softirq 或 workqueue。
情况二:Softirq 服务时间太长
检查:
- 网络包处理路径;
- 防火墙、桥接、隧道、虚拟网络;
- RCU 回调和定时器积压;
- 单次批量处理是否过大;
- 是否存在全局锁或缓存抖动。
不能简单地把所有 Softirq 变成 workqueue。这样可能允许睡眠,但会增加调度延迟,并改变原有的吞吐和并发语义。
情况三:ksoftirqd 等不到 CPU
检查:
- CPU 是否被实时线程占用;
- CPU 是否被隔离但没有保留 housekeeping CPU;
- cgroup CPU 配额是否限制内核线程运行;
- 虚拟机 vCPU 是否被宿主机抢占;
- IRQ 和 ksoftirqd 是否被绑定到繁忙 CPU。
这时调整 NAPI 参数或网卡中断合并未必有效,因为根本问题是服务线程没有获得足够 CPU。
情况四:CPU 利用率不高但尾延迟高
平均 CPU 使用率掩盖不了以下问题:
- IRQ affinity 导致局部过载;
- 突发队列;
- 锁竞争;
- NUMA 远端内存访问;
- CPU 频率切换;
- IRQ/Softirq 与应用线程之间的调度干扰;
- 少数长尾 handler。
应使用 tracing 或基于时间戳的采样观察分位数,例如 p99、p99.9,而不是只观察平均值。
十四、生产环境中的修改和恢复
IRQ affinity、RPS、网卡 coalescing、NAPI 相关参数和实时调度设置都可能立即影响生产流量。修改前至少应保存:
mkdir -p /tmp/irq-state
cat /proc/interrupts > /tmp/irq-state/interrupts.before
cat /proc/softirqs > /tmp/irq-state/softirqs.before
cat /proc/net/softnet_stat > /tmp/irq-state/softnet_stat.before
sudo ethtool -c eth0 > /tmp/irq-state/eth0-coalesce.before
for f in /proc/irq/*/smp_affinity_list; do
[ -r "$f" ] && printf '%s ' "$f" && cat "$f"
done > /tmp/irq-state/irq-affinity.before
验证不能只看“CPU 降了”:
- 业务端到端延迟是否改善;
- p99 和 p99.9 是否改善;
- 网卡、协议栈、socket 是否丢包;
/proc/net/softnet_stat中相关计数是否继续增长;- IRQ 是否出现新的单 CPU 热点;
- 普通任务和实时任务是否受到影响;
- 重启、驱动 reload 或
irqbalance是否覆盖了手工配置。
irqbalance 可能自动调整 IRQ affinity。手工设置后,如果发现配置很快恢复,应检查服务状态和管理策略,而不是反复写入 sysfs/procfs。
十五、在 PREEMPT_RT 等配置下不要套用普通内核模型
普通主线内核中,硬 IRQ、Softirq 和 ksoftirqd 的关系如前文所述。但启用 PREEMPT_RT 等实时配置后,部分中断和 Softirq 的执行方式会发生变化:
- 中断可能被线程化;
- 自旋锁语义可能变化;
- Softirq 可能更多地在线程上下文中处理;
- 优先级继承、抢占和锁延迟会影响路径。
因此,不能把普通内核上的“Softirq 一定不可抢占、一定不会睡眠”简单套到所有实时配置的实现细节上。不过驱动仍应遵守对应内核版本和实时补丁的 API 约束,不能因为某个测试环境中路径被线程化,就在通用 IRQ handler 中随意使用可睡眠接口。
诊断实时系统时,应额外查看:
uname -a
cat /sys/kernel/realtime 2>/dev/null || true
/sys/kernel/realtime 并非所有内核都提供;没有该文件不能单独证明系统一定不是实时内核。还应查看发行版内核配置和文档。
十六、几个容易混淆的结论
“Softirq 就是一个线程”
不正确。Softirq 是内核的一类延后执行机制,可能直接在硬中断返回路径或其他安全点执行;当工作积压时,ksoftirqd/N 才会参与处理。
“ksoftirqd CPU 高说明它有 bug”
不一定。高网络吞吐或块设备负载下,Softirq 本来就可能消耗大量 CPU。需要确认是否伴随 backlog、丢包、延迟或异常增长。
“IRQ 数量少,所以中断开销小”
不一定。一次 IRQ 可能触发大量 NAPI、协议栈或块层工作。应同时看 Softirq 和业务处理路径。
“把 IRQ 分散到所有 CPU 就能降低延迟”
不一定。过度分散会增加跨 CPU通信和缓存失效;跨 NUMA 节点还会增加远端内存访问。正确目标是让硬件队列、Softirq、应用线程和数据内存形成合理的局部性。
“把 Softirq 改成 workqueue 一定更好”
不一定。workqueue 可以睡眠,适合复杂工作,但线程调度和排队会增加延迟;网络接收等高频路径通常需要 NAPI 和 Softirq 的批处理特性。
“降低中断合并参数总能降低延迟”
不一定。它可能减少等待,却提高 IRQ 次数和 CPU 占用。在 CPU 已经接近饱和时,理论上更短的合并等待可能导致更严重的 Softirq backlog。
十七、把完整路径串起来
以网卡收到数据为例,完整的因果链可以写成:
网卡 DMA 写入 RX ring
-> 网卡发送 MSI-X
-> 中断控制器把向量路由到目标 CPU
-> Linux IRQ handler 被调用
-> 驱动确认设备并安排 NAPI
-> NET_RX Softirq pending
-> 中断返回路径或 ksoftirqd 执行 Softirq
-> NAPI poll 批量取包
-> 内核网络协议栈处理
-> 路由、防火墙、socket 分发
-> 唤醒等待中的应用线程
-> 应用获得 CPU 并读取数据
其中任何一段都可能成为延迟来源:
- IRQ 路由错误导致目标 CPU 过载;
- Top Half 太长导致硬中断延迟;
- NAPI budget 或 Softirq 处理能力不足导致 backlog;
ksoftirqd没有及时获得 CPU;- 网络协议栈或锁竞争导致 Softirq 服务时间增加;
- 应用线程调度或 NUMA 布局导致最终可见延迟增加。
因此,分析中断延迟不能只看 /proc/interrupts,也不能只看 ksoftirqd 的 CPU 百分比。应把 IRQ 计数、Softirq 类型、网络 backlog、CPU affinity、调度事件和业务端到端延迟放在同一时间窗口内观察。
Linux 的 Bottom Half 机制本质上是在“立即响应硬件”和“允许系统继续调度”之间做分层处理:Top Half 保证快速应答,Softirq 承担高频且不需睡眠的延后工作,ksoftirqd 防止这些工作无限占据中断返回路径,而 threaded IRQ 和 workqueue 则为需要调度、等待和更复杂同步的任务提供进程上下文。理解这条边界,才能在面对 IRQ 热点、Softirq backlog、ksoftirqd 高 CPU 和尾延迟问题时,先定位真正的瓶颈,再选择不会破坏其他路径的调整方式。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux 调度器深入:CFS、实时策略、优先级、Affinity 和 NUMA
- 下一篇:Linux 内核模块:加载、参数、依赖、签名、DKMS 和故障恢复
- 延伸:Linux 内核与用户态:系统调用、异常、中断和硬件抽象
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论