Skip to content

LV050-tasklet简介

tasklet 是什么?

一、tasklet 简介

下半部要做的事情耗时不是太长的时候,我们就可以考虑使用 tasklet。接下来就来了解一下吧。

1. tasklet 是什么?

在 Linux 内核中, tasklet 是一种特殊的 软中断机制,它是通过软中断控制结构来实现的 ,被广泛用于处理中断下文相关的任务。 tasklet 一词的原意是“小片任务”的意思,这里是指一小段可执行的代码,且通常以函数的形式出现。软中断向量 HI_SOFTIRQTASKLET_SOFTIRQ 均是用 tasklet 机制来实现的。

(1)与一般的软中断不同,某一段 tasklet 代码在某个时刻只能在一个 CPU 上运行,而不像一般的软中断服务函数(即 softirq_action 结构中的 action 函数指针)那样——在同一时刻可以被多个 CPU 并发地执行。因此不会出现并发冲突。

(2)不同的 tasklet 代码在同一时刻可以在多个 CPU 上并发地执行,而不像 BH 机制那样必须严格地串行化执行(也即在同一时刻系统中只能有一个 CPU 执行 BH 函数)。

(3) 需要注意的是, tasklet 绑定的函数中不能调用可能导致休眠的函数, 否则可能引起内核异常。

2. 优缺点

  • 优点

(1)简化的接口和编程模型: tasklet 提供了一个简单的接口和编程模型, 使得在内核中处理延迟工作变得更加容易。 相比自己添加软中断, tasklet 提供了更高级的抽象。

(2)低延迟: tasklet 在软中断上下文中执行, 避免了内核线程的上下文切换开销, 因此具有较低的延迟。 这对于需要快速响应的延迟敏感任务非常重要。

(3)自适应调度: tasklet 具有自适应调度的特性, 当多个 tasklet 处于等待状态时, 内核会合并它们以减少不必要的上下文切换。 这种调度机制可以提高系统的效率。

  • 缺点

(1)无法处理长时间运行的任务: tasklet 适用于短时间运行的延迟工作, 如果需要处理长时间运行的任务, 可能会阻塞其他任务的执行。 对于较长的操作, 可能需要使用工作队列或内核线程来处理。

(2)缺乏灵活性: tasklet 的执行受限于软中断的上下文, 不适用于所有类型的延迟工作。某些情况下, 可能需要更灵活的调度和执行机制, 这时自定义软中断可能更加适合。

(3)资源限制: tasklet 的数量是有限的, 系统中可用的 tasklet 数量取决于架构和内核配置。 如果需要大量的延迟工作处理, 可能会受到 tasklet 数量的限制。

3. 怎么描述一个 tasklet

Linux 用数据结构 tasklet_struct 来描述一个 tasklet,每个结构代表一个独立的小任务。

c
struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};
  • next:指向下一个 tasklet 的指针, 用于形成链表结构, 以便内核中可以同时管理多个 tasklet。
  • state:表示 tasklet 的当前状态。

这一个 32 位的无符号长整数,当前只使用了 bit [1] 和 bit [0] 两个状态位。对这两个状态位的定义(在 interrupt.h 中)如下所示:

c
enum
{
	TASKLET_STATE_SCHED,	/* Tasklet is scheduled for execution */
	TASKLET_STATE_RUN	/* Tasklet is running (SMP only) */
};

(1)bit0 表示 TASKLET_STATE_SCHED,等于 1 时表示已经执行了 tasklet_schedule 把该 tasklet 放入队列了; tasklet_schedule 会判断该位,如果已经等于 1 那么它就不会再次把 tasklet 放入队列。(等于 1 时表示这个 tasklet 已经被调度去等待执行了。)

(2)bit1 表示 TASKLET_STATE_RUN,bit [1] =1 表示这个 tasklet 的 func 函数当前正在某个 CPU 上被执行,它仅对 SMP 系统才有意义,其作用就是为了防止多个 CPU 同时执行一个 tasklet 的情形出现。函数执行完后内核会把该位清 0。

  • count:用于引用计数, 用于确保 tasklet 在多个地方调度或取消调度时的正确处理。

只有当 count 等于 0 时,tasklet 代码段才能执行,也即此时 tasklet 是被使能的;如果 count 非零,则这个 tasklet 是被禁止的。任何想要执行一个 tasklet 代码段的人都首先必须先检查其 count 成员是否为 0。

  • func:指向 tasklet 绑定的函数的指针, 该函数将在 tasklet 执行时被调用。
  • data:传递给 tasklet 绑定函数的参数。这是一个 32 位的无符号整数,其具体含义可供 func 函数自行解释,比如将其解释成一个指向某个用户自定义数据结构的地址值。

二、特殊的软中断—— tasklet 分析

tasklet 是 Linux 内核中的一种软中断机制, 它可以被看作是一种轻量级的延迟处理机制。它是通过软中断控制结构来实现的, 因此也被称为软中断。

我们来从代码层面分析一下为什么 tasklet 是一个特殊的软中断呢?接下来就来看一下,Linux 中有两个软中断向量 HI_SOFTIRQTASKLET_SOFTIRQ,并且为他们实现了专用的触发函数和软中断服务函数。

  • 专用的触发函数

tasklet_schedule() 函数和 tasklet_hi_schedule() 函数分别用来在当前 CPU 上触发软中断向量 TASKLET_SOFTIRQHI_SOFTIRQ,并把指定的 tasklet 加入当前 CPU 所对应的 tasklet 队列中去等待执行。

  • 专用的软中断服务函数

tasklet_action() 函数和 tasklet_hi_action() 函数则分别是软中断向量 TASKLET_SOFTIRQHI_SOFTIRQ 的软中断服务函数。在初始化函数 softirq_init() 中,这两个软中断向量对应的描述符 softirq_vec [0] 和 softirq_vec [6] 中的 action 函数指针就被分别初始化成指向函数 tasklet_hi_action() 和函数 tasklet_action()

可以来看一下初始化函数 softirq_init()

c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

void __init softirq_init(void)
{
	int cpu;
	// 初始化每个可能的 CPU 的 tasklet_vec 和 tasklet_hi_vec
    // 将 tail 指针设置为对应的 head 指针的初始位置,这样做是为了确保 tasklet_vec 和 tasklet_hi_vec 的初始状态是空的。
	for_each_possible_cpu(cpu) {
		per_cpu(tasklet_vec, cpu).tail =
			&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
			&per_cpu(tasklet_hi_vec, cpu).head;
	}
    // 注册 TASKLET_SOFTIRQ 软中断, 并指定对应的处理函数为 tasklet_action
	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    // 注册 HI_SOFTIRQ 软中断, 并指定对应的处理函数为 tasklet_hi_action
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

1. TASKLET_SOFTIRQ

1.1 触发函数 tasklet_schedule()

c
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 成员变量的 bit [0] 位(也即 TASKLET_STATE_SCHED 位)设置为 1,该函数同时还返回 TASKLET_STATE_SCHED 位的原有值。因此如果 bit [0] 为的原有值已经为 1,那就说明这个 tasklet 已经被调度到另一个 CPU 上去等待执行了。由于一个 tasklet 在 某一个时刻只能由一个 CPU 来执行,因此 tasklet_schedule() 函数什么也不做就直接返回了。否则,就继续下面的调度操作。我们来看一下下面调用的__tasklet_schedule()函数:

c
void __tasklet_schedule(struct tasklet_struct *t)
{
	__tasklet_schedule_common(t, &tasklet_vec,
				  TASKLET_SOFTIRQ);
}

继续看这个 __tasklet_schedule_common()

c
static void __tasklet_schedule_common(struct tasklet_struct *t,
				      struct tasklet_head __percpu *headp,
				      unsigned int softirq_nr)
{
	struct tasklet_head *head;
	unsigned long flags;

	local_irq_save(flags);     // 保存当前中断状态, 并禁用本地中断
	head = this_cpu_ptr(headp);// 获取当前 CPU 的 tasklet_head 指针
	t->next = NULL;
	*head->tail = t;           // 将当前 tasklet 添加到 tasklet_head 的尾部
	head->tail = &(t->next);   // 更新 tasklet_head 的尾指针
	raise_softirq_irqoff(softirq_nr);// 触发指定的软中断
	local_irq_restore(flags);  // 恢复中断状态
}
  • 首先,调用 local_irq_save() 函数来关闭当前 CPU 的中断,以保证下面的步骤在当前 CPU 上原子地被执行。

  • 然后,将待调度的 tasklet 添加到当前 CPU 对应的 tasklet 队列的尾部。

  • 接着,调用 raise_softirq_irqoff() 函数在当前 CPU 上触发软中断请求 TASKLET_SOFTIRQ

  • 最后,调用 local_irq_restore() 函数来开当前 CPU 的中断。.

__tasklet_schedule_common()函数将 tasklet 成功添加到链表的末尾。当软中断被触发时, 系统会遍历链表并处理每个 tasklet。 因此, 在添加到链表后, tasklet 将在适当的时机被系统调度和执行。

Tips:

(1)tasklet_schedule 调度 tasklet 时,其中的函数并不会立刻执行,而只是把 tasklet 放入队列;

(2)调用一次 tasklet_schedule,只会导致 tasklnet 的函数被执行一次;

(3)如果 tasklet 的函数尚未执行,多次调用 tasklet_schedule 也是无效的,只会放入队列一次。

1.2 服务程序 tasklet_action()

函数 tasklet_action() 是 tasklet 机制与软中断向量 TASKLET_SOFTIRQ 的联系纽带。正是该函数将当前 CPU 的 tasklet 队列中的各个 tasklet 放到当前 CPU 上来执行的。

当发生硬件中断时,内核处理完硬件中断后,会处理软件中断。对于 TASKLET_SOFTIRQ 软件中断,会调用 tasklet_action 函数。执行过程还是挺简单的:从队列中找到 tasklet,进行状态判断后执行 func 函数,从队列中删除 tasklet。

c
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
	tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}

我们来看 tasklet_action_common() 函数:

c
static void tasklet_action_common(struct softirq_action *a,
				  struct tasklet_head *tl_head,
				  unsigned int softirq_nr)
{
	struct tasklet_struct *list;

	local_irq_disable(); // 禁用本地中断
	list = tl_head->head;// 获取 tasklet_head 中的任务链表
	tl_head->head = NULL;// 清空 tasklet_head 中的任务链表
	tl_head->tail = &tl_head->head;// 将 tail 指针重新指向 head 指针的位置
	local_irq_enable();// 启用本地中断
	// 遍历任务链表, 处理每一个 tasklet
	while (list) {
		struct tasklet_struct *t = list;

		list = list->next;// 获取下一个 tasklet, 并更新链表

		if (tasklet_trylock(t)) {         // 尝试获取 tasklet 的锁
			if (!atomic_read(&t->count)) {// 检查 count 计数器是否为 0
				if (!test_and_clear_bit(TASKLET_STATE_SCHED,
							&t->state))
					BUG();// 如果 state 标志位不正确, 则发生错误
				t->func(t->data);  // 执行 tasklet 的处理函数
				tasklet_unlock(t); // 解锁 tasklet
				continue;
			}
			tasklet_unlock(t);
		}

		local_irq_disable();// 禁用本地中断
		t->next = NULL;
		*tl_head->tail = t;// 将当前 tasklet 添加到 tasklet_head 的尾部
		tl_head->tail = &t->next;// 更新 tail 指针
		__raise_softirq_irqoff(softirq_nr);// 触发软中断
		local_irq_enable();// 启用本地中断
	}
}
  • 首先,在当前 CPU 关中断的情况下,“原子”地读取当前 CPU 的 tasklet 队列头部指针,将其保存到局部变量 list 指针中,然后将当前 CPU 的 tasklet 队列头部指针设置为 NULL,以表示理论上当前 CPU 将不再有 tasklet 需要执行(但最后的实际结果却并不一定如此,下面将会看到)。
  • 然后,用一个 while{}循环来遍历由 list 所指向的 tasklet 队列,队列中的各个元素就是将在当前 CPU 上执行的 tasklet。循环体的执行步骤如下:

(1)用指针 t 来表示当前队列元素,即当前需要执行的 tasklet。

(2)更新 list 指针为 list-> next,使它指向下一个要执行的 tasklet。

(3)用 tasklet_trylock() 宏试图对当前要执行的 tasklet(由指针 t 所指向)进行加锁,如果加锁成功(当前没有任何其他 CPU 正在执行这个 tasklet),则用原子读函数 atomic_read() 进一步判断 count 成员的值。如果 count 为 0,说明这个 tasklet 是允许执行的,于是:

① 先清除 TASKLET_STATE_SCHED 位;

② 然后,调用这个 tasklet 的可执行函数 func;

③ 调用宏 tasklet_unlock()来清除 TASKLET_STATE_RUN 位;

④ 最后,执行 continue 语句跳过下面的步骤,回到 while 循环继续遍历队列中的下一个元素。如果 count 不为 0,说明这个 tasklet 是禁止运行的,于是调用 tasklet_unlock() 清除前面用 tasklet_trylock() 设置的 TASKLET_STATE_RUN 位。

2. HI_SOFTIRQ

HI_SOFTIRQ 相关函数都同理。

三、相关 api 函数

1. 静态初始化函数

1.1 两个宏

在 Linux 内核中, 有一个用于静态初始化 tasklet 的宏函数:DECLARE_TASKLET。 这个宏函数可以帮助我们更方便地进行 tasklet 的静态初始化。

c
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

其中, name 是 tasklet 的名称, func 是 tasklet 的处理函数, data 是传递给处理函数的参数。初始化状态为使能状态。如果 tasklet 初始化函数为非使能状态, 使用 DECLARE_TASKLET_DISABLED

c
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

其中, name 是 tasklet 的名称, func 是 tasklet 的处理函数, data 是传递给处理函数的参数。初始化状态为非使能状态。

1.2 使用实例

下面是一个实例:

c
#include <linux/interrupt.h>
// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
} 
// 静态初始化 tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);
// 驱动程序的其他代码

my_tasklet 是 tasklet 的名称, my_tasklet_handler 是 tasklet 的处理函数, 0 是传递给处理函数的参数。 但是需要注意的是, 使用 DECLARE_TASKLET 静态初始化的 tasklet 无法在运行时动态销毁, 因此在不需要 tasklet 时, 应该避免使用此方法。 如果需要在运行时销毁 tasklet, 应使用 tasklet_init 和 tasklet_kill 函数进行动态初始化和销毁, 接下来我们来学习动态初始化函数。

2. 动态初始化函数

2.1 tasklet_init()

在 Linux 内核中, 可以使用 tasklet_init() 函数对 tasklet 进行动态初始化:

c
void tasklet_init(struct tasklet_struct *t,
		  void (*func)(unsigned long), unsigned long data)
{
	t->next = NULL;
	t->state = 0;
	atomic_set(&t->count, 0);
	t->func = func;
	t->data = data;
}
EXPORT_SYMBOL(tasklet_init);

其中, t 是指向 tasklet 结构体的指针, func 是 tasklet 的处理函数, data 是传递给处理函数的参数。

2.2 使用实例

c
#include <linux/interrupt.h>
// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
} 
// 声明 tasklet 结构体
static struct tasklet_struct my_tasklet;
// 初始化 tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// 驱动程序的其他代码

我们首先定义了 my_tasklet_handler 作为 tasklet 的处理函数。 然后, 声明了一个名为 my_tasklet 的 tasklet 结构体。 接下来, 通过调用 tasklet_init 函数, 进行动态初始化。

通过使用 tasklet_init 函数, 我们可以在运行时动态创建和初始化 tasklet。 这样, 我们可以根据需要灵活地管理和控制 tasklet 的生命周期。 在不再需要 tasklet 时, 可以使用 tasklet_kill() 函数进行销毁, 以释放相关资源。

3. 改变一个 tasklet 的状态

在这里,tasklet 状态指两个方面:

  • (1)state:成员所表示的运行状态;
  • (2)count:成员决定的使能 / 禁止状态。

3.1 使能/关闭状态

3.1.1 tasklet_enable()
c
static inline void tasklet_enable(struct tasklet_struct *t)
{
	smp_mb__before_atomic();
	atomic_dec(&t->count);
}

该函数用来使能(启用) 一个已经初始化的 tasklet,该函数会把 count 增加 1。 其中, t 是指向 tasklet 结构体的指针。 使用示例如下:

c
#include <linux/interrupt.h>
// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
} 
// 声明 tasklet 结构体
static struct tasklet_struct my_tasklet;
// 初始化 tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// 使能 tasklet
tasklet_enable(&my_tasklet);
// 驱动程序的其他代码

我们首先定义了 my_tasklet_handler 作为 tasklet 的处理函数。 然后, 声明了一个名为 my_tasklet 的 tasklet 结构体, 并使用 tasklet_init() 函数对其进行初始化。 最后, 通过调用 tasklet_enable() 函数, 我们使能(启用) 了 my_tasklet。

使能 tasklet 后, 如果调用 tasklet_schedule() 函数触发 tasklet, 则 tasklet 的处理函数将会被执行。 这样, tasklet 将开始按计划执行其处理逻辑。

需要注意的是, 使能 tasklet 并不会自动触发 tasklet 的执行, 而是通过调用 tasklet_schedule() 函数来触发。 同时, 可以使用 tasklet_disable() 函数来临时暂停或停止 tasklet 的执行。 如果需要永久停止 tasklet 的执行并释放相关资源, 则应调用 tasklet_kill() 函数来销毁 tasklet。

3.1.2 tasklet_disable()
c
static inline void tasklet_disable(struct tasklet_struct *t)
{
	tasklet_disable_nosync(t);
	tasklet_unlock_wait(t);
	smp_mb();
}

该函数用来关闭 一个已经初始化的 tasklet,该函数会把 count 减 1。 其中, t 是指向 tasklet 结构体的指针。 使用示例如下:

c
#include <linux/interrupt.h>
// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
} 
// 声明 tasklet 结构体
static struct tasklet_struct my_tasklet;
// 初始化 tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// 关闭 tasklet
tasklet_disable(&my_tasklet);
// 驱动程序的其他代码

我们首先定义了 my_tasklet_handler 作为 tasklet 的处理函数。 然后, 声明了一个名为 my_tasklet 的 tasklet 结构体, 并使用 tasklet_init() 函数对其进行初始化。 最后, 通过调用 tasklet_disable() 函数, 我们关闭了 my_tasklet。

关闭 tasklet 后, 即使调用 tasklet_schedule() 函数触发 tasklet,tasklet 的处理函数也不会被执行。 这可以用于临时暂停或停止 tasklet 的执行, 直到再次启用(通过调用 tasklet_enable() 函数) 。

需要注意的是, 关闭 tasklet 并不会销毁 tasklet 结构体 ,因此可以随时通过调用 tasklet_enable() 函数重新启用 tasklet, 或者调用 tasklet_kill() 函数来销毁 tasklet。

3.2 改变运行状态

state 成员中的 bit [0] 表示一个 tasklet 是否已被调度去等待执行,bit [1] 表示一个 tasklet 是否正在某个 CPU 上执行。对于 state 变量中某位的改变必须是一个原子操作,因此可以用定义在 include/asm/bitops.h 头文件中的位操作来进行。

由于 bit [1] 这一位(即 TASKLET_STATE_RUN)仅仅对于 SMP 系统才有意义,因此 Linux 在 interrupt.h 头文件中显示地定义了对 TASKLET_STATE_RUN 位的操作。如下所示:

c
#ifdef CONFIG_SMP
static inline int tasklet_trylock(struct tasklet_struct *t)
{
	return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}

static inline void tasklet_unlock(struct tasklet_struct *t)
{
	smp_mb__before_atomic();
	clear_bit(TASKLET_STATE_RUN, &(t)->state);
}

static inline void tasklet_unlock_wait(struct tasklet_struct *t)
{
	while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { barrier(); }
}
#else
//......
#endif

显然,在 SMP 系统同,tasklet_trylock() 宏将把一个 tasklet_struct 结构变量中的 state 成员中的 bit [1] 位设置成 1,同时还返回 bit [1] 位的非。因此,如果 bit [1] 位原有值为 1(表示另外一个 CPU 正在执行这个 tasklet 代码),那么 tasklet_trylock()宏将返回值 0,也就表示上锁不成功。如果 bit [1] 位的原有值为 0,那么 tasklet_trylock() 宏将返回值 1,表示加锁成功。而在单 CPU 系统中,tasklet_trylock() 宏总是返回为 1。

任何想要执行某个 tasklet 代码的程序都必须首先调用宏 tasklet_trylock() 来试图对这个 tasklet 进行上锁(即设置 TASKLET_STATE_RUN 位),且只能在上锁成功的情况下才能执行这个 tasklet。建议!即使我们的程序只在 CPU 系统上运行,我们也要在执行 tasklet 之前调用 tasklet_trylock() 宏,以便使我们的代码获得良好可移植性。

在 SMP 系统中,tasklet_unlock_wait() 宏将一直不停地测试 TASKLET_STATE_RUN 位的值,直到该位的值变为 0(即一直等待到解锁),假如:CPU0 正在执行 tasklet A 的代码,在此期间,CPU1 也想执行 tasklet A 的代码,但 CPU1 发现 tasklet A 的 TASKLET_STATE_RUN 位为 1,于是它就可以通过 tasklet_unlock_wait() 宏等待 tasklet A 被解锁(也即 TASKLET_STATE_RUN 位被清零)。在单 CPU 系统中,这是一个空操作。

tasklet_unlock() 用来对一个 tasklet 进行解锁操作,也即将 TASKLET_STATE_RUN 位清零。在单 CPU 系统中,这是一个空操作。

4. 调度函数

4.1 tasklet_schedule()

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

该函数用来调度(触发)一个已经初始化的 tasklet,这个函数会把 tasklet 放入链表,并且设置它的 TASKLET_STATE_SCHED 状态为 1。 其中, t 是指向 tasklet 结构体的指针。

4.2 使用实例

c
#include <linux/interrupt.h>
// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
} 
// 声明 tasklet 结构体
static struct tasklet_struct my_tasklet;
// 初始化 tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// 调度 tasklet 执行
tasklet_schedule(&my_tasklet);
// 驱动程序的其他代码

我们首先定义了 my_tasklet_handler 作为 tasklet 的处理函数。 然后, 声明了一个名为 my_tasklet 的 tasklet 结构体, 并使用 tasklet_init() 函数对其进行初始化。 最后, 通过调用 tasklet_schedule() 函数, 我们调度(触发) 了 my_tasklet 的执行。

需要注意的是, 调度 tasklet 只是将 tasklet 标记为需要执行, 并不会立即执行 tasklet 的处理函数。 实际的执行时间取决于内核的调度和处理机制。

5. 销毁函数

5.1 tasklet_kill()

c
void tasklet_kill(struct tasklet_struct *t)
{
	if (in_interrupt())
		pr_notice("Attempt to kill tasklet from interrupt\n");

	while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
		do {
			yield();
		} while (test_bit(TASKLET_STATE_SCHED, &t->state));
	}
	tasklet_unlock_wait(t);
	clear_bit(TASKLET_STATE_SCHED, &t->state);
}
EXPORT_SYMBOL(tasklet_kill);

该函数用来销毁一个已经初始化的 tasklet,释放相关资源。其中, t 是指向 tasklet 结构体的指针。

  • 如果一个 tasklet 未被调度,tasklet_kill 会把它的 TASKLET_STATE_SCHED 状态清 0。

  • 如果一个 tasklet 已被调度, tasklet_kill 会等待它执行完华,再把它的 TASKLET_STATE_SCHED 状态清 0。

5.2 使用实例

c
#include <linux/interrupt.h>
// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
} 
// 声明 tasklet 结构体
static struct tasklet_struct my_tasklet;
// 初始化 tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
tasklet_disable(&my_tasklet);
// 销毁 tasklet
tasklet_kill(&my_tasklet);
// 驱动程序的其他代码

我们首先定义了 my_tasklet_handler 作为 tasklet 的处理函数。 然后, 声明了一个名为 my_tasklet 的 tasklet 结构体, 并使用 tasklet_init() 函数对其进行初始化。 最后, 通过调用 tasklet_kill() 函数, 我们我们销毁了 my_tasklet。

调用 tasklet_kill() 函数会释放 tasklet 所占用的资源, 并将 tasklet 标记为无效。 因此, 销毁后的 tasklet 不能再被使用。

需要注意的是, 在销毁 tasklet 之前, 应该确保该 tasklet 已经被停止( 通过调用 tasklet_disable() 函数) 。 否则, 销毁一个正在执行的 tasklet 可能导致内核崩溃或其他错误。一旦销毁了 tasklet, 如果需要再次使用 tasklet, 需要重新进行初始化(通过调用 tasklet_init() 函数) 。

四、tasklet 队列

多个 tasklet 可以通过 tasklet 描述符中的 next 成员指针链接成一个单向对列。为此,Linux 定义了结构体 struct tasklet_head 来描述一个 tasklet 队列的头部指针:

c
/*
 * Tasklets
 */
struct tasklet_head {
	struct tasklet_struct *head;
	struct tasklet_struct **tail;
};

尽管 tasklet 机制是特定于软中断向量 HI_SOFTIRQTASKLET_SOFTIRQ 的一种实现,但是 tasklet 机制仍然属于 softirq 机制的整体框架范围内的,因此,它的设计与实现仍然必须坚持“谁触发,谁执行”的思想。为此,Linux 为系统中的每一个 CPU 都定义了一个 tasklet 队列头部,来表示应该有各个 CPU 负责执行的 tasklet 对列。

c
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

最后展开大概应该是这样的:

c
struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;

其中, tasklet_vec[]数组用于软中断向量 TASKLET_SOFTIRQ,而 tasklet_hi_vec[]数组则用于软中断向量 HI_SOFTIRQ

也即,如果 CPUi(0≤i≤NR_CPUS-1)触发了软中断向量 TASKLET_SOFTIRQ,那么对列 tasklet_vec[i]中的每一个 tasklet 都将在 CPUi 服务于软中断向量 TASKLET_SOFTIRQ 时被 CPUi 所执行。

同样地,如果 CPUi(0≤i≤NR_CPUS-1)触发了软中断向量 HI_SOFTIRQ,那么队列 tasklet_hi_vec[i]中的每一个 tasklet 都将 CPUi 在对软中断向量 HI_SOFTIRQ 进行服务时被 CPUi 所执行。

队列 tasklet_vec[i]tasklet_hi_vec[i]中的各个 tasklet 是怎样被所 CPUi 所执行的呢?其关键就是软中断向量 TASKLET_SOFTIRQHI_SOFTIRQ 的软中断服务程序——tasklet_action() 函数和 tasklet_hi_action() 函数。下面我们就来分析这两个函数。

五、使用总结

1. 使用步骤

(1)声明和使用小任务大多数情况下,为了控制一个常用的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快。我们既可以静态地创建小任务,也可以动态地创建它。选择那种方式取决于到底是想要对小任务进行直接引用还是一个间接引用。如果准备静态地创建一个小任务(也就是对它直接引用),使用下面两个宏中的一个:

c
DECLARE_TASKLET(name,func, data);
DECLARE_TASKLET_DISABLED(name,func, data);

这两个宏都能根据给定的名字静态地创建一个 tasklet_struct 结构。当该小任务被调度以后,给定的函数 func 会被执行,它的参数由 data 给出。这两个宏之间的区别在于引用计数器的初始值设置不同。第一个宏把创建的小任务的引用计数器设置为 0,因此,该小任务处于激活状态。另一个把引用计数器设置为 1,所以该小任务处于禁止状态。

c
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);
//这行代码其实等价于
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),  tasklet_handler, dev};

这样就创建了一个名为 my_tasklet 的小任务,其处理程序为 tasklet_handler,并且已被激活。当处理程序被调用的时候,dev 就会被传递给它。

(2)编写自己的小任务处理程序小任务处理程序必须符合如下的函数类型:

c
void  tasklet_handler(unsigned long data);

由于小任务不能睡眠,因此不能在小任务中使用信号量或者其它产生阻塞的函数。但是小任务运行时可以响应中断。

(3)调度自己的小任务通过调用 tasklet_schedule()函数并传递给它相应的 tasklt_struct 指针,该小任务就会被调度以便适当的时候执行:

c
tasklet_schedule(&my_tasklet);        /*把 my_tasklet 标记为挂起 */

在小任务被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的小任务又被调度了,那么它仍然只会运行一次。

可以调用 tasklet_disable()函数来禁止某个指定的小任务。如果该小任务当前正在执行,这个函数会等到它执行完毕再返回。调用 tasklet_enable()函数可以激活一个小任务,如果希望把以 DECLARE_TASKLET_DISABLED()创建的小任务激活,也得调用这个函数,如:

c
tasklet_disable(&my_tasklet);        /* 小任务现在被禁止, 这个小任务不能运行 */
tasklet_enable(&my_tasklet);         /* 小任务现在被激活 */

也可以调用 tasklet_kill()函数从挂起的队列中去掉一个小任务。该函数的参数是一个指向某个小任务的 tasklet_struct 的长指针。在小任务重新调度它自身的时候,从挂起的队列中移去已调度的小任务会很有用。这个函数首先等待该小任务执行完毕,然后再将它移去。

2. 简单示例

c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
 
static struct  t asklet_struct my_tasklet;
 
static void tasklet_handler (unsigned long d ata)
{
    printk(KERN_ALERT,"tasklet_handler is running./n");
}

static int __init test_init(void)
{
    tasklet_init(&my_tasklet,tasklet_handler,0);
    tasklet_schedule(&my_tasklet);
    return0;
}

static  void __exit test_exit(void)
{
    tasklet_kill(&tasklet);
    printk(KERN_ALERT,"test_exit is running./n");
}

MODULE_LICENSE("GPL");
 
module_init(test_init);
module_exit(test_exit);

六、tasklet 使用实例

1. demo 源码

源码可以看这里:13_interrupt/06_nodts_tasklet

2. 开发板测试

我们拷贝驱动到开发板,然后加载驱动:

shell
insmod sdriver_demo.ko
image-20250323122800703

我们按下按键,就会发现,先执行了按键中断,由于我们在按键中断中调度了 tasklet,所以这里我们 tasklet 下半部的函数也会执行:

image-20250323123123347

参考资料:

中断处理下半部机制-CSDN 博客

软中断(softirq)机制_软中断机制-CSDN 博客