4. tasklet 机制详解:结构体、初始化、调度、执行与软中断关系

tasklet 是 Linux 内核中一种常用的中断底半部机制,它建立在软中断(softirq)之上,提供了比直接使用软中断更简单、更安全的接口。对于驱动开发者而言,tasklet 是处理 DMA 传输完成、网络数据包接收等非紧急但需快速响应的任务的理想选择。本节将深入剖析 tasklet 的核心数据结构、生命周期以及它与软中断的协作关系。

4.1 tasklet 核心结构体

tasklet 的底层实现依赖于 tasklet_struct 结构体,该结构体定义在 <linux/interrupt.h> 中。理解其成员是掌握 tasklet 行为的关键。

成员 类型 描述
next struct tasklet_struct * 用于将 tasklet 链接到调度链表中的指针。
state unsigned long tasklet 的状态标志位,主要包含 TASKLET_STATE_SCHED(已调度)和 TASKLET_STATE_RUN(正在执行)。
count atomic_t 引用计数器。当 count 为 0 时,tasklet 被启用;非 0 时被禁用,即使被调度也不会执行。
func void (*func)(unsigned long) 指向 tasklet 处理函数的指针,这是底半部要执行的核心代码。
data unsigned long 传递给 func 函数的参数,通常用于传递设备结构体指针或 DMA 通道索引。
关键点: count 字段是 tasklet 启用/禁用的开关。驱动在初始化时通常将其设为 0(启用)。在需要临时禁止 tasklet 执行(例如在 DMA 描述符链更新期间)时,可以调用 tasklet_disable() 增加计数。

4.2 tasklet 的初始化与注册

tasklet 的初始化有两种主要方式:静态声明和动态初始化。

  • 静态声明(编译时): 使用 DECLARE_TASKLET(name, func, data) 宏。该宏会创建一个 tasklet_struct 实例,并将 count 初始化为 0(启用状态)。
  • 动态初始化(运行时): 使用 tasklet_init(t, func, data) 函数。适用于 tasklet 结构体作为设备结构体成员或动态分配的场景。
// 静态声明示例
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)&my_dev);

// 动态初始化示例
struct tasklet_struct *dma_tasklet;
dma_tasklet = kzalloc(sizeof(*dma_tasklet), GFP_KERNEL);
tasklet_init(dma_tasklet, dma_tasklet_handler, (unsigned long)dma_channel);

4.3 tasklet 的调度与执行流程

tasklet 的执行遵循“调度一次,执行一次”的原则,且保证同一个 tasklet 在同一时刻最多只能在一个 CPU 上执行。其完整生命周期如下图所示:

tasklet 调度与执行生命周期 中断上半部 (ISR) tasklet_schedule() 检查 TASKLET_STATE_SCHED 加入 per-CPU tasklet 队列 触发 HI_SOFTIRQ/TASKLET_SOFTIRQ tasklet_action() 执行处理函数 调度规则: • 若 tasklet 已在其他 CPU 上运行, 则仅标记调度,不重复入队。 • 保证同类型 tasklet 串行执行。
调度核心逻辑: 当调用 tasklet_schedule() 时,内核会检查 TASKLET_STATE_SCHED 位。如果该位未被设置,则设置该位并将 tasklet 加入当前 CPU 的 tasklet 链表,最后触发软中断。如果该位已被设置,则直接返回,避免重复入队。

4.4 tasklet 与软中断的关系

tasklet 本质上是软中断的一种特殊实现。内核定义了两个专用的软中断向量来服务 tasklet:

  • TASKLET_SOFTIRQ(优先级 6): 处理普通优先级的 tasklet。
  • HI_SOFTIRQ(优先级 0): 处理高优先级的 tasklet(使用 tasklet_hi_schedule() 调度)。

当软中断 TASKLET_SOFTIRQ 被触发时,内核会调用 tasklet_action() 函数。该函数遍历当前 CPU 的 tasklet 链表,依次执行每个 tasklet 的处理函数。下表总结了 tasklet 与软中断的关键差异:

特性 软中断 (Softirq) tasklet
并发性 同一类型的软中断可在多个 CPU 上同时执行 同一 tasklet 实例在同一时刻只能在一个 CPU 上执行
可重入性 处理函数必须设计为可重入 处理函数无需考虑可重入(串行化保证)
注册方式 编译时静态分配(open_softirq()) 运行时动态或静态声明
使用复杂度 高,需要处理并发和同步 低,适合大多数驱动场景
典型用途 网络子系统、块设备层等高性能路径 DMA 完成处理、字符设备事件通知
注意事项: 虽然 tasklet 简化了并发处理,但它仍然在软中断上下文中执行。这意味着 tasklet 处理函数中不能调用可能导致睡眠的函数(如 copy_to_user()、kmalloc(GFP_KERNEL)、mutex_lock() 等)。如果需要睡眠,必须将工作进一步推迟到工作队列(workqueue)中。

4.5 tasklet 在 DMA 传输中的典型用法

在 DMA 驱动中,tasklet 通常被用作 DMA 传输完成的中断底半部。以下是一个典型的初始化与调度模式:

// 设备结构体中包含 tasklet
struct my_dma_device {
    struct tasklet_struct dma_complete_tasklet;
    // ... 其他成员
};

// DMA 中断上半部
static irqreturn_t my_dma_irq_handler(int irq, void *dev_id)
{
    struct my_dma_device *dev = (struct my_dma_device *)dev_id;

    // 1. 读取 DMA 状态寄存器,确认传输完成
    // 2. 清除中断标志位
    // 3. 调度 tasklet 处理数据
    tasklet_schedule(&dev->dma_complete_tasklet);

    return IRQ_HANDLED;
}

// tasklet 处理函数(底半部)
static void my_dma_tasklet_handler(unsigned long data)
{
    struct my_dma_device *dev = (struct my_dma_device *)data;

    // 1. 检查 DMA 描述符状态
    // 2. 处理接收到的数据(如复制到上层缓冲区)
    // 3. 重新提交 DMA 描述符以准备下一次传输
    // 注意:此函数中不能睡眠
}

// 驱动初始化时
static int my_dma_probe(struct platform_device *pdev)
{
    struct my_dma_device *dev;
    // ... 分配和初始化 dev

    tasklet_init(&dev->dma_complete_tasklet,
                 my_dma_tasklet_handler,
                 (unsigned long)dev);
    // ...
}

// 驱动卸载时
static int my_dma_remove(struct platform_device *pdev)
{
    struct my_dma_device *dev = platform_get_drvdata(pdev);
    tasklet_kill(&dev->dma_complete_tasklet); // 确保 tasklet 不再运行
    // ...
}
最佳实践: 在驱动卸载时,务必调用 tasklet_kill() 函数。该函数会等待任何正在执行的 tasklet 完成,并防止新的调度,从而避免在模块卸载后 tasklet 处理函数访问已释放的内存。