Linux 内核中断处理机制详解:上半部、下半部与延迟执行策略
目录
- 引言与设计动机
- [中断上半部(Top Half)](#中断上半部(Top Half))
- [中断下半部(Bottom Half)](#中断下半部(Bottom Half))
- 软中断(Softirq)
- Tasklet(小任务)
- 工作队列(Workqueue)
- [中断线程化(Threaded IRQ)](#中断线程化(Threaded IRQ))
- 各机制对比与选型指南
- 实战代码示例
- 总结
1. 引言与设计动机
1.1 中断处理面临的核心矛盾
在 Linux 内核中,中断处理程序面临两个看似矛盾的需求:
- 快速响应:中断处理必须尽快完成,否则会影响系统实时性,导致其他中断丢失或系统卡顿。
- 完成大量工作:实际的中断处理往往涉及数据拷贝、协议解析、设备状态更新等耗时操作。
例如,当网卡接收到数据包时,硬件中断必须立即被响应(清除中断标志、确认硬件状态),但将数据包从 DMA 缓冲区拷贝到内核网络栈、进行协议解析等操作则相对耗时,不适合在中断上下文中完成。
1.2 解决方案:上半部/下半部分离
Linux 内核采用了经典的 "上半部/下半部"(Top Half / Bottom Half) 设计模式:
┌─────────────────────────────────────────────────────────┐
│ 硬件中断触发 │
└───────────────────────────┬─────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 上半部(Top Half / Hardirq) │
│ • 快速响应,严格限制执行时间(通常 < 100μs) │
│ • 关闭中断或禁止抢占 │
│ • 仅完成时间敏感的关键操作 │
│ • 例如:清除中断标志、读取硬件状态、触发下半部 │
└───────────────────────────┬─────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 下半部(Bottom Half) │
│ • 延迟执行,允许更长的处理时间 │
│ • 可以开启中断(部分机制) │
│ • 完成耗时的后续处理工作 │
│ • 例如:数据拷贝、协议处理、通知用户空间 │
└─────────────────────────────────────────────────────────┘
1.3 历史演进
| 内核版本 | 下半部机制 |
|---|---|
| Linux 2.2 及之前 | 下半部(BH)、任务队列(Task Queue) |
| Linux 2.4 | 引入软中断(Softirq)、Tasklet,逐步取代 BH |
| Linux 2.6 | 工作队列(Workqueue)取代任务队列;软中断、Tasklet 为主 |
| Linux 2.6.30+ | 新增中断线程化(Threaded IRQ) |
| Linux 5.9+ | Tasklet 引入新初始化 API(tasklet_setup),社区酝酿长期移除 |
2. 中断上半部(Top Half)
2.1 概念
上半部即硬中断处理程序 (Hard IRQ Handler),是中断发生时 CPU 立即执行的代码。它运行在中断上下文中,具有以下严格约束:
| 约束 | 说明 |
|---|---|
| ❌ 不能睡眠 | 不能调用 schedule()、msleep()、mutex_lock() 等 |
| ❌ 不能访问用户空间 | 不能调用 copy_to_user()、copy_from_user() |
| ❌ 不能分配可睡眠内存 | 不能使用 kmalloc(GFP_KERNEL),只能用 GFP_ATOMIC |
| ❌ 执行时间极短 | 通常要求 < 100μs,工业实时系统要求更严格 |
| ✅ 可以访问内核数据 | 可以操作内核数据结构(需注意并发保护) |
2.2 执行流程
硬件中断信号 → 中断控制器(GIC/APIC)→ CPU 响应
→ 保存当前上下文(寄存器压栈)
→ 跳转到中断向量表对应的处理入口
→ 执行通用中断处理框架(handle_irq)
→ 调用注册的 ISR(Interrupt Service Routine)
→ 恢复上下文,返回被中断的代码
2.3 注册中断处理程序
c
#include <linux/interrupt.h>
/*
* request_irq - 注册中断处理程序
* @irq: 中断号
* @handler: 中断处理函数(上半部)
* @flags: 标志位(IRQF_SHARED, IRQF_ONESHOT 等)
* @name: 设备名称(用于 /proc/interrupts 显示)
* @dev: 设备指针(共享中断时用于区分)
*/
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
/* 中断处理函数原型 */
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
/* 返回值 */
#define IRQ_NONE (0) /* 不是本设备的中断 */
#define IRQ_HANDLED (1) /* 中断已处理 */
#define IRQ_WAKE_THREAD (2) /* 需要唤醒中断线程(线程化中断) */
2.4 上半部的典型工作
c
static irqreturn_t my_device_isr(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
u32 status;
/* 1. 读取中断状态寄存器(必须快速) */
status = readl(dev->base + REG_INT_STATUS);
if (!status)
return IRQ_NONE; /* 不是本设备的中断 */
/* 2. 清除中断源(防止中断风暴) */
writel(status, dev->base + REG_INT_CLEAR);
/* 3. 保存硬件状态/数据到内存 */
dev->saved_data = readl(dev->base + REG_DATA);
/* 4. 触发下半部处理 */
tasklet_schedule(&dev->tasklet); /* 或 schedule_work(), raise_softirq() 等 */
return IRQ_HANDLED;
}
3. 中断下半部(Bottom Half)
3.1 概念
下半部是中断处理中可以延迟执行的部分。它的设计目标是:
- 将耗时操作从中断上下文中移出
- 在更宽松的执行环境中完成后续处理
- 保证系统响应性的同时完成必要工作
3.2 下半部的执行时机
下半部通常在以下时机被调度执行:
- 硬中断返回时 :
do_IRQ()返回前检查是否有待处理的软中断 - 内核线程
ksoftirqd:当软中断数量过多或处理时间过长时,由专用内核线程处理 - 显式调用 :代码中主动调用
local_bh_enable()时触发 - 工作队列线程 :由
kworker内核线程在进程上下文中执行
3.3 下半部的四种实现机制
下半部实现机制
│
┌────────────────┼────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Softirq │ │ Tasklet │ │Workqueue│
│ (软中断) │ │ (小任务) │ │(工作队列)│
└────┬────┘ └────┬────┘ └─────────┘
│ │
│ ┌───────────┘
│ │ (Tasklet 基于 Softirq 实现)
▼ ▼
┌─────────────┐
│ 中断上下文 │ ← 不可睡眠
└─────────────┘
┌─────────────┐
│ 进程上下文 │ ← 可以睡眠(Workqueue, Threaded IRQ)
└─────────────┘
4. 软中断(Softirq)
4.1 概述
软中断是 Linux 内核中最底层、性能最高的下半部机制。它是静态定义的,数量有限,主要供内核核心子系统使用。
4.2 数据结构
c
/* include/linux/interrupt.h */
struct softirq_action {
void (*action)(struct softirq_action *);
};
/* kernel/softirq.c - 软中断向量表(静态数组) */
static struct softirq_action softirq_vec[NR_SOFTIRQS]; /* NR_SOFTIRQS = 10 */
4.3 内核预定义的软中断类型
c
/* include/linux/interrupt.h */
enum {
HI_SOFTIRQ = 0, /* 高优先级 tasklet */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 网络发送 */
NET_RX_SOFTIRQ, /* 网络接收 */
BLOCK_SOFTIRQ, /* 块设备完成 */
IRQ_POLL_SOFTIRQ, /* IRQ 轮询 */
TASKLET_SOFTIRQ, /* 普通优先级 tasklet */
SCHED_SOFTIRQ, /* 调度器负载均衡 */
HRTIMER_SOFTIRQ, /* 高精度定时器 */
RCU_SOFTIRQ, /* RCU 回调 */
NR_SOFTIRQS /* 总数 = 10 */
};
注意 :软中断的数量在编译时确定(
NR_SOFTIRQS),不可动态增加。这是软中断的主要限制之一。
4.4 软中断的触发与执行
触发机制
每个 CPU 维护一个 32 位的 pending 位图(存储在 per-CPU 的 irq_cpustat_t 结构,即 irq_stat.__softirq_pending 字段),每一位对应一种软中断:
c
/* 触发软中断:设置对应 bit */
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr); /* 设置 pending bit */
/* 如果不在中断上下文中,唤醒 ksoftirqd */
if (!in_interrupt())
wakeup_softirqd();
}
执行流程
c
/* kernel/softirq.c */
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME; /* 最大执行时间 2ms */
int max_restart = MAX_SOFTIRQ_RESTART; /* 最大重启次数 10 */
struct softirq_action *h;
__u32 pending;
pending = local_softirq_pending();
/* 清除 pending 标志,防止重入 */
__local_softirq_pending() = 0;
restart:
h = softirq_vec;
while (pending) {
if (pending & 1) {
h->action(h); /* 执行软中断处理函数 */
}
h++;
pending >>= 1;
}
/* 检查是否有新的软中断产生 */
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() && --max_restart)
goto restart; /* 继续处理 */
wakeup_softirqd(); /* 超时则交给 ksoftirqd 线程 */
}
}
执行时机
┌─────────────────────────────────────────────────────┐
│ 硬中断处理完成(do_IRQ 返回前) │
│ → 检查 local_softirq_pending() │
│ → 若有 pending,调用 __do_softirq() │
├─────────────────────────────────────────────────────┤
│ local_bh_enable() 被调用时 │
│ → 检查并执行 pending 的软中断 │
├─────────────────────────────────────────────────────┤
│ ksoftirqd 内核线程 │
│ → 当软中断处理超时或需要调度时 │
│ → 在进程上下文中执行软中断 │
└─────────────────────────────────────────────────────┘
4.5 软中断的并发特性
| 特性 | 说明 |
|---|---|
| 同类型并发 | 同一类型的软中断可以在不同 CPU 上同时执行 |
| 不同类型并发 | 不同类型的软中断可以并发执行 |
| 同 CPU 串行 | 同一 CPU 上,软中断是串行执行的 |
| 可重入 | 同一类型的软中断处理函数必须是可重入的 |
4.6 注册与使用(仅供内核核心使用)
c
/* 注册软中断(编译时静态注册,通常在子系统初始化时调用) */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
/* 示例:网络子系统初始化时注册 */
static int __init net_dev_init(void)
{
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
return 0;
}
⚠️ 重要 :软中断的注册接口
open_softirq()虽然可用,但不建议驱动程序使用。软中断是为内核核心子系统(网络、块设备、定时器等)设计的,数量有限且不可扩展。驱动开发者应使用 Tasklet、工作队列或中断线程化。
5. Tasklet(小任务)
5.1 概述
Tasklet 是基于软中断实现的下半部机制,是驱动程序中最常用的软中断级下半部方案(在旧内核中)。它解决了软中断的两个主要问题:
- 数量限制:Tasklet 可以动态创建,数量不受限制
- 并发复杂性 :同一 Tasklet 在同一时刻只能在一个 CPU 上执行(串行化保证)
5.2 数据结构
c
/* include/linux/interrupt.h */
struct tasklet_struct
{
struct tasklet_struct *next; /* 链表指针(per-CPU 链表) */
unsigned long state; /* 状态:TASKLET_STATE_SCHED / TASKLET_STATE_RUN */
atomic_t count; /* 引用计数:0=启用,>0=禁用 */
bool use_callback; /* 是否使用callback */
union {
void (*func)(unsigned long data); /* 处理函数(旧版) */
void (*callback)(struct tasklet_struct *t); /* 新版内核(5.9+)使用 callback 风格: */
};
unsigned long data; /* 传递给处理函数的参数 */
};
5.3 实现原理
Tasklet 本质上是对 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两种软中断的封装:
┌──────────────────────────────────────────────────────────┐
│ Tasklet 架构 │
├──────────────────────────────────────────────────────────┤
│ │
│ tasklet_schedule() │
│ │ │
│ ▼ │
│ 将 tasklet 加入 per-CPU 链表 │
│ │ │
│ ▼ │
│ raise_softirq(TASKLET_SOFTIRQ) │
│ │ │
│ ▼ │
│ 软中断执行时调用 tasklet_action() │
│ │ │
│ ▼ │
│ 遍历 per-CPU 链表,逐个执行 tasklet->func() │
│ │
└──────────────────────────────────────────────────────────┘
核心执行函数
c
/* kernel/softirq.c */
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
/* 取出当前 CPU 的 tasklet 链表 */
local_irq_disable();
list = tl_head->head;
tl_head->head = NULL;
tl_head->tail = &tl_head->head;
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
/* 尝试获取执行权(保证同一 tasklet 不会并发执行) */
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) {
/* 执行 tasklet 处理函数(区分新旧 API) */
if (t->use_callback)
t->callback(t);
else
t->func(t->data);
tasklet_unlock(t);
continue;
}
}
tasklet_unlock(t);
}
/* 如果获取失败,重新加入链表等待下次执行 */
local_irq_disable();
t->next = NULL;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
5.4 状态机
tasklet_schedule()
│
▼
┌────────────────────────┐
│ TASKLET_STATE_SCHED │ ← 已调度,等待执行
└────────────┬───────────┘
│ tasklet_action() 获取锁
▼
┌────────────────────────┐
│ TASKLET_STATE_RUN │ ← 正在执行
└────────────┬───────────┘
│ func() 执行完毕
▼
┌────────────────────────┐
│ IDLE │ ← 空闲,可再次调度
└────────────────────────┘
5.5 API 使用
c
#include <linux/interrupt.h>
/* ========== 方式一:静态初始化 ========== */
/* 旧版 API(5.9 之前,func 风格,参数通过 data 传递) */
DECLARE_TASKLET_OLD(my_tasklet, my_tasklet_func, my_data);
/* 新版 API(5.9+,callback 风格,宿主结构体指针用 from_tasklet 获取) */
DECLARE_TASKLET(my_tasklet, my_tasklet_callback);
/* ========== 方式二:动态初始化 ========== */
struct tasklet_struct my_tasklet;
/* 旧版 API(5.9 之前,正被 tasklet_setup 取代) */
tasklet_init(&my_tasklet, my_tasklet_func, (unsigned long)my_data);
/* 新版 API(5.9+,推荐) */
tasklet_setup(&my_tasklet, my_tasklet_callback);
/* ========== 处理函数 ========== */
/* 旧版 */
void my_tasklet_func(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
/* 处理逻辑... */
}
/* 新版(推荐) */
void my_tasklet_callback(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, my_tasklet);
/* 处理逻辑... */
}
/* ========== 调度与控制 ========== */
tasklet_schedule(&my_tasklet); /* 调度执行 */
tasklet_hi_schedule(&my_tasklet); /* 高优先级调度 */
tasklet_disable(&my_tasklet); /* 禁用(count++,等待执行完毕) */
tasklet_enable(&my_tasklet); /* 启用(count--) */
tasklet_kill(&my_tasklet); /* 杀死(等待执行完毕,防止重调度) */
5.6 Tasklet 的特性总结
| 特性 | 说明 |
|---|---|
| 执行上下文 | 软中断上下文(中断上下文) |
| 能否睡眠 | ❌ 不能 |
| 并发性 | 同一 tasklet 不会并发执行(串行化) |
| 不同 tasklet | 不同 tasklet 可以在不同 CPU 上并发执行 |
| 优先级 | 普通(TASKLET_SOFTIRQ)或高(HI_SOFTIRQ) |
| 性能 | 高(仅次于直接使用软中断) |
| 适用场景 | 短小、不可睡眠、需要串行化的下半部任务 |
5.7 Tasklet 的 API 演进与长期趋势
从 Linux 5.9 开始,内核社区对 Tasklet 的初始化 API 进行了现代化改造(并非立即废弃):
- 新 API
tasklet_setup()取代旧的tasklet_init(),回调改为callback(struct tasklet_struct *)风格 - 旧的三参数
DECLARE_TASKLET宏被改名为DECLARE_TASKLET_OLD,逐步停止使用 - 社区存在长期移除 Tasklet 的讨论(如 2024 年 LWN 文章 "The end of tasklets"),但截至 6.x,Tasklet 仍被网络、块设备等核心子系统广泛使用
- 原因:Tasklet 运行在中断上下文,限制较多;但其轻量、不可睡眠的延迟执行语义在部分场景仍有价值
6. 工作队列(Workqueue)
6.1 概述
工作队列是运行在进程上下文 中的下半部机制。与软中断和 Tasklet 不同,工作队列由专用的内核线程(kworker)执行,因此:
- ✅ 可以睡眠
- ✅ 可以调度
- ⚠️ 不能直接访问用户空间:
kworker是内核线程(current->mm == NULL),调用copy_to_user()非法,应通过等待队列/poll 通知用户进程 - ✅ 可以分配
GFP_KERNEL内存 - ❌ 延迟相对较高(需要线程调度)
6.2 架构
┌─────────────────────────────────────────────────────────────┐
│ Workqueue 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ work_struct│ │ work_struct│ │ work_struct│ ← 工作项 │
│ └─────┬────┘ └─────┬────┘ └─────┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ workqueue_struct │ ← 工作队列│
│ │ (链表管理所有 pending work) │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ worker_pool (per-CPU) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ kworker │ │ kworker │ │ kworker │ ... │ ← 内核线程│
│ │ │ /0:0 │ │ /0:1 │ │ /1:0 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
6.3 数据结构
c
/* include/linux/workqueue.h */
struct work_struct {
atomic_long_t data; /* 内部状态数据 */
struct list_head entry; /* 链表节点 */
work_func_t func; /* 工作函数 */
};
typedef void (*work_func_t)(struct work_struct *work);
/* 延迟工作 */
struct delayed_work {
struct work_struct work;
struct timer_list timer; /* 用于延迟执行的定时器 */
struct workqueue_struct *wq;
int cpu;
};
6.4 API 使用
c
#include <linux/workqueue.h>
/* ========== 1. 定义工作项 ========== */
struct my_device {
struct work_struct work; /* 嵌入 work_struct */
struct delayed_work dwork; /* 或延迟工作 */
/* ... 其他成员 ... */
};
/* ========== 2. 工作函数 ========== */
static void my_work_func(struct work_struct *work)
{
/* 通过 container_of 获取宿主结构体 */
struct my_device *dev = container_of(work, struct my_device, work);
/* 可以睡眠! */
msleep(100);
mutex_lock(&dev->lock);
/* 复杂处理逻辑... */
mutex_unlock(&dev->lock);
}
/* ========== 3. 初始化 ========== */
/* 静态初始化 */
DECLARE_WORK(my_work, my_work_func);
/* 动态初始化 */
INIT_WORK(&dev->work, my_work_func);
INIT_DELAYED_WORK(&dev->dwork, my_delayed_work_func);
/* ========== 4. 调度执行 ========== */
/* 使用系统默认工作队列(events) */
schedule_work(&dev->work);
schedule_delayed_work(&dev->dwork, msecs_to_jiffies(100)); /* 延迟 100ms */
/* ========== 5. 创建自定义工作队列 ========== */
struct workqueue_struct *my_wq;
my_wq = create_singlethread_workqueue("my_wq"); /* 单线程 */
/* 或 */
my_wq = alloc_workqueue("my_wq", WQ_UNBOUND | WQ_HIGHPRI, 0); /* 高级配置 */
queue_work(my_wq, &dev->work);
queue_delayed_work(my_wq, &dev->dwork, delay);
/* ========== 6. 同步与取消 ========== */
flush_work(&dev->work); /* 等待工作完成 */
flush_workqueue(my_wq); /* 等待队列中所有工作完成 */
cancel_work_sync(&dev->work); /* 取消并等待完成 */
cancel_delayed_work_sync(&dev->dwork);
/* ========== 7. 销毁 ========== */
destroy_workqueue(my_wq);
6.5 系统默认工作队列
内核预定义了多个全局工作队列:
| 工作队列 | 说明 |
|---|---|
system_wq |
默认工作队列,schedule_work() 使用 |
system_highpri_wq |
高优先级工作队列 |
system_long_wq |
适合长时间运行的工作 |
system_unbound_wq |
不绑定 CPU 的工作队列 |
system_freezable_wq |
系统挂起时可冻结的工作队列 |
6.6 工作队列的标志位
c
enum {
WQ_UNBOUND = 1 << 1, /* 不绑定特定 CPU */
WQ_FREEZABLE = 1 << 2, /* 系统休眠时可冻结 */
WQ_MEM_RECLAIM = 1 << 3, /* 内存回收路径可用 */
WQ_HIGHPRI = 1 << 4, /* 高优先级执行 */
WQ_CPU_INTENSIVE = 1 << 5, /* CPU 密集型,不阻塞其他工作 */
WQ_SYSFS = 1 << 6, /* 在 sysfs 中可见 */
};
6.7 工作队列的特性总结
| 特性 | 说明 |
|---|---|
| 执行上下文 | 进程上下文(内核线程) |
| 能否睡眠 | ✅ 可以 |
| 能否调度 | ✅ 可以 |
| 延迟 | 相对较高(依赖线程调度) |
| 并发性 | 默认 per-CPU 绑定,可配置为 unbound |
| 适用场景 | 需要睡眠、耗时较长、需要进程上下文的下半部任务 |
7. 中断线程化(Threaded IRQ)
7.1 概述
中断线程化是 Linux 2.6.30 引入的机制(由 Thomas Gleixner 主导),它将中断处理函数的一部分或全部放到内核线程中执行。这是 PREEMPT_RT 补丁的核心特性之一,现已合入主线内核。
7.2 设计思想
传统模型:
硬中断上下文 → ISR(不可睡眠)→ 下半部(softirq/tasklet/workqueue)
线程化模型:
硬中断上下文 → 快速 handler(仅确认中断)→ 唤醒内核线程 → 线程中执行 thread_fn(可睡眠)
7.3 注册接口
c
#include <linux/interrupt.h>
/**
* request_threaded_irq - 注册线程化中断
* @irq: 中断号
* @handler: 硬中断处理函数(上半部,在硬中断上下文执行)
* @thread_fn: 线程处理函数(下半部,在内核线程中执行)
* @irqflags: 标志位
* @devname: 设备名
* @dev_id: 设备指针
*
* 返回值:0 成功,负值失败
*/
int request_threaded_irq(unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long irqflags,
const char *devname,
void *dev_id);
7.4 执行流程
┌─────────────────────────────────────────────────────────────────┐
│ 线程化中断执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 硬件中断触发 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ handler()(硬中断上下文) │ │
│ │ • 确认中断 │ │
│ │ • 清除中断源(如需要) │ │
│ │ • 返回 IRQ_WAKE_THREAD │ │
│ └──────────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 唤醒内核线程 irq/%d-%s │ │
│ │ (如:irq/32-eth0) │ │
│ └──────────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ thread_fn()(进程上下文) │ │
│ │ • 可以睡眠 ✅ │ │
│ │ • 可以调度 ✅ │ │
│ │ • 不能直接访问用户空间 ❌ │ │
│ │ • 执行耗时的中断处理逻辑 │ │
│ │ • 返回 IRQ_HANDLED / IRQ_NONE │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.5 代码示例
c
#include <linux/interrupt.h>
#include <linux/module.h>
struct my_device {
int irq;
void __iomem *base;
struct mutex lock;
u32 rx_count;
};
/*
* 硬中断处理函数(上半部)
* 运行在硬中断上下文,必须快速
*/
static irqreturn_t my_hardirq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
u32 status;
status = readl(dev->base + REG_INT_STATUS);
if (!status)
return IRQ_NONE;
/* 清除中断源 */
writel(status, dev->base + REG_INT_CLEAR);
/* 返回 IRQ_WAKE_THREAD 表示需要执行线程化处理 */
return IRQ_WAKE_THREAD;
}
/*
* 线程化处理函数(下半部)
* 运行在内核线程中,可以睡眠
*/
static irqreturn_t my_threaded_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* 可以睡眠! */
mutex_lock(&dev->lock);
/* 复杂的数据处理 */
dev->rx_count++;
/* 可以调用 kmalloc(GFP_KERNEL)、msleep() 等(copy_to_user() 不可用) */
mutex_unlock(&dev->lock);
return IRQ_HANDLED;
}
static int my_probe(struct platform_device *pdev)
{
struct my_device *dev;
int ret;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
dev->irq = platform_get_irq(pdev, 0);
/* 注册线程化中断 */
ret = devm_request_threaded_irq(&pdev->dev,
dev->irq,
my_hardirq_handler, /* 硬中断 handler */
my_threaded_handler, /* 线程化 handler */
IRQF_ONESHOT, /* 重要标志! */
"my_device",
dev);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
return 0;
}
7.6 关键标志:IRQF_ONESHOT
使用线程化中断时,必须 (在大多数情况下)设置 IRQF_ONESHOT 标志:
c
#define IRQF_ONESHOT 0x00002000
原因 :在线程化中断中,硬中断 handler 返回后中断线不会被立即重新使能(因为线程还在处理中)。IRQF_ONESHOT 告诉内核:在线程处理完成之前,保持中断线禁用,防止中断重入。
7.7 handler 为 NULL 的情况
如果不需要硬中断 handler(例如中断控制器已经处理了确认工作),可以将 handler 设为 NULL:
c
ret = request_threaded_irq(irq, NULL, my_thread_fn,
IRQF_ONESHOT, "my_dev", dev);
此时内核会使用默认的 irq_default_primary_handler(),它直接返回 IRQ_WAKE_THREAD。
7.8 线程化中断的内核线程
每个线程化中断会创建一个专用的内核线程:
bash
$ ps -eLf | grep irq
root 1234 2 0 irq/32-eth0 # 中断号32,设备名eth0
root 1235 2 0 irq/33-i2c0
root 1236 2 0 irq/45-gpio-keys
可以通过 /proc/interrupts 查看线程化中断的信息。
7.9 线程化中断的特性总结
| 特性 | 说明 |
|---|---|
| 执行上下文 | 进程上下文(专用内核线程) |
| 能否睡眠 | ✅ 可以 |
| 能否调度 | ✅ 可以(线程有优先级,可被调度) |
| 延迟 | 中等(比工作队列低,比软中断高) |
| 实时性 | 可设置线程优先级(SCHED_FIFO),适合 RT 场景 |
| 适用场景 | 需要进程上下文、对实时性有要求的中断处理 |
8. 各机制对比与选型指南
8.1 综合对比表
| 特性 | 软中断 (Softirq) | Tasklet | 工作队列 (Workqueue) | 中断线程化 (Threaded IRQ) |
|---|---|---|---|---|
| 执行上下文 | 中断上下文 | 中断上下文 | 进程上下文 | 进程上下文 |
| 能否睡眠 | ❌ | ❌ | ✅ | ✅ |
| 能否调度 | ❌ | ❌ | ✅ | ✅ |
| 执行延迟 | 极低 | 低 | 中等 | 中等 |
| 并发模型 | 同类型可并发 | 同实例串行 | 可配置 | 单线程串行 |
| 数量限制 | 固定(10个) | 无限制 | 无限制 | 每个 IRQ 一个 |
| 使用者 | 内核核心子系统 | 驱动程序(旧) | 驱动程序/子系统 | 驱动程序 |
| 优先级控制 | 固定 | 高/普通两级 | 可配置 | 可设置 RT 优先级 |
| 性能 | ★★★★★ | ★★★★☆ | ★★★☆☆ | ★★★☆☆ |
| 灵活性 | ★☆☆☆☆ | ★★★☆☆ | ★★★★★ | ★★★★☆ |
8.2 选型决策树
需要在中断后延迟处理任务?
│
├─ 你在编写内核核心子系统(网络/块设备/定时器等)?
│ └─ YES → 使用 Softirq(仅限内核核心,驱动不能新增软中断)
│
├─ 任务需要睡眠/可能阻塞?
│ │
│ ├─ 由本设备中断直接触发,对延迟/实时性要求高?
│ │ └─ YES → 使用中断线程化(Threaded IRQ)
│ │
│ └─ 对延迟要求一般,或需从任意上下文调度?
│ └─ YES → 使用工作队列(Workqueue)
│
├─ 任务不需要睡眠,且要求低延迟?
│ │
│ ├─ 需要同一实例串行化?
│ │ └─ YES → Tasklet(旧内核);新内核优先中断线程化(天然串行),
│ │ 其次工作队列(同一 work 项天然不重入)
│ │
│ └─ 不需要串行化?
│ └─ 中断线程化或工作队列(软中断级延迟不对驱动开放)
│
└─ 不确定?
└─ 优先选择工作队列(最通用、最安全)
⚠️ 关于"访问用户空间" :无论工作队列还是中断线程化,下半部都运行在内核线程
上下文(
current->mm == NULL),不能直接调用copy_to_user()/copy_from_user()。需要与用户空间交换数据时,应通过等待队列/poll 唤醒用户进程,由系统调用路径完成拷贝。
内核线程确需访问用户内存时,必须先用
kthread_use_mm()借用进程地址空间(如 USB Function FS 驱动的做法)。
8.3 现代内核的推荐实践
| 场景 | 推荐方案 |
|---|---|
| 新驱动开发 | 中断线程化 或 工作队列 |
| 需要睡眠的操作(I2C/SPI 通信、内存分配) | 工作队列 或 中断线程化 |
| 高频短任务(网络收包) | Softirq(内核已实现) |
| 旧驱动维护 | Tasklet 仍可使用,但新代码避免使用 |
| PREEMPT_RT 实时系统 | 中断线程化(几乎所有中断都被线程化) |
9. 实战代码示例
9.1 完整驱动示例:对比三种下半部机制
c
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/slab.h>
#include <linux/io.h>
#define REG_INT_STATUS 0x00
#define REG_INT_CLEAR 0x04
#define REG_DATA 0x08
struct demo_device {
struct device *dev;
void __iomem *base;
int irq;
/* 方式一:Tasklet */
struct tasklet_struct tasklet;
/* 方式二:工作队列 */
struct work_struct work;
struct workqueue_struct *wq;
/* 方式三:中断线程化(无需额外结构) */
/* 共享数据 */
spinlock_t lock;
u32 rx_data;
u32 rx_count;
};
/* ==================== 方式一:Tasklet ==================== */
static void demo_tasklet_func(struct tasklet_struct *t)
{
struct demo_device *demo = from_tasklet(demo, t, tasklet);
unsigned long flags;
spin_lock_irqsave(&demo->lock, flags);
demo->rx_count++;
/* 注意:这里不能睡眠! */
dev_dbg(demo->dev, "Tasklet: processed %u packets\n", demo->rx_count);
spin_unlock_irqrestore(&demo->lock, flags);
}
static irqreturn_t demo_isr_tasklet(int irq, void *dev_id)
{
struct demo_device *demo = dev_id;
u32 status;
status = readl(demo->base + REG_INT_STATUS);
if (!status)
return IRQ_NONE;
writel(status, demo->base + REG_INT_CLEAR);
demo->rx_data = readl(demo->base + REG_DATA);
/* 调度 tasklet */
tasklet_schedule(&demo->tasklet);
return IRQ_HANDLED;
}
/* ==================== 方式二:工作队列 ==================== */
static void demo_work_func(struct work_struct *work)
{
struct demo_device *demo = container_of(work, struct demo_device, work);
unsigned long flags;
/* 进程上下文,可以睡眠,但持锁期间不能睡眠 */
spin_lock_irqsave(&demo->lock, flags);
demo->rx_count++;
dev_dbg(demo->dev, "Workqueue: processed %u packets\n", demo->rx_count);
spin_unlock_irqrestore(&demo->lock, flags);
/* 模拟耗时操作(睡眠必须放在锁外) */
msleep(10); /* 合法! */
}
static irqreturn_t demo_isr_workqueue(int irq, void *dev_id)
{
struct demo_device *demo = dev_id;
u32 status;
status = readl(demo->base + REG_INT_STATUS);
if (!status)
return IRQ_NONE;
writel(status, demo->base + REG_INT_CLEAR);
demo->rx_data = readl(demo->base + REG_DATA);
/* 调度工作队列 */
queue_work(demo->wq, &demo->work);
return IRQ_HANDLED;
}
/* ==================== 方式三:中断线程化 ==================== */
static irqreturn_t demo_hardirq(int irq, void *dev_id)
{
struct demo_device *demo = dev_id;
u32 status;
status = readl(demo->base + REG_INT_STATUS);
if (!status)
return IRQ_NONE;
writel(status, demo->base + REG_INT_CLEAR);
return IRQ_WAKE_THREAD; /* 唤醒线程 */
}
static irqreturn_t demo_thread_fn(int irq, void *dev_id)
{
struct demo_device *demo = dev_id;
/* 进程上下文,可以睡眠 */
demo->rx_data = readl(demo->base + REG_DATA);
demo->rx_count++;
dev_dbg(demo->dev, "Threaded IRQ: processed %u packets\n", demo->rx_count);
/* 可以执行任何可睡眠的进程上下文操作 */
// kmalloc(size, GFP_KERNEL);
// msleep(...);
/* copy_to_user() 不可用:中断线程无用户地址空间,需经等待队列/poll 转交 */
return IRQ_HANDLED;
}
/* ==================== Probe / Remove ==================== */
static int demo_probe(struct platform_device *pdev)
{
struct demo_device *demo;
int ret;
demo = devm_kzalloc(&pdev->dev, sizeof(*demo), GFP_KERNEL);
if (!demo)
return -ENOMEM;
demo->dev = &pdev->dev;
demo->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(demo->base))
return PTR_ERR(demo->base);
demo->irq = platform_get_irq(pdev, 0);
if (demo->irq < 0)
return demo->irq;
spin_lock_init(&demo->lock);
platform_set_drvdata(pdev, demo);
/* --- 选择一种方式注册中断 --- */
/* 方式一:Tasklet */
tasklet_setup(&demo->tasklet, demo_tasklet_func);
ret = devm_request_irq(&pdev->dev, demo->irq,
demo_isr_tasklet, 0, "demo_tasklet", demo);
/* 方式二:工作队列 */
// demo->wq = alloc_workqueue("demo_wq", WQ_HIGHPRI, 0);
// INIT_WORK(&demo->work, demo_work_func);
// ret = devm_request_irq(&pdev->dev, demo->irq,
// demo_isr_workqueue, 0, "demo_wq", demo);
/* 方式三:中断线程化 */
// ret = devm_request_threaded_irq(&pdev->dev, demo->irq,
// demo_hardirq, demo_thread_fn,
// IRQF_ONESHOT, "demo_threaded", demo);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ: %d\n", ret);
return ret;
}
dev_info(&pdev->dev, "Demo device probed successfully\n");
return 0;
}
static void demo_remove(struct platform_device *pdev)
{
struct demo_device *demo = platform_get_drvdata(pdev);
tasklet_kill(&demo->tasklet);
/* 或 */
// cancel_work_sync(&demo->work);
// destroy_workqueue(demo->wq);
dev_info(&pdev->dev, "Demo device removed\n");
}
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "demo-device",
},
};
module_platform_driver(demo_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Demo: Bottom Half Mechanisms Comparison");
9.2 查看系统中断信息
bash
# 查看中断统计
$ cat /proc/interrupts
CPU0 CPU1
0: 42 0 GICv2 27 Level arch_timer
32: 156 89 GICv2 44 Level eth0
33: 12 0 GICv2 45 Level i2c0
# 查看软中断统计
$ cat /proc/softirqs
CPU0 CPU1
HI: 0 0
TIMER: 1234 1198
NET_TX: 45 32
NET_RX: 5678 5432
BLOCK: 123 119
IRQ_POLL: 0 0
TASKLET: 89 76
SCHED: 456 432
HRTIMER: 12 10
RCU: 2345 2298
# 查看中断线程
$ ps -eLf | grep "irq/"
root 1234 2 0 irq/32-eth0
root 1235 2 0 irq/33-i2c0
# 查看 ksoftirqd 线程
$ ps -eLf | grep ksoftirqd
root 10 2 0 ksoftirqd/0
root 11 2 0 ksoftirqd/1
# 查看 kworker 线程
$ ps -eLf | grep kworker
root 100 2 0 kworker/0:1-events
root 101 2 0 kworker/1:0-events
10. 总结
10.1 核心要点回顾
-
上半部/下半部分离是 Linux 中断处理的核心设计模式,目的是在保证快速响应的同时完成复杂的后续处理。
-
软中断是最底层的下半部机制,性能最高但灵活性最低,仅供内核核心子系统使用。
-
Tasklet 是软中断的封装,提供串行化保证和动态创建能力,但运行在中断上下文,不可睡眠。新内核(5.9+)对其 API 做了现代化改造,社区有长期移除的讨论,目前仍被广泛使用。
-
工作队列运行在进程上下文,是最灵活的下半部机制,可以睡眠、调度(但不能直接访问用户空间),适合大多数驱动场景。
-
中断线程化将中断处理直接放入内核线程,兼具低延迟和可睡眠的优势,是 PREEMPT_RT 实时系统的基石,也是现代驱动开发的推荐方案。
10.2 一句话总结
能快则快(Softirq/Tasklet),需睡则睡(Workqueue/Threaded IRQ),新代码优先选择中断线程化或工作队列。
本文基于 Linux Kernel 5.x/6.x 版本撰写,部分 API 在更新版本中可能有变化,请以实际内核源码为准。