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);
}
调度流程的关键步骤:
- 原子置位:通过
test_and_set_bit将TASKLET_STATE_SCHED置 1,保证同一个 tasklet 不会被重复加入调度队列。 - 加入链表:将 tasklet 挂入当前 CPU 的
tasklet_vec(普通优先级)或tasklet_hi_vec(高优先级)链表。 - 触发软中断:调用
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 从初始化到销毁的完整状态转换:
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 是软中断的简化版本,牺牲了并行性换取了使用安全性。理解其数据结构、调度原子性和执行时机,是后续排查软中断上下文竞态条件的基础。