目录
- 为什么中断必须分顶半部与底半部?
- [顶半部与底半部 核心区别与分工](#顶半部与底半部 核心区别与分工)
- [底半部的两种主流实现:tasklet VS 工作队列](#底半部的两种主流实现:tasklet VS 工作队列)
- 终极对比表:12个维度全面对比
- 适用场景与选择原则(开发必背)
- 完整代码示例:标准中断驱动写法
- 常见误区澄清
- 核心知识点汇总
1. 为什么中断必须分顶半部与底半部?
中断处理函数有一个不可妥协的铁律 :
必须快!越快越好!
因为中断上下文是整个系统优先级最高的执行环境,一旦进入中断处理函数:
- 整个系统的所有其他代码都会被暂停
- 同优先级和更低优先级的中断无法响应
- 执行时间过长会导致后续中断丢失、外设数据丢包
- 严重时会触发看门狗复位,导致系统重启
但在实际硬件处理中,任何一个中断的完整处理流程都包含两类完全不同的工作:
- 必须立刻做的紧急工作:如清除硬件中断标志(不清除会无限触发中断)、读取硬件寄存器中的原始数据(不读会被下一次数据覆盖)
- 可以延后做的非紧急工作:如数据解析、日志打印、状态更新、业务逻辑处理
为了解决"中断必须快"和"处理需要时间"的矛盾,Linux内核将中断处理拆分为两个阶段:
- 顶半部(上半部) :快速处理紧急工作,运行在中断上下文
- 底半部(下半部) :延时处理非紧急工作,运行在可调度上下文
用公司紧急会议类比理解
- 顶半部:10分钟紧急故障排查会,只做最紧急的事:确认故障现象、记录关键数据、通知相关人员
- 底半部:会议结束后的后续工作:写故障报告、修复代码、发布补丁、复盘总结
这样拆分的好处是:
- 紧急会议(顶半部)很短,不会耽误大家太多时间
- 耗时的修复工作(底半部)可以在不影响正常工作的情况下慢慢做
- 整个公司(系统)的运转不会因为一次故障而完全停滞
2. 顶半部与底半部 核心区别与分工
2.1 核心区别对比表
| 对比项 | 顶半部(上半部) | 底半部(下半部) |
|---|---|---|
| 运行环境 | 中断上下文 | 软中断上下文(tasklet)/ 内核线程上下文(工作队列) |
| 能否睡眠 | ❌ 绝对不能 | ❌ 不能(tasklet) / ✅ 可以(工作队列) |
| 执行速度要求 | 必须极快(微秒级) | 可稍慢(毫秒级) |
| 主要职责 | 清中断标志、读取硬件数据、调度底半部 | 数据处理、日志打印、状态更新、业务逻辑 |
| 实现方式 | 中断处理函数 | tasklet / 工作队列 / 软中断 |
2.2 顶半部的铁律
顶半部是中断处理中最关键的部分,必须严格遵守以下规则:
- 只做最紧急、必须立刻做的事
- 绝对不能睡眠、不能阻塞
- 绝对不能执行任何耗时操作
- 执行时间越短越好,最好控制在100微秒以内
2.3 底半部的核心价值
底半部的存在就是为了承接顶半部中所有不紧急但又必须做的工作,让顶半部能够尽快返回,恢复系统的正常运行。
一句话总结两者的分工:
顶半部只做紧急必须做的;底半部做剩下所有不紧急但耗时的。
3. 底半部的两种主流实现:tasklet VS 工作队列
Linux内核提供了多种底半部实现机制,其中最常用的是tasklet 和工作队列(Workqueue) 。很多新手会误以为"底半部只能用tasklet",这是一个非常常见的误区。实际上,工作队列是现代内核最推荐的通用底半部方案。
3.1 底层原理深度解析
tasklet:基于软中断的轻量级实现
tasklet是对内核软中断的轻量级封装。内核预先定义了10个软中断,其中两个专门用于tasklet:
TASKLET_SOFTIRQ:普通优先级taskletHI_SOFTIRQ:高优先级tasklet
所有tasklet都挂在这两个软中断的全局链表上。当你调用tasklet_schedule()时,内核会把这个tasklet加入到对应CPU的链表中。在中断返回前,内核会检查软中断链表,依次执行所有待处理的tasklet。
核心特性:
- 运行在软中断上下文,不属于任何进程
- 一旦开始执行,就会一直执行到完成,不会被其他进程抢占
- 同一个tasklet永远只会在一个CPU上执行,不会在多核并行
- 绝对不能睡眠,不能调用任何阻塞函数
工作队列:基于内核线程的通用实现
工作队列是内核创建的一组通用内核线程池 。在现代内核中,每个CPU都有一个对应的kworker内核线程(如kworker/0:0、kworker/1:0)。
当你调用schedule_work()时,内核会把这个工作加入到某个kworker的任务队列中。kworker线程在进程上下文中运行,会被内核调度器正常调度,依次执行队列中的所有工作。
核心特性:
- 运行在内核线程上下文 ,属于
kworker进程 - 执行过程中可以被更高优先级的进程抢占
- 同一个工作可以在多个CPU上并行执行
- 可以做任何事,可以睡眠、可以调用任何阻塞函数
4. 终极对比表:12个维度全面对比
| 对比项 | tasklet | 工作队列(Workqueue) |
|---|---|---|
| 底层实现机制 | 基于软中断 | 基于内核线程池 |
| 运行上下文 | 软中断上下文 | 内核线程上下文(进程上下文) |
| 能否睡眠 | ❌ 绝对不能 | ✅ 完全可以 |
| 能否被抢占 | ❌ 不能 | ✅ 可以 |
| 执行时机 | 中断返回前立即执行 | 内核调度器调度执行(有微小延迟) |
| 典型延迟 | 微秒级(<100μs) | 毫秒级(1-2ms) |
| 能否调用阻塞函数 | ❌ 不可以 | ✅ 可以 |
能否用GFP_KERNEL分配内存 |
❌ 不可以 | ✅ 可以 |
能否用msleep/mutex |
❌ 不可以 | ✅ 可以 |
| 多核并行性 | 同一个tasklet不会多核并行 | 同一个工作可以在多核并行 |
| 内核开销 | 极小 | 较小 |
| 内核推荐度 | 特定场景使用 | 通用推荐 |
5. 适用场景与选择原则(开发必背)
5.1 选择口诀(记住这一句就够了)
能不用tasklet就不用tasklet,除非你明确知道为什么必须用它。
5.2 必须用tasklet的场景
只有当你的底半部满足所有以下条件时,才应该使用tasklet:
- 执行时间极短:通常不超过100微秒
- 绝对无阻塞:不需要等待任何硬件或软件资源
- 延迟要求极高:必须在中断触发后立即执行
- 不需要睡眠:不需要调用任何可能睡眠的函数
典型应用场景:
- 网卡驱动:顶半部读取数据包,tasklet快速解析头部并交给网络协议栈
- 高速串口驱动:顶半部读取串口数据,tasklet将数据放入环形缓冲区
- GPIO按键驱动:顶半部读取按键状态,tasklet上报输入事件(无消抖需求时)
5.3 必须用工作队列的场景
只要你的底半部满足任何一个以下条件,就必须使用工作队列:
- 需要睡眠 :需要调用
msleep、wait_event、schedule_timeout等函数 - 需要互斥 :需要用
mutex_lock保护共享资源 - 需要内存分配 :需要用
kmalloc(GFP_KERNEL)分配内存 - 执行时间较长:超过1毫秒的操作
- 需要用户态交互 :需要通过
copy_to_user/copy_from_user和用户态通信 - 需要复杂数据处理:需要进行大量计算或数据转换
典型应用场景:
- 块设备驱动:处理磁盘IO请求,需要等待硬件完成
- USB驱动:处理USB传输,需要等待设备响应
- 传感器驱动:读取传感器数据,需要等待AD转换完成
- 按键驱动:需要进行按键消抖(必须睡眠10-20ms)
- 任何需要复杂业务逻辑的驱动
6. 完整代码示例:标准中断驱动写法
6.1 通用设备树节点
dts
demo_device {
compatible = "demo,irq";
interrupts = <0 0 0>; // 根据你的硬件实际中断号修改
status = "okay";
};
6.2 标准顶半部 + tasklet底半部 代码
这是最经典的中断驱动写法,适合短耗时、无睡眠需求的场景:
c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/io.h>
// 设备私有数据结构体
struct demo_dev {
int irq;
void __iomem *reg_base; // 寄存器基地址
struct tasklet_struct tlet;
u32 hw_data; // 保存从硬件读取的数据
};
static struct demo_dev *dev;
// ==============================================
// 底半部:tasklet 处理函数
// ==============================================
static void demo_tasklet(unsigned long data)
{
struct demo_dev *dev = (struct demo_dev *)data;
printk("=== 底半部执行:数据处理、日志打印、耗时操作 ===\n");
printk("从硬件读取的数据:0x%08x\n", dev->hw_data);
// 这里可以做:
// 数据解析、状态更新、日志、上报事件
// 但仍然不能睡眠!不能调用msleep、mutex_lock等
}
// ==============================================
// 顶半部:中断处理函数(越快越好)
// ==============================================
static irqreturn_t demo_irq_handler(int irq, void *dev_id)
{
struct demo_dev *dev = dev_id;
// 1. 清中断标志(必须!否则会无限触发中断)
writel(0x01, dev->reg_base + 0x04); // 假设0x04是中断清除寄存器
// 2. 读取硬件数据(必须!否则会被下一次数据覆盖)
dev->hw_data = readl(dev->reg_base + 0x00); // 假设0x00是数据寄存器
// 3. 调度底半部(tasklet)
tasklet_schedule(&dev->tlet);
printk("顶半部:中断处理完成 → 调度底半部\n");
return IRQ_HANDLED;
}
// ==============================================
// 驱动 probe:注册中断 + 初始化 tasklet
// ==============================================
static int demo_probe(struct platform_device *pdev)
{
struct resource *mem_res;
int irq, ret;
// 分配设备结构体
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) {
return -ENOMEM;
}
dev_set_drvdata(&pdev->dev, dev);
// 1. 映射寄存器地址
mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!mem_res) {
dev_err(&pdev->dev, "获取内存资源失败\n");
return -ENODEV;
}
dev->reg_base = devm_ioremap_resource(&pdev->dev, mem_res);
if (IS_ERR(dev->reg_base)) {
return PTR_ERR(dev->reg_base);
}
// 2. 获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "获取中断号失败\n");
return irq;
}
dev->irq = irq;
// 3. 初始化 tasklet
tasklet_init(&dev->tlet, demo_tasklet, (unsigned long)dev);
// 4. 注册中断(顶半部)
ret = devm_request_irq(&pdev->dev, irq, demo_irq_handler,
IRQF_TRIGGER_RISING, "demo_irq", dev);
if (ret) {
dev_err(&pdev->dev, "注册中断失败\n");
return ret;
}
printk("=== 驱动 probe 成功,中断注册完成 ===\n");
return 0;
}
// ==============================================
// 驱动 remove:清理资源
// ==============================================
static int demo_remove(struct platform_device *pdev)
{
struct demo_dev *dev = dev_get_drvdata(&pdev->dev);
// 销毁 tasklet,等待正在执行的tasklet完成
tasklet_kill(&dev->tlet);
printk("驱动移除,tasklet 销毁\n");
return 0;
}
// ==============================================
// 设备树匹配表
// ==============================================
static const struct of_device_id demo_ids[] = {
{ .compatible = "demo,irq" },
{}, // 必须以空结构体结尾
};
MODULE_DEVICE_TABLE(of, demo_ids);
// ==============================================
// platform 驱动结构体
// ==============================================
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "irq_demo",
.of_match_table = demo_ids,
},
};
module_platform_driver(demo_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver");
MODULE_DESCRIPTION("标准中断驱动示例(顶半部+tasklet底半部)");
MODULE_VERSION("1.0");
6.3 工作队列版本底半部代码
如果你的底半部需要睡眠(比如按键消抖),只需要把tasklet替换成工作队列即可:
c
// 设备私有数据结构体
struct demo_dev {
int irq;
void __iomem *reg_base;
struct work_struct work; // 替换tasklet
u32 hw_data;
};
// 底半部:工作队列处理函数
static void demo_work(struct work_struct *work)
{
struct demo_dev *dev = container_of(work, struct demo_dev, work);
// 这里可以安全地睡眠!
msleep(10); // 例如:按键消抖
printk("=== 工作队列底半部执行 ===\n");
printk("从硬件读取的数据:0x%08x\n", dev->hw_data);
}
// 顶半部:中断处理函数
static irqreturn_t demo_irq_handler(int irq, void *dev_id)
{
struct demo_dev *dev = dev_id;
writel(0x01, dev->reg_base + 0x04);
dev->hw_data = readl(dev->reg_base + 0x00);
// 调度工作队列
schedule_work(&dev->work);
printk("顶半部:中断处理完成 → 调度工作队列\n");
return IRQ_HANDLED;
}
// probe函数中初始化工作
INIT_WORK(&dev->work, demo_work);
// remove函数中取消工作
cancel_work_sync(&dev->work);
7. 常见误区澄清
❌ 误区1:底半部只能用tasklet
错误。工作队列是现在最主流的底半部实现,绝大多数驱动都使用工作队列。
❌ 误区2:工作队列比tasklet慢很多
错误。在现代多核CPU上,工作队列的典型延迟在1-2毫秒左右,对于99%的驱动来说完全足够。只有对延迟要求极高的场景(如10Gbps网卡、高速ADC)才需要考虑使用tasklet。
❌ 误区3:tasklet可以在多核上并行执行
错误。同一个tasklet永远只会在一个CPU上执行,不会在多核上并行。这是tasklet的设计特性,目的是保证线程安全,不需要额外的同步机制。
❌ 误区4:工作队列不能在中断上下文中调度
错误 。schedule_work()可以在任何上下文中调用,包括中断上下文、软中断上下文和进程上下文。
8. 核心知识点汇总
8.1 顶半部(中断处理函数)
- 运行在中断上下文
- 不能睡眠、不能耗时
- 只做三件事:清中断 → 读数据 → 调度底半部
- 函数返回
IRQ_HANDLED
8.2 底半部
- 运行在软中断上下文(tasklet)或内核线程上下文(工作队列)
- 处理耗时但不紧急的逻辑
- 两种主流实现:tasklet和工作队列
8.3 tasklet核心要点
- 基于软中断实现,轻量级
- 不能睡眠,执行快
- 适合极短、无阻塞的操作
- 核心API:
DECLARE_TASKLET/tasklet_inittasklet_scheduletasklet_kill
8.4 工作队列核心要点
- 基于内核线程实现,通用型
- 可以睡眠,功能完整
- 适合长耗时、需要阻塞的操作
- 核心API:
INIT_WORKschedule_workcancel_work_sync
8.5 标准驱动流程
硬件触发中断
→ 顶半部快速处理(清中断、读数据)
→ 调度底半部(tasklet/工作队列)
→ 底半部执行延时处理
→ 驱动卸载销毁底半部资源