按键驱动中的中断下半部机制
一、中断处理的分层结构
1. 中断处理流程
中断源 → CPU → 中断顶(上)半部【中断服务程序】 → 调度 → 中断底(下)半部
2. 中断上下文的区别
| 特性 | 中断上下文 | 进程上下文 |
|---|---|---|
| 定义 | 中断处理相关代码执行环境 | 进程相关代码执行环境 |
| 包含内容 | 中断服务程序、软中断、tasklet | open、read、write等文件操作 |
| 能否休眠 | 不能休眠、不能阻塞 | 可以休眠、可以阻塞 |
| 调度 | 不能被调度 | 可以被调度 |
| 执行速度 | 要求快进快出 | 可以执行耗时操作 |
二、工作队列机制 (key_workqueue.c)
1. 工作队列的作用
-
工作队列:用来处理中断的普通线程
-
特点:可以处理不紧急任务,可以阻塞,可以休眠
-
适用场景:需要执行耗时操作的中断处理
2. 关键代码分析
(1) 工作结构体定义
static struct work_struct work; // 定义工作结构体
(2) 工作处理函数
static void key_work_func(struct work_struct *work)
{
ssleep(1); // 休眠1秒(在进程上下文中可以休眠)
condition = 1;
wake_up_interruptible(&wq);
printk("key_work_func ..\n");
}
特点:
-
可以调用
ssleep(1)进行休眠 -
可以执行耗时操作
-
在进程上下文中执行
(3) 中断处理函数中的调度
static irqreturn_t key_irq_handler(int irq, void * dev)
{
int arg = *(int *)dev;
if(100 != arg)
return IRQ_NONE;
schedule_work(&work); // 调度工作队列执行
printk("irq = %d dev = %d\n", irq, arg);
return IRQ_HANDLED;
}
schedule_work():将工作加入到系统工作队列中调度执行
(4) 初始化工作队列
INIT_WORK(&work, key_work_func); // 初始化工作,绑定处理函数
-
在
probe函数中初始化 -
指定工作处理函数为
key_work_func
3. 执行流程
1. 按键按下触发中断
2. 执行中断顶半部(key_irq_handler)
- 快速处理(打印信息)
- 调度工作队列(schedule_work)
- 立即返回
3. 系统工作队列调度执行底半部
- 执行 key_work_func
- 可以休眠(ssleep)
- 设置条件变量
- 唤醒等待进程
三、Tasklet机制 (key_tasklet.c)
1. Tasklet的作用
-
Tasklet:基于软中断实现的机制
-
特点:处理紧急任务,要求快进快出,不能休眠,不能阻塞
-
适用场景:需要快速处理的中断下半部
2. 关键代码分析
(1) Tasklet结构体定义
static struct tasklet_struct tsk; // 定义tasklet结构体
// tasklet结构体定义(简化的内核结构)
struct tasklet_struct
{
struct tasklet_struct *next; // 下一个tasklet
unsigned long state; // 状态
atomic_t count; // 引用计数
void (*func)(unsigned long); // 处理函数
unsigned long data; // 传递给处理函数的数据
};
(2) Tasklet处理函数
static void key_tasklet_handler(unsigned long arg)
{
condition = 1;
wake_up_interruptible(&wq);
printk("key_tasklet_handler arg = %ld\n", arg);
}
特点:
-
不能调用休眠函数(如
ssleep) -
必须快进快出
-
在中断上下文中执行
(3) 中断处理函数中的调度
static irqreturn_t key_irq_handler(int irq, void * dev)
{
int arg = *(int *)dev;
if(100 != arg)
return IRQ_NONE;
tasklet_schedule(&tsk); // 调度tasklet执行
printk("irq = %d dev = %d\n", irq, arg);
return IRQ_HANDLED;
}
tasklet_schedule():将tasklet加入到调度队列中执行
(4) 初始化Tasklet
tasklet_init(&tsk, key_tasklet_handler, 100); // 初始化tasklet
-
在
probe函数中初始化 -
参数1:tasklet结构体
-
参数2:处理函数
-
参数3:传递给处理函数的数据(这里传递100)
3. 执行流程
1. 按键按下触发中断
2. 执行中断顶半部(key_irq_handler)
- 快速处理(打印信息)
- 调度tasklet(tasklet_schedule)
- 立即返回
3. 内核在适当的时机执行tasklet
- 执行 key_tasklet_handler
- 不能休眠(直接执行)
- 设置条件变量
- 唤醒等待进程
四、两种下半部机制的对比
1. 调度方式对比
| 特性 | 工作队列 (Workqueue) | Tasklet |
|---|---|---|
| 调度类型 | 同步调度 | 异步调度 |
| 执行上下文 | 进程上下文 | 中断上下文 |
| 能否休眠 | 可以休眠 | 不能休眠 |
| 执行时机 | 由内核线程调度 | 在软中断中立即执行 |
| 适用场景 | 耗时操作,需要阻塞 | 快速操作,不能阻塞 |
2. 同步 vs 异步调度示例
同步调度(工作队列)
// 类似这样的执行流程
fun() {
sleep(1); // 可以休眠
printf("world\n");
}
main() {
fun(); // 调用函数,等待其执行完成
printf("hello\n"); // 只有fun()执行完后才执行
}
// 输出顺序:world → hello
异步调度(Tasklet)
// 类似这样的执行流程
fun() {
printf("world\n"); // 快速执行,不能休眠
}
main() {
schedule_fun(); // 调度函数异步执行
printf("hello\n"); // 立即执行,不等待fun()
}
// 输出顺序可能是:hello → world 或 world → hello
3. 代码初始化对比
// 工作队列初始化
INIT_WORK(&work, key_work_func);
// Tasklet初始化
tasklet_init(&tsk, key_tasklet_handler, 100);
五、中断处理的完整流程
1. 中断顶半部(上半部)要求
-
短小:执行时间要尽量短
-
快进快出:尽快处理完返回
-
置标志位:设置必要的标志,调度下半部处理
-
不执行耗时操作:不能休眠、不能阻塞
2. 中断底半部(下半部)选择原则
中断发生
↓
判断任务性质:
↓
如果任务紧急、简短 → 使用 Tasklet(不能休眠)
↓
如果任务耗时、需要阻塞 → 使用工作队列(可以休眠)
↓
执行相应的下半部处理
3. 实际应用场景
-
Tasklet适用场景:
-
简单的数据处理
-
状态标志设置
-
快速唤醒等待队列
-
-
工作队列适用场景:
-
需要访问文件系统
-
需要分配大量内存
-
需要执行I/O操作
-
需要休眠等待资源
-
六、驱动框架总结
1. 整体架构
应用程序(read阻塞)
↓
驱动程序
├── 文件操作接口(open/read/close)
├── 中断顶半部(快速处理,调度下半部)
├── 中断底半部(Tasklet/工作队列)
│ ├── Tasklet:快速处理,不能休眠
│ └── 工作队列:耗时处理,可以休眠
└── 等待队列机制(同步应用程序和中断)
2. 核心机制
-
等待队列:实现进程的阻塞和唤醒
-
中断处理:快速响应硬件事件
-
下半部机制:处理中断的后续工作
-
设备树匹配:硬件与驱动的自动匹配
3. 使用建议
-
优先使用Tasklet:如果任务简单且不需要休眠
-
必要时使用工作队列:如果任务需要休眠或执行耗时操作
-
保持顶半部简短:中断处理函数中只做必要的最小工作
-
合理使用等待队列:同步用户空间和内核空间的操作
总结:
-
中断分为上下两部分:顶半部(快速处理)和底半部(后续处理)
-
Tasklet:基于软中断,不能休眠,适合快速处理
-
工作队列:基于内核线程,可以休眠,适合耗时操作
-
两种调度方式:同步调度(工作队列)和异步调度(Tasklet)
-
选择依据:根据任务是否需要休眠来选择合适的下半部机制
这些机制都是为了在保证中断响应速度的同时,能够处理复杂的后续工作,提高系统的整体性能和响应能力。