中断可以分成两个部分,顶半部和底半部

一、中断为何分为两部分
进入中断处理函数时,CPU 通常处于关闭中断(或屏蔽同级中断)的状态。
这意味着:
其他设备的中断进不来
系统调度被暂停
整个系统的响应速度会急剧下降
如果你的中断处理函数里:
循环处理大量数据
延时等待
打印一堆 log
做复杂计算
那系统就会变卡、丢数据,甚至看起来像 "死机"。
所以内核设计者提出一个原则:
中断处理必须快进快出,不能耗时。
但现实是:
很多设备确实需要快速响应硬件,又需要做一些耗时的后续处理。
矛盾怎么解决?
于是就有了:上半部 + 下半部 的分离设计。
二、顶半部
顶半部 = 硬件中断处理函数本身。
顶半部只负责 "紧急且快速" 的事,其余一律甩给下半部
它的特点:
执行时机最紧急
中断上下文
不能睡眠、不能阻塞
必须极快完成
它只适合干三件事:
1、读取硬件状态 / 清除中断标志
告诉硬件 "中断我收到了,你别再重复触发"。
2、做必须立即完成的极简操作
比如简单寄存器读写、标记事件发生。
3、启动底半部
把真正耗时的工作丢出去。
三、底半部
底半部 = 被顶半部调度出来的、延后执行的处理流程。
下半部处理不紧急、但耗时间的逻辑。
它的特点:
不那么紧急
执行时中断已经打开
可以被新中断打断
部分机制允许睡眠(比如 workqueue)
适合干这些事:
数据拷贝、数据解析
网络包 / 串口数据处理
大量打印日志
控制外设的慢操作
上报数据到应用层
补充:同步调用:事情要一件一件做完,不做完就不往下走
异步调用:不用等,你先去干别的,等好了通知你
因此底半部是异步调用的
四、顶半部vs底半部
| 对比项 | 上半部 top half | 下半部 bottom half |
|---|---|---|
| 执行时机 | 中断触发立即执行 | 延后执行 |
| 中断状态 | 关中断 / 屏蔽中断 | 开中断,可被新中断打断 |
| 上下文 | 中断上下文 | 软中断 / 进程上下文 |
| 能否睡眠 | 绝对不能 | workqueue 可以睡,tasklet 不行 |
| 执行速度 | 必须极快 | 可以适当耗时 |
| 主要任务 | 应答硬件、清中断、调度下半部 | 真正的数据处理、业务逻辑 |
五、Linux 中有哪些下半部实现
1. tasklet(小任务)(做不耗时任务)
是一种软中断
1、特点
也要较短小
基于软中断
不能睡眠
执行快、轻量
适合简单、快速的延后处理
同一个 tasklet 不会在多个 CPU 上同时跑
2、适用场景
快速、简单、非阻塞的延后工作
不需要睡眠
中断后马上要处理的轻量任务
3、使用步骤
- 定义 tasklet 结构体
- 写 tasklet 回调函数
- 初始化 tasklet
- 在中断上半部调度 tasklet
2. workqueue(工作队列)(做耗时任务)
就是一个普通的内核线程
1、特点
运行在内核线程上下文
可以睡眠、可以阻塞
写驱动最安全、最常用
适合稍微复杂的处理逻辑
2、适用场景
需要延时
需要睡眠
需要复杂处理
需要操作 I2C、SPI、GPIO 等慢设备
3、使用步骤
定义 work_struct
写工作函数
初始化 work
在上半部调度
3、二者对比
| 机制 | 适用场景 | 核心特性 | 你的选择建议 |
|---|---|---|---|
| Tasklet | 轻量级延时任务 | 基于软中断,不能睡眠,执行效率极高 | 处理速度极快、短小的任务 |
| Workqueue | 重量级延后处理 | 运行在内核线程上下文 ,可以睡眠 | 最常用!适合读写文件、复杂逻辑、延时操作 |
| 对比项 | tasklet | workqueue |
|---|---|---|
| 运行环境 | 软中断上下文 | 进程上下文 |
| 能否睡眠 | 不能 | 可以 |
| 能否阻塞 | 不能 | 可以 |
| 执行速度 | 很快 | 稍慢 |
| 安全性 | 一般 | 非常安全 |
| 抢占 | 不可抢占 | 可被抢占 |
| 驱动推荐 | 轻量任务 | 绝大多数驱动场景 |
| 最典型用途 | 快速清标志、简单处理 | 数据处理、延时、设备操作 |
六、完整驱动代码(上半部 + tasklet + workqueue)
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
/* 本驱动演示:
* 中断上半部 → 快速处理、清中断
* tasklet → 轻量级下半部,不能睡眠
* workqueue → 通用下半部,可以睡眠
*/
//=============================
// 1. 定义 tasklet(软中断下半部)
//=============================
struct tasklet_struct my_tasklet;
void tasklet_func(unsigned long data)
{
printk("=== 下半部:tasklet 执行(不能睡眠)===\n");
}
//=============================
// 2. 定义 workqueue(可睡眠下半部)
//=============================
struct work_struct my_work;
void work_func(struct work_struct *work)
{
printk("=== 下半部:workqueue 执行(可以睡眠)===\n");
// workqueue 可以睡眠!tasklet 绝对不能这么写!
msleep(50);
}
//=============================
// 3. 中断上半部(中断处理函数)
//=============================
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
printk("\n★ 进入中断上半部:快速处理\n");
// 1. 清中断(硬件操作,必须快)
// 这里省略硬件寄存器操作...
// 2. 调度 tasklet 下半部
tasklet_schedule(&my_tasklet);
// 3. 调度 workqueue 下半部
schedule_work(&my_work);
printk("★ 退出中断上半部\n");
return IRQ_HANDLED;
}
//=============================
// 4. 驱动入口(初始化)
//=============================
static int __init my_drv_init(void)
{
int irq;
// 初始化 tasklet
tasklet_init(&my_tasklet, tasklet_func, 0);
// 初始化 workqueue
INIT_WORK(&my_work, work_func);
// 模拟获取一个中断号(真实驱动从设备树获取)
// 这里假设你用的是 GPIO 中断
irq = gpio_to_irq(101); // 101 是示例 GPIO
// 注册中断
request_irq(irq, my_irq_handler, IRQF_TRIGGER_RISING,
"my_irq", NULL);
printk("=== 中断驱动初始化完成 ===\n");
return 0;
}
//=============================
// 驱动出口
//=============================
static void __exit my_drv_exit(void)
{
int irq = gpio_to_irq(101);
free_irq(irq, NULL);
tasklet_kill(&my_tasklet);
printk("=== 驱动卸载 ===\n");
}
module_init(my_drv_init);
module_exit(my_drv_exit);
MODULE_LICENSE("GPL");
七、补充
1、中断上下文(不可被打断)
不可打断,不做休眠,不做耗时任务,不被操作系统调度
这里的不可被打断是同级之间的,同级之间不可打断,高优先级可以打断
中断上下文是 CPU 响应硬件中断后进入的执行环境,它并非属于任何一个用户进程或内核进程,没有对应的进程控制块(task_struct),也没有独立的进程 ID。当硬件触发中断时,CPU 会立即暂停当前正在执行的任务,跳转到中断处理函数(即中断上半部),此时就进入了中断上下文;此外,基于软中断实现的 tasklet,其执行环境也属于中断上下文的范畴。中断上下文的核心要求是 "快进快出",因为在这种上下文下,CPU 会屏蔽同级或更低优先级的中断,若执行时间过长,会导致系统调度暂停、其他设备中断无法响应,甚至引发系统卡顿或崩溃。同时,中断上下文有严格的限制:绝对不能睡眠、不能进行进程调度,也不能调用任何可能导致阻塞的函数(如 msleep、mutex_lock、copy_from_user 等),而且其栈空间非常有限,不能进行递归调用或定义过大的数组,只能执行最紧急、最简短的操作,比如清除中断标志、读取硬件寄存器状态,以及调度下半部任务,完成后立即退出,恢复系统正常运行。
包含:中断服务程序/软中断/tasklet
2、进程上下文(可以被打断)
可被打断、可休眠、可阻塞、可做耗时任务、可被操作系统调度
workqueue属于进程上下文
进程上下文是 CPU 执行用户进程、内核线程或驱动初始化 / 卸载函数时所处的执行环境,它明确属于某个具体的进程或内核线程,拥有完整的进程控制块,有独立的进程 ID。无论是用户程序调用系统调用(如 open、read、write)进入内核,还是内核线程(比如 workqueue 对应的内核线程)执行任务,或是驱动的 init、exit 函数运行时,都处于进程上下文。与中断上下文不同,进程上下文没有 "必须快速执行" 的强制要求,支持进程调度和睡眠 ------ 内核可以根据调度策略,将当前进程切换出去,执行其他优先级更高的任务,待条件满足后再切换回来继续执行。因此,在进程上下文下,我们可以调用绝大多数内核 API,包括需要延时的 msleep、用于同步的互斥锁、用于数据拷贝的 copy_from_user/copy_to_user,以及等待队列等,执行时间也可以相对较长,能够处理复杂的数据解析、外设慢操作等耗时任务。对于驱动开发而言,workqueue 之所以能支持睡眠和阻塞操作,核心就是因为它运行在进程上下文(内核线程)中,这也是它成为驱动开发中最常用下半部机制的关键原因。
包含:open/read/write等等系统调用
3、硬中断
硬中断是可以打断tasklet的
在 Linux 内核架构中,硬中断对应的就是中断上半部,运行在严格的中断上下文环境中。为了不阻塞整个系统,硬中断处理有非常严格的限制:执行时间必须极短,不能睡眠、不能阻塞、不能进行耗时操作,核心任务只包括快速应答硬件、清除中断标志,避免硬件持续触发中断,然后将复杂、耗时的处理逻辑交给软中断、tasklet 或 workqueue 等下半部机制去异步执行。