Linux 中断的两半机制
中断处理有个核心矛盾:需要快速响应,但有些处理很耗时。
Linux 的解决方案:上半部 + 下半部
中断触发
│
▼
┌─────────────────────────────┐
│ 上半部(Top Half) │ ← 必须快,关中断执行
│ · 读取硬件数据 │
│ · 清除中断标志 │
│ · 登记下半部任务 │
└──────────────┬──────────────┘
│ 立即返回,重新开中断
▼
┌─────────────────────────────┐
│ 下半部(Bottom Half) │ ← 稍后执行,可被中断
│ · 数据处理 │
│ · 协议栈处理 │
│ · 唤醒等待进程 │
└─────────────────────────────┘
下半部的三种实现方式
1. softirq(软中断)
· 编译时静态定义,数量固定(32个)
· 同一 softirq 可在多个 CPU 并发执行
· 优先级最高,用于网络收发、块设备等
· 在 ksoftirqd 内核线程或中断返回时执行
2. tasklet
· 基于 softirq 实现
· 同一 tasklet 同一时刻只在一个 CPU 执行(更安全)
· 动态创建,驱动开发常用
3. workqueue(工作队列)
· 在内核线程(worker thread)中执行
· 可以睡眠、阻塞(前两种不能睡眠)
· 适合耗时操作
| softirq | tasklet | workqueue | |
|---|---|---|---|
| 执行上下文 | 中断上下文 | 中断上下文 | 进程上下文 |
| 可以睡眠 | ❌ | ❌ | ✅ |
| 并发 | 多CPU并发 | 串行 | 多线程 |
| 使用难度 | 复杂 | 简单 | 简单 |
驱动中如何使用中断
#include <linux/interrupt.h>
// 1. 定义中断处理函数
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
// 上半部:快速处理
// 清中断标志、读数据...
// 登记下半部
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
// 2. 注册中断
int irq = gpio_to_irq(gpio_num); // 获取IRQ号
request_irq(irq, // IRQ号
my_irq_handler, // 处理函数
IRQF_TRIGGER_RISING, // 触发方式
"my_device", // 名字
dev); // 传给handler的参数
// 3. 释放中断
free_irq(irq, dev);
下半部用 tasklet
// 定义 tasklet
static void my_tasklet_func(unsigned long data)
{
// 下半部处理,耗时操作
}
DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);
// 在上半部中触发
tasklet_schedule(&my_tasklet);
下半部用 workqueue
// 定义 work
static void my_work_func(struct work_struct *work)
{
// 可以睡眠,可以阻塞
msleep(100); // workqueue 中可以这样用
}
DECLARE_WORK(my_work, my_work_func);
// 触发
schedule_work(&my_work);
中断上下文的限制
在中断处理程序(上半部、softirq、tasklet)中:
❌ 不能睡眠 // 没有进程上下文,无法调度
❌ 不能调用可能睡眠的函数 // mutex_lock、kmalloc(GFP_KERNEL)...
❌ 不能访问用户空间内存
✅ 可以用 spinlock
✅ 可以用 kmalloc(GFP_ATOMIC)
✅ 可以读写硬件寄存器
中断屏蔽
// 关闭本地CPU所有中断
local_irq_disable();
// ... 临界区 ...
local_irq_enable();
// 更安全的方式(保存中断状态)
unsigned long flags;
local_irq_save(flags);
// ... 临界区 ...
local_irq_restore(flags);
// 只禁止某个IRQ
disable_irq(irq);
enable_irq(irq);
完整流程总结
硬件触发中断
│
▼
GIC 仲裁,通知 CPU
│
▼
CPU 切换到内核态,保存现场,切换到中断栈
│
▼
查向量表,调用 handle_irq
│
▼
执行上半部(irq_handler)── 快速返回
│
▼
触发下半部(softirq / tasklet / workqueue)
│
▼
恢复现场,返回被中断的代码
- 中断上半部分和下半部分的关系:
| 下半部类型 | 能否被硬中断打断 | 能否被同类打断 |
|---|---|---|
| softirq | ✅ 能 | ❌ 不能(同一CPU) |
| tasklet | ✅ 能 | ❌ 不能 |
| workqueue | ✅ 能 | ✅ 能 |
硬中断可以打断下半部
时间轴 ──────────────────────────────→
┌──────────┐
│ 下半部 │
│ 执行中 │
└────┬─────┘
│ 新的硬中断来了
▼
┌──────────┐
│ 上半部 │ ← 打断下半部,优先处理
└────┬─────┘
│ 上半部结束
▼
┌──────────┐
│ 下半部 │ ← 继续执行
│ 继续 │
└──────────┘
因为下半部执行时中断是开着的,所以硬中断随时可以进来。
但 softirq 不能被同类打断
// softirq 执行时,会设置标志位
// 同一个 CPU 上,同种 softirq 不会重入
CPU0 正在执行 NET_RX_SOFTIRQ
│
│ 又来了网络包,触发 NET_RX_SOFTIRQ
│
└──→ 不会立即执行,等当前 softirq 结束再说
但不同 CPU 可以同时执行同一种 softirq,所以 softirq 处理函数要考虑并发。
tasklet 更严格
同一个 tasklet,任意时刻只在一个 CPU 上执行
│
├── CPU0 执行 tasklet A ──→ CPU1 想执行 tasklet A?等着
└── CPU0 执行 tasklet A ──→ CPU1 执行 tasklet B?可以
所以 tasklet 比 softirq 更安全,驱动开发更常用。
workqueue 最自由
workqueue 跑在内核线程里,是完整的进程上下文:
能被硬中断打断 ✅
能被其他线程抢占 ✅
能睡眠 ✅
能被调度器调度 ✅
跟普通内核线程没有区别。
一句话总结
下半部对硬中断都是"敞开的",硬中断随时能打断它。但下半部内部有自己的互斥机制,防止自己被同类重入。
- 线程来实现中断下半部分,就是 threaded IRQ(线程化中断),Linux 2.6.30 之后引入的机制。
每个中断可以有一个专属的内核线程来处理下半部。
用法
// 传统方式
request_irq(irq, top_half_handler, flags, "name", dev);
// 线程化中断
request_threaded_irq(
irq,
top_half_handler, // 上半部,硬中断上下文,可以为NULL
thread_handler, // 下半部,在内核线程中执行
flags,
"name",
dev
);
内核会自动创建一个名为 irq/中断号-name 的内核线程:
$ ps aux | grep irq
root irq/27-eth0 # 网卡中断线程
root irq/44-mmc0 # SD卡中断线程
执行流程
硬件中断
│
▼
┌────────────────┐
│ 上半部 │ 硬中断上下文,关中断,必须快
│ 快速处理 │
└───────┬────────┘
│ 唤醒内核线程
▼
┌────────────────┐
│ irq/xx 线程 │ 进程上下文
│ 下半部处理 │ 可以睡眠
│ 可以阻塞 │ 可以被调度
└────────────────┘
和 workqueue 的区别
看起来都是用线程执行下半部,但有区别:
| threaded IRQ | workqueue | |
|---|---|---|
| 线程归属 | 每个IRQ独占线程 | 共享worker线程池 |
| 实时性 | 更高,可设优先级 | 一般 |
| 使用场景 | 中断下半部专用 | 通用异步任务 |
| 可睡眠 | ✅ | ✅ |
优点
1. 可以睡眠
static irqreturn_t thread_handler(int irq, void *dev)
{
mutex_lock(&my_mutex); // ✅ 可以,进程上下文
msleep(10); // ✅ 可以睡眠
// 处理数据...
mutex_unlock(&my_mutex);
return IRQ_HANDLED;
}
2. 可以设置优先级
// 设置线程为实时优先级
irq_set_affinity_hint(irq, cpumask); // 绑定CPU
// 或通过 chrt 命令设置线程优先级
3. 实时内核(PREEMPT_RT)的基础
PREEMPT_RT 补丁把几乎所有中断都线程化
目的是让内核完全可抢占,实现硬实时
RK3568 的实时系统场景就用这个
上半部返回值
线程化中断的上半部有特殊返回值:
static irqreturn_t top_half(int irq, void *dev)
{
if (不是我的中断)
return IRQ_NONE; // 不是我的,忽略
// 快速处理...
return IRQ_WAKE_THREAD; // 唤醒下半部线程
// 或
return IRQ_HANDLED; // 上半部已经处理完,不需要线程
}
总结
下半部实现方式汇总:
softirq 最快,不能睡眠,并发复杂,内核自用
tasklet 基于softirq,不能睡眠,驱动常用
workqueue 线程池,能睡眠,通用
threaded IRQ 专属线程,能睡眠,实时性好,现代驱动推荐
线程化中断完整的驱动示例(按键中断)
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/mutex.h>
struct button_dev {
struct gpio_desc *gpio;
int irq;
struct mutex lock;
int press_count;
};
/* 上半部 */
static irqreturn_t button_top_half(int irq, void *dev_id)
{
struct button_dev *bdev = dev_id;
// 仅做最快的处理
pr_info("上半部:中断触发,IRQ=%d\n", irq);
return IRQ_WAKE_THREAD; // 唤醒线程
}
/* 下半部线程 */
static irqreturn_t button_thread_handler(int irq, void *dev_id)
{
struct button_dev *bdev = dev_id;
pr_info("下半部线程:开始处理\n");
// 消抖延时(上半部不能做,线程可以)
msleep(20);
// 读取GPIO状态
int val = gpiod_get_value(bdev->gpio);
mutex_lock(&bdev->lock);
if (val == 0) { // 低电平:按键按下
bdev->press_count++;
pr_info("按键按下,第 %d 次\n", bdev->press_count);
}
mutex_unlock(&bdev->lock);
return IRQ_HANDLED;
}
static int button_probe(struct platform_device *pdev)
{
struct button_dev *bdev;
int ret;
bdev = devm_kzalloc(&pdev->dev, sizeof(*bdev), GFP_KERNEL);
if (!bdev)
return -ENOMEM;
mutex_init(&bdev->lock);
// 获取GPIO
bdev->gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(bdev->gpio))
return PTR_ERR(bdev->gpio);
// 获取IRQ号
bdev->irq = gpiod_to_irq(bdev->gpio);
if (bdev->irq < 0)
return bdev->irq;
// 注册线程化中断
ret = request_threaded_irq(
bdev->irq,
button_top_half,
button_thread_handler,
IRQF_TRIGGER_FALLING, // 下降沿(按下)
"button_irq",
bdev
);
if (ret) {
dev_err(&pdev->dev, "注册中断失败: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, bdev);
dev_info(&pdev->dev, "按键驱动加载成功,IRQ=%d\n", bdev->irq);
return 0;
}
static int button_remove(struct platform_device *pdev)
{
struct button_dev *bdev = platform_get_drvdata(pdev);
free_irq(bdev->irq, bdev);
return 0;
}