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);

调度流程如下:

  1. 检查 t->state 是否已置 TASKLET_STATE_SCHED,若已置位则直接返回(防止重复调度)。
  2. 原子置位 TASKLET_STATE_SCHED。
  3. 将 tasklet 挂入当前 CPU 的 tasklet_vec(普通)或 tasklet_hi_vec(高优先级)链表尾部。
  4. 触发对应软中断(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 关系总览

tasklet API 生命周期与关系图 DECLARE_TASKLET tasklet_init() tasklet_schedule() 处理函数 func() tasklet_disable() tasklet_enable() tasklet_kill() 重新调度 count 控制 关键约束 • 不可休眠/阻塞 • 同一 tasklet 串行执行 • 运行于软中断上下文 销毁顺序 1. tasklet_kill() 等待完成 2. 释放关联内存

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())。