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 上并发执行。
状态迁移规则:
- IDLE → SCHED:调用
tasklet_schedule()或tasklet_hi_schedule(),设置TASKLET_STATE_SCHED,并将 tasklet 加入对应 CPU 的待执行链表。 - SCHED → RUN:软中断处理函数
tasklet_action()或tasklet_hi_action()遍历链表时,清除TASKLET_STATE_SCHED,设置TASKLET_STATE_RUN,然后执行func。 - RUN → IDLE:
func执行完毕后,清除TASKLET_STATE_RUN,tasklet 回到空闲状态。 - SCHED → SCHED(重调度):如果在
func执行期间再次调用tasklet_schedule(),由于TASKLET_STATE_RUN已置位,调度函数会重新设置TASKLET_STATE_SCHED,确保 tasklet 在本次执行结束后再次被调度。
3.3 tasklet 调度原理
tasklet 的调度依赖于软中断机制,具体使用 HI_SOFTIRQ(高优先级)和 TASKLET_SOFTIRQ(普通优先级)两个软中断向量。调度流程分为触发、排队和执行三个阶段。
3.3.1 调度触发:tasklet_schedule()
当驱动调用 tasklet_schedule(&my_tasklet) 时,内核执行以下操作:
- 检查
TASKLET_STATE_SCHED是否已置位,若已置位则直接返回(防止重复入队)。 - 设置
TASKLET_STATE_SCHED标志位。 - 将 tasklet 添加到当前 CPU 的
tasklet_vec链表尾部。 - 触发
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())。该函数的执行流程如下:
- 禁用本地中断,将当前 CPU 的
tasklet_vec链表摘下来,替换为一个空链表。 - 重新启用本地中断。
- 遍历摘下来的链表,对每个 tasklet:
- 清除
TASKLET_STATE_SCHED,设置TASKLET_STATE_RUN。 - 检查
count是否为 0,若不为 0 则跳过执行。 - 执行
func(data)。 - 清除
TASKLET_STATE_RUN。 - 如果某个 tasklet 在
func执行期间被重新调度(TASKLET_STATE_SCHED被再次设置),则将其重新加入链表,并在本次循环结束后再次触发软中断。
3.4 调度特性总结
| 特性 | 说明 |
|---|---|
| 串行化 | 同一 tasklet 不会在多个 CPU 上并发执行,但不同 tasklet 可以并行 |
| 优先级 | 高优先级 tasklet(HI_SOFTIRQ)先于普通 tasklet 执行 |
| 可重入 | tasklet 执行期间可被再次调度,但需等到当前执行完毕才会处理新调度 |
| 禁用控制 | tasklet_disable() 同步等待当前执行完成,tasklet_kill() 确保 tasklet 不再被调度 |
| 上下文 | 在软中断上下文中执行,不能睡眠,不能使用信号量等阻塞原语 |
实践建议:tasklet 适用于执行时间短(微秒级)、不能睡眠、且对 CPU 亲和性要求不高的场景。如果任务需要长时间运行或可能睡眠,应使用 workqueue 替代。