一、中断机制概述
中断(Interrupt)是计算机系统中最重要的机制之一,它允许硬件设备在完成某个事件后通知 CPU 进行处理。Linux 内核的中断处理机制是整个系统响应外部事件的核心路径,理解它对于系统性能调优、驱动开发和问题诊断至关重要。
中断的本质:打断 CPU 当前正在执行的程序,转去执行特定的处理程序(中断服务例程,ISR),处理完毕后恢复原程序的执行。
1.1 中断的分类
- 同步中断(异常):由 CPU 执行指令时产生,包括故障(Fault)、陷阱(Trap)、中止(Abort)
- 异步中断(硬中断):由硬件设备产生,如网卡收到数据包、键盘按键、磁盘 I/O 完成
- 软中断(SoftIRQ):内核机制,用于处理硬中断下半部分的延迟工作
- Tasklet / Workqueue:基于软中断或内核线程的延迟处理机制
1.2 中断的硬件基础
现代 x86 系统中,中断通过 APIC(Advanced Programmable Interrupt Controller)管理:
- Local APIC:集成在每个 CPU 核心中,接收本地中断源(定时器、温度传感器、LINT0/LINT1)
- I/O APIC:外部芯片,管理来自外部设备的中断请求(IRQ),通过中断路由表(PRT)分发到指定 CPU 的 Local APIC
- MSI/MSI-X:消息信号中断,设备通过写入特定内存地址来触发中断,无需物理中断线,支持 2048 个中断向量
- 中断优先级:基于中断向量号除以 16 得到优先级(0-15),高优先级可打断低优先级
1.3 IDT:中断描述符表
IDT 是 x86 架构中关联中断/异常向量号与其处理函数的核心数据结构:
struct gate_struct {
u16 offset_low; // 处理函数地址低16位
u16 segment; // 代码段选择子
unsigned ist : 3, // IST索引(中断栈表)
zero0 : 5,
type : 5, // 门类型(中断门/陷阱门/任务门)
dpl : 2, // 描述符特权级
p : 1; // 存在位
u16 offset_middle; // 处理函数地址中16位
u32 offset_high; // 处理函数地址高32位
u32 zero1;
} __attribute__((packed));
- 中断门(Interrupt Gate, type=14):进入时自动清除 IF 标志(屏蔽可屏蔽中断),用于硬件中断
- 陷阱门(Trap Gate, type=15):进入时不改变 IF 标志,用于异常和系统调用
- IST(Interrupt Stack Table):提供 7 个备用栈指针,防止中断发生在损坏的栈上
二、硬中断处理流程
当一个硬件中断到来时,CPU 内核经过以下路径进行处理:
2.1 中断入口:entry_INT80_32 / entry_IRQ
// x86_64 中断入口
SYM_CODE_START(entry_ALLQ_FUNCTION)
// 1. 保存被中断程序的上下文(pt_regs)
// - 通用寄存器 (RAX, RBX, RCX, RDX, RSI, RDI, RBP, R8-R15)
// - 段寄存器 (CS, DS, ES, FS, GS)
// - RFLAGS, RIP, RSP
// 2. 如果从用户态切入,切换到内核栈
// 3. 调用 do_IRQ() 或对应的异常处理
SYM_CODE_END(entry_ALLQ_FUNCTION)
2.2 do_IRQ:中断分发核心
// arch/x86/kernel/irq.c
unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc;
unsigned vector = ~regs->orig_ax;
// 进入中断上下文(禁止抢占和调度)
irq_enter();
// 通过向量号从 irq_desc[] 数组获取中断描述符
desc = this_cpu(vector_irq)[vector];
if (likely(desc))
generic_handle_irq_desc(desc); // 调用 handle_irq()
// 退出中断上下文
irq_exit();
set_irq_regs(old_regs);
return 1;
}
2.3 中断描述符:irq_desc
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int *kstat_irqs;
irq_flow_handler_t handle_irq; // 流控处理函数
struct irqaction *action; // 中断动作链表
unsigned int status_use_accessors;
unsigned int depth; // 禁用深度
unsigned int wake_depth; // 唤醒深度
unsigned int irq_count; // 中断计数
unsigned long last_unhandled;
unsigned int irqs_unhandled;
atomic_t threads_active;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_SPARC
struct irq_cfg *irq_cfg;
#endif
} ____cacheline_internodealigned_in_smem;
每个 IRQ 编号对应一个 irq_desc 结构体,其中 action 字段是一个链表,包含所有注册在该 IRQ 上的中断处理函数。
三、上半部与下半部:中断分割处理
设计动机:硬中断处理时需要屏蔽中断,长时间关中断会导致中断延迟增加、系统响应能力下降。Linux 将中断处理分为:
- 上半部(Top Half / HardIRQ):必须在关中断或屏蔽当前 IRQ 的条件下执行,只做最紧急的工作(读取硬件状态、应答中断)
- 下半部(Bottom Half):延迟执行的任务(数据处理、协议栈操作),可以开中断运行
3.1 request_irq:注册中断处理函数
// include/linux/interrupt.h
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
// 完整注册接口
int request_threaded_irq(unsigned int irq,
irq_handler_t handler, // 上半部处理函数
irq_handler_t thread_fn, // 线程化处理函数(下半部)
unsigned long flags,
const char *name, void *dev_id);
各 flag 参数说明:
IRQF_SHARED:多个设备共享同一 IRQ 线(PCI 设备默认要求共享)IRQF_PROBE_SHARED:仅在探测阶段允许共享IRQF_TIMER:定时器中断,有特殊处理规则IRQF_PERCPU:中断绑定到单个 CPUIRQF_NO_THREAD:禁止中断线程化IRQF_ONESHOT:在中断线程处理完成前保持中断屏蔽(用于 threaded IRQ)
四、软中断(SoftIRQ)详解
软中断是 Linux 内核中下半部机制的基础,是最底层、最高效的延迟处理机制。
4.1 软中断类型与优先级
// include/linux/interrupt.h
enum {
HI_SOFTIRQ = 0, // 高优先级 Tasklet(最早执行)
TIMER_SOFTIRQ, // 定时器软中断
NET_TX_SOFTIRQ, // 网络发送
NET_RX_SOFTIRQ, // 网络接收(NAPI poll 在此调用)
BLOCK_SOFTIRQ, // 块设备(完成请求处理)
IRQ_POLL_SOFTIRQ, // IRQ poll
TASKLET_SOFTIRQ, // 普通 Tasklet
SCHED_SOFTIRQ, // 调度器(SCHED_FIFO 等)
HRTIMER_SOFTIRQ, // 高精度定时器
RCU_SOFTIRQ, // RCU 回调(宽限期处理)
NR_SOFTIRQS // 软中断类型总数 = 10
};
4.2 软中断的执行时机
软中断在以下时机会被检查和执行:
- 硬中断返回时:
irq_exit()中调用invoke_softirq() - 下半部重新使能时:
local_bh_enable()时检查是否有 pending 软中断 - ksoftirqd 内核线程:如果软中断触发过于频繁,由 per-CPU 的
ksoftirqd线程在普通进程上下文中处理
4.3 SoftIRQ 执行流程
// kernel/softirq.c
static void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
...
if (unlikely(!in_interrupt() && local_softirq_pending()))
do_softirq(); // 处理所有 pending 软中断
...
}
asmlinkage __visible void do_softirq(void)
{
unsigned long flags;
struct action *h;
if (in_softirq()) // 已经在软中断上下文中则退出
return;
local_irq_save(flags);
// 获取当前 CPU 的 pending 软中断位图
pending = local_softirq_pending();
if (pending) {
// 设置当前 task 的软中断上下文标记
h = softirq_vec;
set_softirq_pending(0); // 清除所有 pending 位
local_irq_restore(flags);
// 循环处理所有 pending 的软中断
do {
if (pending & 1)
h->action(h);
h++;
pending >>= 1;
} while (pending);
}
}
4.4 软中断的并发模型
- Per-CPU 变量:软中断处理函数操作的 pending 位图是 per-CPU 的,不同 CPU 可以同时处理不同类型的软中断
- 同类互斥:同一类型的软中断在同一个 CPU 上不可重入(串行执行)
- 开中断执行:软中断执行时允许硬中断打断
- 限制循环次数:每个 CPU 每轮 softirq 最多执行
MAX_SOFTIRQ_RESTART(默认 10)次,之后唤醒 ksoftirqd
4.5 ksoftirqd 内核线程
当软中断触发频率过高(例如大量网络数据包到达),硬中断返回时软中断处理不完,内核会唤醒 ksoftirqd 线程:
// kernel/softirq.c
static void run_ksoftirqd(unsigned int cpu)
{
ksoftirqd_enter(cpu);
local_bh_disable();
// 检查并处理软中断
if (local_softirq_pending()) {
do_softirq();
local_bh_enable();
cond_resched();
return;
}
local_bh_enable();
ksoftirqd_exit(cpu);
}
// 典型优化:运行 ksoftirqd(防止单核过载)
// ps aux | grep ksoftirqd
// root 1234 0.5 0.0 0 0 ? S 10:00 0:00 [ksoftirqd/0]
五、Tasklet:基于软中断的下半部
Tasklet 是基于 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 的通用下半部机制,大多数驱动程序使用 Tasklet。
5.1 Tasklet 数据结构
// include/linux/interrupt.h
struct tasklet_struct {
struct tasklet_struct *next; // 链表指针
unsigned long state; // 状态位图
atomic_t count; // 引用计数(0=启用,!=0=禁用)
void (*func)(unsigned long); // 处理函数
unsigned long data; // 传递给 func 的参数
};
// Tasklet 状态
enum {
TASKLET_STATE_SCHED = 0, // 已被调度(挂入 executing 链表)
TASKLET_STATE_RUN, // 正在执行
};
5.2 Tasklet 调度与执行
// kernel/softirq.c
void tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
// 检查是否已经在调度队列中
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
// 挂入当前 CPU 的 tasklet_vec 链表
t->next = NULL;
*__this_cpu_read(tasklet_vec.head) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
// 触发软中断
raise_softirq_irqoff(TASKLET_SOFTIRQ);
}
local_irq_restore(flags);
}
// Tasklet 执行函数
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, &__this_cpu(tasklet_vec.head));
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
// 清除 SCHED 状态后执行
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
// 如果被禁用,重新挂入队列
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.head) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
5.3 Tasklet 关键特性
- 串行执行:同一个 Tasklet 在任意时刻只能在某一个 CPU 上执行(通过 TASKLET_STATE_RUN 状态保证)
- Per-CPU 调度队列:Tasklet 总是在调度它的 CPU 上执行,避免缓存失效
- 不能睡眠:Tasklet 在软中断上下文中执行,不能调用可能睡眠的函数
- 禁止类型限制:Tasklet 执行时不能持有信号架、不能调用 copy_to_user/copy_from_user
六、工作队列(Workqueue):可睡眠的下半部
工作队列是将工作推迟到内核线程(worker)中执行的机制,可以睡眠、可以执行耗时操作。
6.1 工作队列架构
// include/linux/workqueue.h
struct workqueue_struct {
struct list_head pwqs; // 挂载的 pwq 链表
struct list_head list; // 全局工作队列链表
struct mutex mutex;
int work_color; // 颜色标记(调试用)
int flush_color;
atomic_t nr_pwqs_to_flush;
struct wq_device *wq_dev;
char name[WQ_NAME_LEN];
...
};
struct pool_workqueue {
struct worker_pool *pool; // 关联的线程池
struct workqueue_struct *wq; // 所属工作队列
int work_color;
int flush_color;
struct list_head delayed_works; // 延迟执行的 work
...
};
struct worker_pool {
spinlock_t lock;
int cpu;
int node; // NUMA 节点
int id;
unsigned int flags;
struct list_head worklist; // 工作链表
int nr_workers; // 工作线程数
...
};
6.2 创建与调度工作
// 方式1: 使用共享的全局工作队列
schedule_work(&work); // 立即调度
schedule_delayed_work(&work, delay); // 延迟调度(单位:jiffies)
// 方式2: 创建私有工作队列
struct workqueue_struct *my_wq = create_workqueue("my_wq");
queue_work(my_wq, &work);
queue_delayed_work(my_wq, &dwork, msecs_to_jiffies(100));
// 方式3: 使用 alloc_workqueue(推荐)
struct workqueue_struct *wq = alloc_workqueue("my_wq", WQ_UNBOUND | WQ_HIGHPRI, 0);
6.3 Worker 线程生命周期
// kernel/workqueue.c
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;
// 设置 worker 线程状态
worker->task->flags |= PF_WQ_WORKER;
recheck:
// 如果没有待处理的工作,睡眠等待
if (likely(!need_more_worker(pool)))
schedule();
// 醒来后检查工作列表
while (!list_empty(&pool->worklist)) {
struct work_struct *work = list_first_entry(
&pool->worklist, struct work_struct, entry);
// 从链表中取出,执行
worker->current_work = work;
work->func(work);
worker->current_work = NULL;
}
goto recheck;
}
6.4 Workqueue 各类型比较
- WQ_UNBOUND:不绑定 CPU,适用于跨 NUMA 节点的工作
- WQ_HIGHPRI:高优先级 worker 线程(nice -20),适用于延迟敏感任务
- WQ_CPU_INTENSIVE:CPU 密集型工作,不计入 workqueue 并发限制
- WQ_MEM_RECLAIM:可参与内存回收的工作(用于文件系统 Journal 等)
- WQ_FREEZABLE:系统休眠时冻结的工作
- max_active:最大并发工作数(默认 256,WQ_UNBOUND 默认 使用 nr_cpu_ids * 4)
七、线程化中断(Threaded IRQ)
Linux 3.x 引入了线程化中断机制,将硬中断处理的大部分工作从 HardIRQ 上下文移到内核线程中执行,减少关中断时间。
7.1 线程化中断的优势
- 减少硬中断延迟:HardIRQ 处理函数只做最少的操作(如确认中断来源)
- 可睡眠:线程化 handler 中可以调用可能睡眠的函数(mutex、alloc、I/O)
- 实时优先级:线程化 handler 可以设置为 SCHED_FIFO 优先级
- 可被抢占:线程化 handler 执行期间可被更高优先级的中断/进程抢占
7.2 注册线程化中断
// 方法1: 使用 request_threaded_irq
int ret = request_threaded_irq(irq,
my_hard_handler, // 上半部(可为 NULL)
my_thread_handler, // 线程化处理
IRQF_ONESHOT, // 推荐:处理完成前屏蔽中断
"my_device", dev);
// 方法2: 使用 devm_request_threaded_irq(自动释放)
ret = devm_request_threaded_irq(dev, irq, NULL, my_handler,
IRQF_ONESHOT, "my_device", dev);
// 方法3: 使用 request_any_context_irq(自动选择)
ret = request_any_context_irq(irq, handler, flags, name, dev);
// 返回值:
// - 非负:成功
// - -EOPTNOTSUPP:不支持线程化
// - ENOSYS:没有 hardirq handler 且不支持线程化
7.3 IRQF_ONESHOT 标志
这是线程化中断最重要的标志,它保证在线程handler函数执行完成之前,中断线始终被屏蔽:
// kernel/irq/manage.c
static irqreturn_t irq_thread_fn(void *data)
{
struct irqaction *action = data;
irqreturn_t ret;
while (true) {
// 执行 handler 前清掉中断
if (!IRQ_HANDLED_WITHOUT_MASKING)
unmask_threaded_irq(desc); // 解屏蔽中断线
ret = action->thread_fn(action-> irq, action->dev_id);
// 再次确认是否有新的中断到达(级联中断场景)
if (!(desc->istate & IRQS_PENDING))
break;
}
// handler 返回 IRQ_HANDLED 后,屏蔽中断并唤醒等待者
return ret;
}
八、中断亲和性(IRQ Affinity)
中断亲和性允许将特定的 IRQ 绑定到指定的 CPU 核心处理,是实现网卡多队列、隔离中断负载的关键技术。
8.1 设置中断亲和性
// 查看当前 IRQ 的 CPU 绑定(IRQ 号 123 为例)
cat /proc/irq/123/smp_affinity
cat /proc/irq/123/smp_affinity_list
// 将 IRQ 123 绑定到 CPU 0-1(十六进制掩码 0x3)
echo 3 > /proc/irq/123/smp_affinity
// 将 IRQ 123 绑定到 CPU 2-3(十六进制掩码 0xc)
echo c > /proc/irq/123/smp_affinity
// 通过 irqbalance 服务自动管理
systemctl stop irqbalance # 停止手动管理
8.2 网卡多队列与 RSS
现代网卡通常支持 16-128 个 TX/RX 队列,每个队列有独立的中断:
# 查看网卡队列
ethtool -l eth0
# Combined: 8 # 当前使用 8 个队列
# RX: 0
# TX: 0
# 设置网卡队列数
ethtool -L eth0 combined 16
# 配置网卡 Ring Buffer
ethtool -G eth0 rx 4096 tx 4096
# 查看各队列的 IRQ 号
cat /proc/interrupts | grep eth0
# 手动分配中断到不同 CPU
echo "1" > /proc/irq/123/smp_affinity # CPU0 - eth0-rx-0
echo "2" > /proc/irq/124/smp_affinity # CPU1 - eth0-rx-1
echo "4" > /proc/irq/125/smp_affinity # CPU2 - eth0-rx-2
echo "8" > /proc/irq/126/smp_affinity # CPU3 - eth0-rx-3
8.3 irqbalance 服务
irqbalance 是用户空间的中断负载均衡守护进程:
- 工作模式:周期性采样各 CPU 的中断负载,动态调整 IRQ 与 CPU 的绑定关系
- SNC(Sub-Numa Clustering):在 NUMA 架构下,优先将中断绑定到设备所在的 NUMA 节点
- 禁止规则:可配置特定 IRQ 不被 irqbalance 管理(如手机、实时系统)
- 性能建议:在高性能网络场景建议禁用 irqbalance,手动配置亲和性
九、中断处理中的并发与同步
9.1 中断上下文 vs 进程上下文
- 中断上下文:没有关联的 task_struct,不能睡眠(没有调度实体),执行时间短
- 进程上下文:内核线程或用户进程,可以睡眠,可重入
- 判断方法:
in_interrupt()返回 true 表示在中断/软中断上下文 - 危险操作:在中断上下文调用
copy_to_user()、kmalloc(GFP_KERNEL)、mutex_lock()等会导致系统崩溃
9.2 自旋锁与中断屏蔽
// 场景1: 仅进程上下文共享数据(同一 CPU 不同时间点执行)
spin_lock(&lock);
// 访问临界区
spin_unlock(&lock);
// 场景2: 进程上下文 + 硬件中断共享数据
unsigned long flags;
spin_lock_irqsave(&lock, flags);
// 访问临界区(中断已屏蔽,不会被打断)
spin_unlock_irqrestore(&lock, flags);
// 场景3: 进程上下文 + 软中断共享数据
spin_lock_bh(&lock);
// 访问临界区(软中断被屏蔽)
spin_unlock_bh(&lock);
// 场景4: 与 Tasklet 共享数据(Tasklet 不能睡眠)
spin_lock_bh(&lock); // 或 spin_lock(&lock)
// 访问临界区
spin_unlock_bh(&lock);
9.3 Per-CPU 变量
Per-CPU 变量是中断上下文中最常用的无锁数据访问方式:
// 声明
DEFINE_PER_CPU(unsigned long, my_counter);
// 读写(不需要锁,因为每个 CPU 有自己的副本)
get_cpu_var(my_counter)++;
__get_cpu_var(my_counter) += 10;
put_cpu_var(my_counter);
// API 形式
this_cpu_inc(my_counter); // 原子 +1
this_cpu_add(my_counter, 5); // 原子 +5
raw_cpu_ptr(&my_counter); // 获取原始指针
9.4 RCU 与中断上下文
RCU(Read-Copy-Update)是读多写少的并发机制,在 INTERRUPT 上下文中:
- 读侧:
rcu_read_lock()/rcu_read_unlock()可在中断上下文中使用(不可睡眠) - 写侧:
synchronize_rcu()会睡眠,不能在中断上下文使用 - 替代方案:使用
call_rcu()将回调函数放到 RCU_SOFTIRQ 中异步执行 - 中断延迟:
rcu_nocbs参数可将特定 CPU 移出 RCU 回调,降低中断延迟
十、中断性能调优与监控
10.1 关键性能参数
# /etc/sysctl.conf
# 软中断处理包数上限(每轮)
net.core.netdev_budget = 600 # NAPI 单次 poll 最大包数(默认 300)
net.core.netdev_budget_usecs = 8000 # NAPI 单次 poll 最大耗时(微秒)
# ksoftirqd 优先级(负值提升优先级)
# /etc/systemd/system/ksoftirqd.service
# Nice=-20
# CPUSchedulingPolicy=fifo
# 中断处理 Workers 数量
# 内核编译参数 CONFIG_RPS_RFS_ENTRIES=32768
# PCI 总线相关
# 确保 PCIe Max_P_Read_Request_Size 合理(通常为 4096 字节)
10.2 中断监控命令
# 查看所有中断在各 CPU 上的分布
cat /proc/interrupts
# 输出示例:
# CPU0 CPU1 CPU2 CPU3
# 9: 12345 678 ... ... PCI-MSI-edge eth0-rx-0
# 10: 678 12345 ... ... PCI-MSI-edge eth0-rx-1
# 11: ... ... 12345 ... PCI-MSI-edge eth0-rx-2
# 软中断统计(包括 NET_RX、NET_TX、BLOCK 等)
cat /proc/softirqs
# 中断处理耗时分析(使用 perf)
perf record -g -e irq:irq_handler_entry -ag sleep 10
# NAPI/softirq 监控
cat /proc/net/softnet_stat # 每列:处理数、时延、budget用完次数、CPU碰撞
# ksoftirqd 线程 CPU 占用(如果某 CPU 上此线程占用高,说明软中断过载)
top -p $(pgrep ksoftirqd)
10.3 中断优化策略
- 合并中断:
ethtool -C eth0 rx-usecs 100(100微秒合并间隔)减少中断次数 - 多队列分流:配置多队列网卡 RSS + 手动 IRQ affinity,分散中断负载
- NAPI 机制:网卡数据包接收切换到轮询模式,降低高速率场景下的中断风暴
- XDP 绕过:在网卡驱动层直接处理/丢弃数据包,零协议栈开销
- CPU 隔离:通过
isolcpus内核参数将特定 CPU 从调度器中隔离,专用于中断处理
十一、实战案例:网卡中断风暴处理
11.1 问题现象
某视频流媒体服务器(10GbE 网卡)在高峰期出现:
- TCP 重传率上升至 15%
ksoftirqd/3CPU 占用持续 100%top显示si(soft interrupt)占用比sy高出 10 倍
11.2 诊断分析
# 查看 CPU 3 处理的软中断
watch -n 1 'cat /proc/softirqs | head -6'
# NET_RX_SOFTIRQ 在 CPU 3 上每秒处理 30000+ 次
# 检查网卡中断分布
cat /proc/interrupts | grep eth0
# 发现所有网卡中断都落在 CPU 3 上(亲和性配置错误)
# 查看网卡队列数
ethtool -l eth0
# Combined: 1 # 仅启用 1 个队列!
# 查看 /proc/net/softnet_stat 第 3 列(budget耗尽次数)
# 该值持续增长,表示 NAPI poll 配额不够
11.3 解决方案
# 步骤1: 启用多队列
ethtool -L eth0 combined 8
# 步骤2: 手动配置多队列 RSS
ethtool -X eth0 equal 8
# 步骤3: 手动绑定中断到不同 CPU
cat /proc/interrupts | grep eth0
# eth0-rx-0 -> IRQ 120 -> CPU 0
# eth0-rx-1 -> IRQ 121 -> CPU 1
# ...
echo 1 > /proc/irq/120/smp_affinity
echo 2 > /proc/irq/121/smp_affinity
echo 4 > /proc/irq/122/smp_affinity
echo 8 > /proc/irq/123/smp_affinity
# 步骤4: 增大 NAPI budget
sysctl -w net.core.netdev_budget=600
# 步骤5: 停止 irqbalance(手动管理更精确)
systemctl stop irqbalance
# 步骤6: 启用 XDP 处理高吞吐模式
ip link set dev eth0 xdp obj xdp_pass.o
优化后:
- TCP 重传率从 15% 降至 0.1% 以下
- ksoftirqd 从单核 100% 降至 8 核各 12%
- 网卡吞吐量从 2.1Gbps 提升至 9.4Gbps(接近线速)
十二、总结
Linux 内核中断处理机制的核心设计原则是:快速响应、延迟处理。通过将中断分为上半部(HardIRQ)和下半部(SoftIRQ / Tasklet / Workqueue),在保证硬件及时响应的同时,将耗时操作推迟到更安全的上下文中执行。
关键知识点回顾:
- IDT + IRQ 向量:中断的入口路由机制
- 上半部/下半部分离:HardIRQ 做最少工作,Bottom Half 延迟处理
- SoftIRQ:10 种类型、Per-CPU 位图、串行执行、最高优先级下半部
- Tasklet:基于 SoftIRQ,不可睡眠,适用于驱动下半部
- Workqueue:可睡眠、可重入,适用于文件系统、块设备等复杂场景
- Threaded IRQ:IRQF_ONESHOT,将中断大部分工作转移到线程中
- IRQ Affinity:多队列网卡 + CPU 绑定,是实现高性能网络的基础
- 同步原语:spin_lock_irqsave(关中断)、spin_lock_bh(关软中断)、Per-CPU 变量
掌握这些机制,不仅能写出更高效的内核驱动代码,也能在系统性能调优中精准定位中断相关的瓶颈。

发表评论 取消回复