LV06-13-中断-03-中断下半部-01-tasklet

tasklet是什么?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、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,每个结构代表一个独立的小任务。

1
2
3
4
5
6
7
8
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中)如下所示:

1
2
3
4
5
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()

1
2
3
4
5
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()函数:

1
2
3
4
5
void __tasklet_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}

继续看这个 __tasklet_schedule_common()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。

1
2
3
4
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}

我们来看 tasklet_action_common() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 的静态初始化。

1
2
#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

1
2
#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 使用实例

下面是一个实例:

1
2
3
4
5
6
7
8
9
10
#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 进行动态初始化:

1
2
3
4
5
6
7
8
9
10
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 使用实例

1
2
3
4
5
6
7
8
9
10
11
12
#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()

1
2
3
4
5
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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()

1
2
3
4
5
6
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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位的操作。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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()

1
2
3
4
5
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 使用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 使用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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队列的头部指针:

1
2
3
4
5
6
7
/*
* Tasklets
*/
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
};

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

1
2
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

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

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

1
2
DECLARE_TASKLET(name,func, data);
DECLARE_TASKLET_DISABLED(name,func, data);

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

1
2
3
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)编写自己的小任务处理程序小任务处理程序必须符合如下的函数类型:

1
void  tasklet_handler(unsigned long data);

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

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

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

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

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

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

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

2. 简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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 · 苏木/imx6ull-driver-demo - 码云 - 开源中国

2. 开发板测试

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

1
insmod sdriver_demo.ko
image-20250323122800703

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

image-20250323123123347

参考资料:

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

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