4. tasklet API 详解:DECLARE_TASKLET、tasklet_schedule、tasklet_kill 等函数的使用
tasklet 是 Linux 内核中一种基于软中断的、原子性底半部机制。它运行在软中断上下文(atomic context),具有不可休眠、不可阻塞、同一 tasklet 不会在多个 CPU 上并发执行等特性。掌握其核心 API 是正确使用 tasklet 的前提。
4.1 核心数据结构:struct tasklet_struct
所有 tasklet 操作都围绕 struct tasklet_struct 展开。该结构体定义在 <linux/interrupt.h> 中,主要字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
next |
struct tasklet_struct * |
链表指针,用于将 tasklet 挂入调度队列 |
state |
unsigned long |
状态位:TASKLET_STATE_SCHED(已调度)和 TASKLET_STATE_RUN(正在运行) |
count |
atomic_t |
引用计数:非 0 时 tasklet 被禁用,不会执行 |
func |
void (*func)(unsigned long) |
底半部处理函数指针 |
data |
unsigned long |
传递给 func 的参数 |
关键约束:
count 字段初始值为 0(启用状态)。若在调度前调用 tasklet_disable() 使 count 递增,则 tasklet 即使被 tasklet_schedule() 调度也不会执行,直到 tasklet_enable() 将 count 减回 0。
4.2 静态初始化:DECLARE_TASKLET 与 DECLARE_TASKLET_DISABLED
内核提供两个宏用于在编译时静态声明并初始化 tasklet:
/* 声明并初始化一个已启用的 tasklet */
DECLARE_TASKLET(name, function, data);
/* 声明并初始化一个已禁用的 tasklet(count 初始为 1) */
DECLARE_TASKLET_DISABLED(name, function, data);
宏展开后等价于:
struct tasklet_struct name = {
.next = NULL,
.state = 0,
.count = ATOMIC_INIT(0), /* DECLARE_TASKLET */
.count = ATOMIC_INIT(1), /* DECLARE_TASKLET_DISABLED */
.func = function,
.data = data,
};
实践建议:绝大多数驱动场景使用
DECLARE_TASKLET 即可。若需要在特定时机(如硬件初始化完成后再使能底半部)才允许 tasklet 执行,则使用 DECLARE_TASKLET_DISABLED,后续手动调用 tasklet_enable() 激活。
4.3 动态初始化:tasklet_init
对于运行时动态创建的 tasklet(例如设备探测阶段才分配的结构体),使用 tasklet_init():
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
该函数将 t 的 next 置 NULL、state 清 0、count 置 0,并绑定处理函数与参数。
注意:不要对已使用
DECLARE_TASKLET 静态初始化的 tasklet 再次调用 tasklet_init(),否则会覆盖原有状态,可能导致调度状态丢失。
4.4 调度与执行:tasklet_schedule 与 tasklet_hi_schedule
调度函数将 tasklet 挂入对应 CPU 的调度链表,并触发软中断:
| 函数 | 优先级 | 说明 |
|---|---|---|
tasklet_schedule() |
普通 | 挂入 TASKLET_SOFTIRQ 软中断向量,默认优先级 |
tasklet_hi_schedule() |
高 | 挂入 HI_SOFTIRQ 软中断向量,优先执行 |
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
调度流程如下:
- 检查
t->state是否已置TASKLET_STATE_SCHED,若已置位则直接返回(防止重复调度)。 - 原子置位
TASKLET_STATE_SCHED。 - 将 tasklet 挂入当前 CPU 的
tasklet_vec(普通)或tasklet_hi_vec(高优先级)链表尾部。 - 触发对应软中断(
raise_softirq())。
执行保证:同一 tasklet 在多个 CPU 上同时调用
tasklet_schedule() 时,只会被调度一次。执行期间若再次被调度,会在当前执行完成后重新排队,不会丢失事件。
4.5 生命周期管理:tasklet_kill、tasklet_disable 与 tasklet_enable
在驱动卸载或模块退出时,必须确保 tasklet 不再运行:
void tasklet_kill(struct tasklet_struct *t);
tasklet_kill() 会阻塞等待,直到满足以下条件之一:
- tasklet 不在任何 CPU 的调度链表中(
TASKLET_STATE_SCHED被清除)。 - tasklet 当前未在任何 CPU 上执行(
TASKLET_STATE_RUN被清除)。
死锁风险:不要在 tasklet 处理函数内部调用
tasklet_kill(),否则会导致自死锁。同样,持有自旋锁时调用 tasklet_kill() 也可能因等待 tasklet 执行完毕而引发死锁。
临时禁用与重新启用:
void tasklet_disable(struct tasklet_struct *t); /* 同步禁用,等待当前执行完成 */
void tasklet_disable_nosync(struct tasklet_struct *t); /* 异步禁用,仅递增 count */
void tasklet_enable(struct tasklet_struct *t); /* 递减 count,减到 0 时允许执行 */
tasklet_disable() 会自旋等待直到 tasklet 完成当前执行,适合需要确保底半部完全停止的场景。tasklet_disable_nosync() 仅递增 count,不等待,适合快速路径。
4.6 API 关系总览
4.7 典型使用模式
以下代码展示了一个完整的 tasklet 生命周期:
#include <linux/interrupt.h>
/* 1. 静态声明 */
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);
/* 处理函数 */
static void my_tasklet_handler(unsigned long data)
{
/* 底半部处理:访问硬件寄存器、更新统计、唤醒等待队列等 */
pr_info("tasklet executed on CPU %d\n", smp_processor_id());
}
/* 中断顶半部 */
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
/* 2. 调度 tasklet */
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
/* 模块初始化 */
static int __init my_driver_init(void)
{
/* 动态初始化(若未使用 DECLARE_TASKLET) */
/* tasklet_init(&my_tasklet, my_tasklet_handler, 0); */
return request_irq(IRQ_NUM, my_interrupt_handler, IRQF_SHARED,
"my_device", dev_id);
}
/* 模块退出 */
static void __exit my_driver_exit(void)
{
free_irq(IRQ_NUM, dev_id);
/* 3. 销毁前确保 tasklet 不再运行 */
tasklet_kill(&my_tasklet);
}
最佳实践:
- 优先使用
DECLARE_TASKLET静态声明,减少运行时初始化开销。 - 在模块退出函数中,先释放中断,再调用
tasklet_kill(),避免中断再次调度已销毁的 tasklet。 - 若 tasklet 处理函数需要访问共享数据,使用自旋锁保护(因为不可休眠)。
4.8 常见错误与排查
| 错误场景 | 后果 | 解决方案 |
|---|---|---|
在 tasklet 中调用 tasklet_kill() |
自死锁,系统挂起 | 只在进程上下文调用 tasklet_kill() |
未调用 tasklet_kill() 直接释放内存 |
UAF(Use-After-Free),内核崩溃 | 严格遵循先 kill 再 free 的顺序 |
在 tasklet 中调用 msleep() 或 wait_event() |
内核调度异常,软中断阻塞 | 改用 workqueue 或内核线程 |
| 多个 CPU 同时调度同一 tasklet | 不会并发执行,但可能丢失调度事件 | 若需每 CPU 独立处理,使用 DECLARE_PER_CPU tasklet |
调试提示:若怀疑 tasklet 未按预期执行,检查
/proc/softirqs 中 TASKLET 和 HI 的计数是否增长。若计数增长但处理函数未执行,检查 count 字段是否被意外递增(例如多次调用 tasklet_disable() 未配对 tasklet_enable())。