3. tasklet核心概念

tasklet 是 Linux 内核中一种基于软中断的延迟执行机制,专门用于在原子上下文中执行短小、非阻塞的任务。理解 tasklet 的内部数据结构、状态迁移以及调度流程,是正确使用它的前提。本节从这三个维度展开剖析。

3.1 tasklet 数据结构

tasklet 在内核中由 struct tasklet_struct 表示,定义在 <linux/interrupt.h> 中。该结构体包含了调度、执行和状态管理所需的所有字段。

字段 类型 说明
next struct tasklet_struct * 指向链表中的下一个 tasklet,用于将多个 tasklet 链接成待执行队列
state unsigned long tasklet 的状态位图,包含 TASKLET_STATE_SCHED 和 TASKLET_STATE_RUN
count atomic_t 引用计数器,非零时表示 tasklet 被禁用,不会被调度执行
func void (*func)(unsigned long) tasklet 的执行函数指针,即实际要执行的工作
data unsigned long 传递给 func 的参数,通常用于传递设备或上下文标识
关键点:count 字段是原子变量,初始值为 0(启用状态)。调用 tasklet_disable() 会递增该值,调用 tasklet_enable() 递减。只有当 count == 0 时,tasklet 才允许被调度。

3.2 tasklet 状态机

tasklet 的状态由 state 字段的低两位控制,分别对应两个标志位:

  • TASKLET_STATE_SCHED (bit 0):表示 tasklet 已被调度,等待执行。
  • TASKLET_STATE_RUN (bit 1):表示 tasklet 正在某个 CPU 上执行。

状态迁移遵循严格的规则,确保同一 tasklet 不会在多个 CPU 上并发执行。

状态迁移规则:
  1. IDLE → SCHED:调用 tasklet_schedule() 或 tasklet_hi_schedule(),设置 TASKLET_STATE_SCHED,并将 tasklet 加入对应 CPU 的待执行链表。
  2. SCHED → RUN:软中断处理函数 tasklet_action() 或 tasklet_hi_action() 遍历链表时,清除 TASKLET_STATE_SCHED,设置 TASKLET_STATE_RUN,然后执行 func。
  3. RUN → IDLE:func 执行完毕后,清除 TASKLET_STATE_RUN,tasklet 回到空闲状态。
  4. SCHED → SCHED(重调度):如果在 func 执行期间再次调用 tasklet_schedule(),由于 TASKLET_STATE_RUN 已置位,调度函数会重新设置 TASKLET_STATE_SCHED,确保 tasklet 在本次执行结束后再次被调度。
tasklet 状态机迁移图 IDLE count==0, 无标志位 SCHED TASKLET_STATE_SCHED RUN TASKLET_STATE_RUN tasklet_schedule() 软中断处理 func() 执行完毕 RUN 期间再次调度 注意:同一 tasklet 不会同时在两个 CPU 上执行 RUN 状态

3.3 tasklet 调度原理

tasklet 的调度依赖于软中断机制,具体使用 HI_SOFTIRQ(高优先级)和 TASKLET_SOFTIRQ(普通优先级)两个软中断向量。调度流程分为触发、排队和执行三个阶段。

3.3.1 调度触发:tasklet_schedule()

当驱动调用 tasklet_schedule(&my_tasklet) 时,内核执行以下操作:

  1. 检查 TASKLET_STATE_SCHED 是否已置位,若已置位则直接返回(防止重复入队)。
  2. 设置 TASKLET_STATE_SCHED 标志位。
  3. 将 tasklet 添加到当前 CPU 的 tasklet_vec 链表尾部。
  4. 触发 TASKLET_SOFTIRQ 软中断(通过 raise_softirq_irqoff())。
原子性要求:tasklet_schedule() 可以在中断上下文调用,因为它只操作 per-CPU 链表和原子位操作,不会导致睡眠。

3.3.2 排队与 per-CPU 链表

每个 CPU 维护两个 tasklet 链表:

链表变量 关联软中断 优先级 典型用途
tasklet_vec TASKLET_SOFTIRQ 普通 大多数设备驱动 tasklet
tasklet_hi_vec HI_SOFTIRQ 高 对延迟敏感的 tasklet(如音频、网络)

调度函数根据调用的是 tasklet_schedule() 还是 tasklet_hi_schedule(),将 tasklet 加入对应的链表。

3.3.3 执行阶段:tasklet_action()

当软中断被触发后,内核在 do_softirq() 中调用 tasklet_action()(或 tasklet_hi_action())。该函数的执行流程如下:

  1. 禁用本地中断,将当前 CPU 的 tasklet_vec 链表摘下来,替换为一个空链表。
  2. 重新启用本地中断。
  3. 遍历摘下来的链表,对每个 tasklet:
    • 清除 TASKLET_STATE_SCHED,设置 TASKLET_STATE_RUN。
    • 检查 count 是否为 0,若不为 0 则跳过执行。
    • 执行 func(data)。
    • 清除 TASKLET_STATE_RUN。
  4. 如果某个 tasklet 在 func 执行期间被重新调度(TASKLET_STATE_SCHED 被再次设置),则将其重新加入链表,并在本次循环结束后再次触发软中断。
tasklet 调度与执行完整流程 中断处理函数 tasklet_schedule() per-CPU 链表 调用 入队 raise_softirq() 触发软中断 do_softirq() tasklet_action() 执行 func(data) 摘链表时关闭本地中断

3.4 调度特性总结

特性 说明
串行化 同一 tasklet 不会在多个 CPU 上并发执行,但不同 tasklet 可以并行
优先级 高优先级 tasklet(HI_SOFTIRQ)先于普通 tasklet 执行
可重入 tasklet 执行期间可被再次调度,但需等到当前执行完毕才会处理新调度
禁用控制 tasklet_disable() 同步等待当前执行完成,tasklet_kill() 确保 tasklet 不再被调度
上下文 在软中断上下文中执行,不能睡眠,不能使用信号量等阻塞原语
实践建议:tasklet 适用于执行时间短(微秒级)、不能睡眠、且对 CPU 亲和性要求不高的场景。如果任务需要长时间运行或可能睡眠,应使用 workqueue 替代。