3. tasklet 基础:数据结构、初始化、调度与执行时机

tasklet 是 Linux 内核中最常用的软中断底半部机制之一,建立在软中断 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 之上。它提供了一种简单、原子、不可重入的延迟执行方式,特别适合在中断上半部与进程上下文之间执行轻量级处理。

3.1 tasklet 数据结构

每个 tasklet 由 struct tasklet_struct 描述,定义在 <linux/interrupt.h> 中:

struct tasklet_struct {
    struct tasklet_struct *next;      /* 链表中的下一个 tasklet */
    unsigned long state;              /* TASKLET_STATE_SCHED / TASKLET_STATE_RUN */
    atomic_t count;                   /* 引用计数:0 表示使能,非 0 表示禁用 */
    bool use_callback;                /* 是否使用 callback 函数指针 */
    union {
        void (*func)(unsigned long data);  /* 传统回调函数 */
        void (*callback)(struct tasklet_struct *t); /* 新式回调函数 */
    };
    unsigned long data;               /* 传递给 func 的参数 */
};
字段 作用 关键说明
next 链表指针 所有被调度的 tasklet 挂在 per-CPU 链表上
state 状态标志 位 0 表示已调度,位 1 表示正在运行
count 使能计数器 0 可执行,非 0 被禁用
func / callback 执行函数 二选一,新内核推荐 callback
data 参数传递 仅 func 模式使用
调试要点: 在 crash 或 kgdb 中查看 state 字段可以快速判断 tasklet 是否处于悬挂调度或正在执行状态,是排查死锁和竞态的第一手证据。

3.2 tasklet 初始化

内核提供两种初始化方式:静态声明和动态初始化。

3.2.1 静态声明

// 传统方式
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);
DECLARE_TASKLET_DISABLED(my_tasklet_dis, my_handler, 0);

// 新式 callback 方式 (Linux 5.13+)
DECLARE_TASKLET(my_tasklet_cb, my_callback);

DECLARE_TASKLET 将 count 初始化为 0(使能),DECLARE_TASKLET_DISABLED 将 count 初始化为 1(禁用)。

3.2.2 动态初始化

struct tasklet_struct *t;
t = kmalloc(sizeof(*t), GFP_KERNEL);
tasklet_init(t, my_handler, 0);       // 传统 func 模式
// 或
tasklet_setup(t, my_callback);         // 新式 callback 模式
调试建议: 动态分配的 tasklet 在释放前必须调用 tasklet_kill() 确保没有未完成的调度,否则释放后仍可能被软中断访问,造成 use-after-free。

3.3 tasklet 调度

调度函数 tasklet_schedule() 是 tasklet 机制的核心入口:

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}

调度流程的关键步骤:

  1. 原子置位:通过 test_and_set_bit 将 TASKLET_STATE_SCHED 置 1,保证同一个 tasklet 不会被重复加入调度队列。
  2. 加入链表:将 tasklet 挂入当前 CPU 的 tasklet_vec(普通优先级)或 tasklet_hi_vec(高优先级)链表。
  3. 触发软中断:调用 raise_softirq_irqoff(TASKLET_SOFTIRQ) 或 HI_SOFTIRQ,唤醒软中断处理线程。
竞态陷阱: test_and_set_bit 是原子操作,但 __tasklet_schedule 内部的链表操作需要关闭本地中断保护。如果在中断上半部调用 tasklet_schedule,必须确保中断已关闭;如果在进程上下文调用,则需考虑抢占问题。

3.4 tasklet 执行时机

tasklet 的执行由软中断 tasklet_action 和 tasklet_hi_action 驱动,触发时机包括:

触发点 上下文 说明
中断返回路径 软中断上下文 中断上半部结束后,irq_exit() 检查并执行待处理的软中断
本地软中断触发 软中断上下文 通过 raise_softirq() 主动触发
ksoftirqd 内核线程 进程上下文 当软中断执行时间过长或重入次数过多时,由 ksoftirqd 接管
显式调用 调用者上下文 tasklet_hi_schedule() 或 tasklet_kill() 同步等待
关键认知: tasklet 总是在调度它的 CPU 上执行,这是 per-CPU 设计。利用这一特性可以避免锁竞争,但也会导致 CPU 间负载不均。

3.5 tasklet 生命周期与状态机

下图展示了 tasklet 从初始化到销毁的完整状态转换:

tasklet 生命周期状态机 INIT count=0, state=0 DISABLED count > 0 SCHED SCHED=1, count=0 RUN RUN=1, SCHED=0 KILLED 已释放/销毁 tasklet_schedule() DECLARE_TASKLET_DISABLED tasklet_enable() + schedule() 软中断执行 tasklet_schedule() tasklet_kill() tasklet_kill() tasklet_init() 重新初始化

3.6 tasklet 与软中断的关系

tasklet 本质上是对软中断的封装,两者关系如下:

特性 软中断 (softirq) tasklet
并发模型 同一类型可在多 CPU 并行 同一 tasklet 串行,不同 tasklet 可并行
重入性 可重入,需自行处理 不可重入,内核保证
优先级 固定优先级 (0-9) 普通 / 高优先级 (HI)
动态注册 编译时静态分配 运行时动态创建/销毁
使用复杂度 高,需处理并发和重入 低,适合大多数驱动场景
调试视角: 在 /proc/softirqs 中观察 TASKLET 和 HI 两行的计数,可以判断 tasklet 的调度频率。如果某个 CPU 的计数远高于其他 CPU,说明 tasklet 调度存在 CPU 亲和性问题。

3.7 常见调试场景与排查方法

  • tasklet 未执行:检查 count 是否为 0,state 是否包含 TASKLET_STATE_SCHED,以及软中断是否被屏蔽。
  • tasklet 重复调度:test_and_set_bit 保证同一 tasklet 不会入队两次,但不同 tasklet 之间没有限制。
  • tasklet 执行中死锁:使用 ftrace 的 irqsoff 追踪器或 lockdep 检测软中断上下文中的锁依赖。
  • use-after-free:在释放 tasklet 前务必调用 tasklet_kill() 并确认 state 为 0。
核心结论: tasklet 是软中断的简化版本,牺牲了并行性换取了使用安全性。理解其数据结构、调度原子性和执行时机,是后续排查软中断上下文竞态条件的基础。