一、tasklet
1.Tasklet概论
1. Tasklet 的定位与作用
Tasklet 是 Linux 内核中一种延迟执行中断处理任务的机制,属于“软中断”(SoftIRQ)的一种高级封装。它主要用于将**硬件中断处理程序(硬中断)**中的非紧急任务拆分出来,延迟到内核的“安全时间”执行,从而避免长时间占用硬中断上下文,影响系统实时性。
2. 硬件中断(HardIRQ)与软中断(SoftIRQ)的差异
硬件中断:
由硬件设备触发(如网卡收到数据包、键盘输入)。
需要立即响应,处理时间必须极短(通常以微秒计)。
执行时会关闭本地 CPU 的中断响应(防止嵌套中断导致竞态条件)。
软中断:
由内核触发,基于硬中断的后续处理需求。
允许延迟执行,适合耗时较长的任务(如数据包处理、磁盘 I/O)。
执行时不关闭中断,可被更高优先级的中断打断。
3. 为什么收到硬件中断后要设置软中断?
当硬件中断发生时,内核需要快速完成以下步骤:
硬中断处理:
立即响应硬件请求(如读取设备状态寄存器)。
标记需要后续处理的任务(例如将网卡数据拷贝到内存缓冲区)。
触发软中断:
将耗时任务(如协议栈处理、数据解析)委托给软中断。
软中断会被内核调度,在**中断下半部(Bottom Half)**安全执行。
核心原因:
硬中断必须快速完成:长时间关闭中断会导致系统失去响应(例如鼠标卡顿)。
软中断提供异步处理能力:允许内核在合适的时间调度任务,同时保证中断处理的实时性。
4. Tasklet 的特点与实现
低优先级:
Tasklet 的优先级低于硬中断和其他软中断,确保关键任务优先执行。
例如:网络数据包处理的优先级可能低于磁盘 I/O。
可中断性与自我恢复:
Tasklet 执行时可以被硬中断或其他软中断打断。
内核通过维护 Tasklet 的状态队列实现“断点续运行”(例如标记为
TASKLET_STATE_SCHED
)。基于软中断的封装:
Tasklet 内部依赖软中断框架(如
TASKLET_SOFTIRQ
类型)。提供更简单的 API(如
tasklet_schedule()
),避免直接操作底层的软中断。
5. Tasklet 与工作队列(Workqueue)的区别
Tasklet:
运行在中断上下文(不可睡眠,不能调用可能阻塞的函数)。
适合处理与硬件交互紧密的轻量级任务(如更新设备寄存器)。
工作队列:
运行在进程上下文(可以睡眠,允许长时间操作)。
适合需要复杂逻辑的任务(如文件系统操作、长时间计算)。
特性 Tasklet 工作队列(Workqueue) 执行上下文 中断上下文(不可睡眠) 进程上下文(可睡眠) 并发性 同一 CPU 串行 多线程并行 优先级 高(接近软中断) 低(普通线程) 适用任务 短小、实时性要求高 耗时、可能阻塞 调度延迟 微秒级 毫秒级 实现机制 基于软中断 基于内核线程 同步需求 需自旋锁 需互斥锁
6. 示例流程:硬件中断触发 Tasklet
网卡收到数据包,触发硬中断。
硬中断处理程序:
快速读取数据到内核缓冲区。
调用
tasklet_schedule()
触发关联的 Tasklet。内核退出硬中断上下文后,调度软中断。
Tasklet 执行实际的数据包处理(如解析 TCP/IP 协议)。
tasklet实际上就是一个内核定时器,在一个“软中断”的上下文执行(以原子模式),在使能硬件中断执行异步任务的一个内核机制。软中断是将操作推迟到未来执行的最有效的办法。但该延期机制处理起来非常复杂,因为多个处理器可以同时且独立处理软中断,同一个软中断的处理程序例程可以在几个CPU上运行。
2.创建tasklet
各个tasklet中的数据结构称作tasklet_struct,内核源码如下:
1.
tasklet_struct
的作用
tasklet_struct
是 Linux 内核中用于实现 Tasklet 机制的核心数据结构。
每个tasklet_struct
实例代表一个 延迟执行的任务,通常用于处理硬件中断的“下半部”(Bottom Half),将耗时操作从硬中断上下文中剥离,延迟到内核的安全时间异步执行。
2. 核心字段详解
以下是
tasklet_struct
中最重要的字段及其作用:
字段 类型 说明 next
struct tasklet_struct *
链表指针,将多个 Tasklet 连接成队列,允许多个任务排队等待执行。 state
unsigned long
状态标志:
-TASKLET_STATE_SCHED
:Tasklet 已被调度,等待执行。
-TASKLET_STATE_RUN
:Tasklet 正在执行。count
atomic_t
原子计数器:
- 值为 0 时,Tasklet 可被激活和执行。
- 值非 0 时,Tasklet 被禁用(通常用于同步或资源保护)。func
void (*)(unsigned long)
任务处理函数:指向实际要执行的函数,该函数会在 Tasklet 被调度后调用。 data
unsigned long
传递给 func
的参数:可以是任意用户定义的数据(如设备指针、状态标志等)。
3. 关键机制说明
链表管理:
通过next
指针,内核将多个 Tasklet 组织成链表,实现任务排队。例如,当多个硬件中断触发时,它们的 Tasklet 可以依次加入队列,按顺序执行。状态切换:
调用
tasklet_schedule()
时,设置state
为TASKLET_STATE_SCHED
,表示 Tasklet 已加入调度队列。执行前,内核将状态设为
TASKLET_STATE_RUN
,防止同一 Tasklet 在多个 CPU 上并发执行。原子计数器
count
:
atomic_inc(&tasklet->count)
:禁用 Tasklet(使其不可调度)。
atomic_dec(&tasklet->count)
:启用 Tasklet(若计数器为 0,则允许调度)。
4. 具体案例:网卡驱动中的 Tasklet 使用
场景描述:
网卡收到数据包后触发硬中断,中断处理程序(上半部)快速将数据拷贝到内存缓冲区,然后通过 Tasklet(下半部)异步处理协议栈(如解析 TCP/IP 头)。代码实现步骤:
定义 Tasklet 和回调函数:
void process_packet(unsigned long data) {struct net_device *dev = (struct net_device *)data;// 解析数据包协议栈、提交给上层网络栈 }DECLARE_TASKLET(nic_tasklet, process_packet, (unsigned long)dev);
硬中断中调度 Tasklet:
irqreturn_t nic_interrupt(int irq, void *dev_id) {// 1. 读取网卡寄存器,确认数据到达// 2. 将数据拷贝到内存缓冲区// 3. 调度 Tasklet 处理协议栈tasklet_schedule(&nic_tasklet);return IRQ_HANDLED; }
Tasklet 执行流程:
硬中断退出后,内核触发软中断(
TASKLET_SOFTIRQ
)。软中断处理程序遍历 Tasklet 链表,依次执行
nic_tasklet->func(dev)
。
5. 关键过程图示
硬件中断触发│↓ 执行硬中断上半部(快速拷贝数据)│↓ 调用 tasklet_schedule() 将 Tasklet 加入队列│↓ 硬中断退出,内核触发软中断│↓ 软中断处理程序执行 Tasklet 的 func()│↓ 协议栈处理完成,数据提交给上层应用
3.注册tasklet
tasklet_schedule将一个tasklet的任务注册到系统中:
1.
tasklet_schedule
的核心作用
tasklet_schedule()
是 Linux 内核中用于将 Tasklet 注册到系统并触发异步执行的关键函数。其核心目的是:
将 Tasklet 加入 CPU 本地队列,确保任务被内核后续的软中断处理程序执行。
避免重复调度:通过状态标志
TASKLET_STATE_SCHED
防止同一 Tasklet 被多次加入队列。分离中断上下文:允许硬件中断处理程序(上半部)快速完成,耗时操作延迟到 Tasklet(下半部)执行。
2. 注册流程详解
以下是
tasklet_schedule()
的具体执行步骤:(1) 检查 Tasklet 状态
标志位
TASKLET_STATE_SCHED
:
每个 Tasklet 的state
字段中有一个比特位表示是否已被调度。
如果该标志已设置(值为
1
),说明 Tasklet 已被加入队列,无需重复操作,直接退出。如果未设置(值为
0
),继续执行后续操作。代码逻辑:
if (test_and_set_bit(TASKLET_STATE_SCHED, &t->state))return; // 已注册,直接退出
(2) 将 Tasklet 加入 CPU 本地队列
目标队列:每个 CPU 维护一个
tasklet_vec
链表(普通优先级)或tasklet_hi_vec
(高优先级)。插入操作:
获取当前 CPU 的队列头(
struct tasklet_head
)。将 Tasklet 的
next
指针设为NULL
(表示链表末尾)。通过二级指针
tail
快速插入到链表尾部(时间复杂度 O(1)O(1))。代码逻辑:
struct tasklet_head *head = this_cpu_ptr(&tasklet_vec); // 获取当前 CPU 的队列 t->next = NULL; // 新节点 next 置空 *head->tail = t; // 将 t 写入链表尾部节点的 next 字段 head->tail = &t->next; // 更新 tail 指针到新节点的 next 地址
(3) 触发软中断
软中断类型:
普通优先级 Tasklet 触发
TASKLET_SOFTIRQ
。高优先级 Tasklet 触发
HI_SOFTIRQ
。内核响应:软中断处理程序(如
tasklet_action()
)将在后续的软中断处理阶段执行队列中的 Tasklet。代码逻辑:
raise_softirq_irqoff(TASKLET_SOFTIRQ); // 触发软中断
3. 关键设计思想
状态标志 (
TASKLET_STATE_SCHED
):
避免同一 Tasklet 被多次加入队列,防止重复执行和竞态条件。例如,在网卡中断频繁触发时,即使多次调用tasklet_schedule()
,实际仅第一次有效。每 CPU 队列 (
tasklet_vec
):
每个 CPU 维护独立的队列,避免多核竞争(无锁设计),提升性能。二级指针尾部插入:
通过直接操作链表尾部节点的next
字段地址,实现 O(1)O(1) 时间复杂度的插入,无需遍历链表。
4.执行tasklet
tasklet的生命周期中最重要的部分就是将其执行。因为tasklet是基于软中断实现的,它们总是在处理软中断的时候被执行。
tasklet关联到TASKLET_SOFTIRQ软中断。因而,调用raise_softirq(TASKLET_SOFTIRQ)
,即可在下一个适当的时机执行当前处理器的tasklet。内核使用task_action作为该软中断的action函数。
1.
tasklet_action
的核心作用
tasklet_action
是 Linux 内核中处理 TASKLET_SOFTIRQ 软中断的核心函数,其核心作用为:
遍历并执行当前 CPU 的 Tasklet 队列(
tasklet_vec
)中注册的所有 Tasklet。协调 Tasklet 的状态切换:在 Tasklet 执行前标记为运行状态(
TASKLET_STATE_RUN
),执行后清除调度状态(TASKLET_STATE_SCHED
)。确保 Tasklet 的串行执行:同一 Tasklet 不会在多个 CPU 上并发执行。
2. 函数调用流程
以下是
tasklet_action
的执行流程及关键代码逻辑:(1) 软中断触发与入口
当内核触发 TASKLET_SOFTIRQ 软中断时(例如通过
raise_softirq(TASKLET_SOFTIRQ)
),软中断子系统会调用注册的tasklet_action
函数:static __latent_entropy void tasklet_action(struct softirq_action *a) {// 调用通用处理函数,传入当前 CPU 的 Tasklet 队列(tasklet_vec)tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ); }
(2) 通用处理函数
tasklet_action_common
tasklet_action_common
是实际处理 Tasklet 队列的函数,其核心步骤如下:
禁用本地中断:防止并发操作导致队列状态不一致。
获取当前 CPU 的 Tasklet 队列(例如
tasklet_vec
)。清空队列头尾指针:将队列重置为空,后续新调度的 Tasklet 会加入新队列。
遍历链表并逐个执行 Tasklet:
检查 Tasklet 的
count
原子计数器(若为 0 才执行)。设置状态为
TASKLET_STATE_RUN
,防止多核并发执行。调用 Tasklet 的
func
函数执行实际任务。清除
TASKLET_STATE_SCHED
和TASKLET_STATE_RUN
状态。
(3)新增任务
1. 队列已有任务:全部执行
当
tasklet_action
被软中断触发时,会遍历当前 CPU 的tasklet_vec
链表,逐个执行队列中已挂载的所有tasklet_struct
任务。例如,若队列中有 A、B 两个 tasklet,tasklet_action
会先执行 A,再执行 B,直到队列清空。2. 执行中新增任务:下次处理
如果在某个 tasklet 执行过程中,又通过
tasklet_schedule
调度了新的 tasklet(包括当前正在执行的 tasklet 再次调度自己),新调度的 tasklet 会被加入队列,但 不会在本次tasklet_action
中立即执行,而是等待下一次TASKLET_SOFTIRQ
软中断触发时,由tasklet_action
处理。
5.tasklet队列
在 Linux 内核中,Tasklet 队列(存储所有待处理任务的结构)并不是一个独立的全局结构体,而是通过 每 CPU(Per-CPU)的链表 来管理的。具体来说,每个 CPU 核心维护两个 Tasklet 队列:
-
普通优先级队列(
tasklet_vec
):用于普通 Tasklet。 -
高优先级队列(
tasklet_hi_vec
):用于高优先级 Tasklet(如定时器相关任务)。
1. Tasklet 队列的结构体
队列头定义
Tasklet 队列的链表头通过
struct tasklet_head
表示:struct tasklet_head {struct tasklet_struct *head; // 指向链表中的第一个 Taskletstruct tasklet_struct **tail; // 指向链表尾部的指针(用于快速插入) };
每 CPU 的队列实例
内核为每个 CPU 定义了两个
tasklet_head
实例:// 普通优先级队列(每个 CPU 一个) static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); // 高优先级队列(每个 CPU 一个) static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
2. Tasklet 队列的工作原理
(1) 队列组织方式
每个 Tasklet(
tasklet_struct
)通过其next
字段链接成单链表,链表的头尾由tasklet_head
管理:struct tasklet_struct {struct tasklet_struct *next; // 链表指针// ... 其他字段(state, func, data 等) };
(2) 任务调度流程
当调用
tasklet_schedule()
注册一个 Tasklet 时:
根据 Tasklet 的优先级(普通或高),将其添加到当前 CPU 的
tasklet_vec
或tasklet_hi_vec
链表中。触发软中断(
TASKLET_SOFTIRQ
或HI_SOFTIRQ
),通知内核在适当时间处理队列。(3) 任务执行流程
内核在软中断处理函数(如
tasklet_action()
)中:
遍历当前 CPU 的 Tasklet 链表。
依次执行每个 Tasklet 的
func
函数。执行完成后,清除
TASKLET_STATE_SCHED
状态。
3. 关键代码示例
(1) 队列初始化
内核初始化时,为每个 CPU 的队列设置空链表:
// 初始化普通优先级队列 for_each_possible_cpu(cpu) {per_cpu(tasklet_vec, cpu).head = NULL;per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; } // 高优先级队列同理
(2) Tasklet 入队操作
以
tasklet_schedule()
为例,将 Tasklet 加入当前 CPU 的队列:void tasklet_schedule(struct tasklet_struct *t) {// 1. 检查 Tasklet 是否已被调度if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {// 2. 获取当前 CPU 的队列指针struct tasklet_head *head = this_cpu_ptr(&tasklet_vec);// 3. 将 Tasklet 插入链表尾部t->next = NULL;*head->tail = t;head->tail = &t->next;// 4. 触发软中断raise_softirq_irqoff(TASKLET_SOFTIRQ);} }
4. 实际案例:网卡驱动中的 Tasklet 队列使用
场景描述
网卡收到数据包后触发硬中断,中断处理程序(上半部)快速将数据存入缓冲区,然后通过 Tasklet(下半部)处理协议栈。
代码实现
定义 Tasklet 和队列
// 定义 Tasklet 处理函数 void process_packet(unsigned long data) {struct net_device *dev = (struct net_device *)data;// 解析数据包并提交给网络协议栈 }// 初始化 Tasklet(关联到当前 CPU 的队列) DECLARE_TASKLET(nic_tasklet, process_packet, (unsigned long)dev);
硬中断中调度 Tasklet
irqreturn_t nic_interrupt(int irq, void *dev_id) {// 1. 读取网卡状态寄存器// 2. 拷贝数据到内存缓冲区// 3. 将 Tasklet 加入当前 CPU 的队列tasklet_schedule(&nic_tasklet);return IRQ_HANDLED; }
内核处理队列
硬中断退出后,触发
TASKLET_SOFTIRQ
软中断。内核调用
tasklet_action()
,遍历当前 CPU 的tasklet_vec
队列,依次执行所有 Tasklet 的func
。
二 、等待队列
等待队列(wait queue)用于使进程等待某一特定事件发生,而无需频繁轮询。进程在等待期间睡眠,在事件发生时有内核自动唤醒。完成量(completion)机制基于等待队列,内核利用该机制等待某一操作结束。
在内核里面,等待队列有很多用处,尤其是在中断处理、进程同步、定时等等场合。可以使用等待队列实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问等等。
1.等待队列头数据结构
每一个等待队列都有一个等待队列头,由一下源码表示:
2.等待队列中的有效成员wait_queue_entry
每一个wait_queue_entry关联着一个等待特定事件的进程。它记录了等待队列中具体等待进程的相关信息,是进程和等待队列之间的桥梁。
3.使进程睡眠
add_wait_queue函数用于将一个进程增加到等待队列,该函数在获得必要的自旋锁后,将工作委托给__add_wait_queue:
4.唤醒进程
内核定义了一系列宏,可以用于唤醒等待队列中的进程。它们基于同一个函数:
三、工作队列
每个工作队列都有一个数组,数组项的数目与系统中处理器的数目相同。每个数组项都列出了将延期执行的任务。对每个工作队列来说,内核都会创建一个新的内核守护进程,延期任务使用上文描述的等待队列机制,在该守护进程的上下文中执行。
所有推送到工作队列上的任务,都必须打包为 work_struct 结构的实例,内核源码如下:
workqueue_struct,这个结构是用来描述内核队列的数据结构,源码如下:
1. 内核工作队列的分类
Linux 内核中的工作队列(Workqueue)分为两类:
共享工作队列(Shared Workqueue):由内核在启动时自动创建,供所有驱动和子系统共享使用。
自定义工作队列(Custom Workqueue):由开发者自行创建和管理,适用于需要独立调度或优先级控制的场景。
2. 共享工作队列的核心特点
全局性:系统仅维护一个共享队列(如
system_wq
),所有用户共用。无需手动创建队列:开发者只需定义任务(Work),无需关心队列的创建和销毁。
简单易用:适用于大多数不需要特殊控制的异步任务。
3. 共享工作队列的使用步骤
使用共享工作队列需要以下步骤:
(1) 静态定义工作结构
通过
DECLARE_WORK
宏静态定义一个工作结构体(work_struct
),并关联处理函数。DECLARE_WORK(my_work, my_work_handler); // 静态定义,初始化时绑定处理函数
(2) 动态初始化工作结构(可选)
如果需要在运行时动态初始化工作结构,可使用
INIT_WORK
宏。struct work_struct my_work; INIT_WORK(&my_work, my_work_handler); // 动态初始化
(3) 调度工作到共享队列
通过
schedule_work()
将工作项添加到共享队列,由内核异步执行。schedule_work(&my_work); // 将工作项加入共享队列
4. 关键函数与宏解析
函数/宏 作用 使用场景 DECLARE_WORK
静态声明并初始化一个 work_struct
结构体,绑定处理函数。编译时已知任务处理函数。 INIT_WORK
动态初始化一个已定义的 work_struct
结构体,绑定处理函数。运行时动态创建任务。 schedule_work
将工作项提交到共享工作队列( system_wq
),由内核调度执行。触发异步任务执行。