Linux中断子系统9(基于Linux6.6)---中断之tasklet
一、前情回顾
对于中断处理而言,linux将其分成了两个部分,一个叫做中断handler(top half),属于不那么紧急需要处理的事情被推迟执行,我们称之deferable task,或者叫做bottom half,。具体如何推迟执行分成下面几种情况:
1、推迟到top half执行完毕
2、推迟到某个指定的时间片(例如40ms)之后执行
3、推迟到某个内核线程被调度的时候执行
对于第一种情况,内核中的机制包括sotfirq机制和tasklet机制。第二种情况是属于softirq机制的一种应用场景(timer类型的softirq)。第三种情况主要包括threaded irq handler以及通用的workqueue机制。
中断之tasklet是Linux内核中一种处理中断的机制,主要用于将中断处理分为两部分:一部分是紧急的、需要快速响应的部分,另一部分是不太紧急的、可以稍后处理的部分。tasklet机制允许将不紧急的中断处理工作推迟到系统较为空闲的时候进行,从而提高了系统的响应性和效率。以下是对tasklet的详细概述:
tasklet的定义与特性
-
定义:tasklet是一种特定类型的软中断,它基于软中断机制实现,但相对于软中断来说更加灵活和易用。tasklet可以看作是一小段可执行的代码,通常以函数的形式出现。
-
特性:
- 一种特定类型的tasklet只能运行在一个CPU上,不能并行执行,只能串行处理。
- tasklet是在两种软中断类型的基础上实现的,如果不需要软中断的并行执行特性,tasklet是一个很好的选择。
- tasklet的执行效率较高,无需循环查表。
- tasklet可以在运行时动态改变,比如添加模块时。
二、为什么需要tasklet?
linux内核为什么还要引入tasklet机制呢?主要原因是软中断的pending标志位也就32位,一般情况是不随意增加软中断处理的。而且内核也没有提供通用的增加软中断的接口。其次内,软中断处理函数要求可重入,需要考虑到竞争条件比较多,要求比较高的编程技巧。所以内核提供了tasklet这样的一种通用的机制。
tasklet对于softirq而言,带来了几个显著的好处,这些好处主要体现在灵活性、易用性和同步保护等方面。以下是对这些好处的详细解释:
- 动态分配与注册:
- tasklet可以动态地分配和注册,这意味着开发者可以在运行时根据需要创建和销毁tasklet,而无需在编译时静态地分配它们。
- 相比之下,softirq通常是在编译期间静态分配的,其数量和类型在内核启动时就已经确定,无法在运行时动态改变。
- 易用性:
- tasklet提供了一种相对简单的编程接口,允许开发者通过定义处理函数和调度tasklet来管理中断处理任务。
- 由于tasklet在底层实现了同步保护(即同一类型的tasklet不会在不同的CPU上并行执行),开发者在编写tasklet处理函数时无需过多考虑并发问题,从而降低了编程复杂度。
- 同步保护:
- tasklet在设计时考虑了同步保护的需求,确保同一类型的tasklet不会在不同的CPU上并行执行。
- 这种同步保护机制有助于避免竞态条件和资源冲突,提高了系统的稳定性和可靠性。
- 相比之下,softirq虽然也支持并行处理,但开发者在编写softirq处理函数时需要自行考虑重入问题和同步保护。
- 执行效率:
- tasklet的执行效率通常较高,因为它们是在软中断上下文中运行的,且不需要像进程那样进行复杂的上下文切换。
- 此外,由于tasklet在调度时会被添加到相应的CPU的tasklet链表中,并在适当的时机由软中断处理函数执行,因此它们的执行时机和方式相对可控,有助于优化系统性能。
- 对SMP系统的支持:
- tasklet机制支持SMP(对称多处理)系统,允许多个CPU并行处理中断任务。
- 在SMP系统上,tasklet被确保在第一个调度它的CPU上运行,以提供更好的高速缓存行为,从而提高性能。
三、tasklet的基本原理
3.1、如何抽象一个tasklet
内核中用下面的数据结构来表示tasklet:include/linux/interrupt.h
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
bool use_callback;
union {
void (*func)(unsigned long data);
void (*callback)(struct tasklet_struct *t);
};
unsigned long data;
};
每个cpu都会维护一个链表,将本cpu需要处理的tasklet管理起来,next这个成员指向了该链表中的下一个tasklet。
func和data成员描述了该tasklet的callback函数,func是调用函数,data是传递给func的参数。
state成员表示该tasklet的状态,TASKLET_STATE_SCHED表示该tasklet以及被调度到某个CPU上执行,TASKLET_STATE_RUN表示该tasklet正在某个cpu上执行。
count成员是和enable或者disable该tasklet的状态相关,如果count等于0那么该tasklet是处于enable的,如果大于0,表示该tasklet是disable的。
在sotfirq中,local_bh_disable/enable函数就是用来disable/enable bottom half的,这里就包括softirq和tasklet。但是,有的时候内核同步的场景不需disable所有的softirq和tasklet,而仅仅是disable该tasklet,这时候,tasklet_disable和tasklet_enable就派上用场了。
include/linux/interrupt.h
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t); /* 给tasklet的count加一 */
tasklet_unlock_wait(t); /* 如果该tasklet处于running状态,那么需要等到该tasklet执行完毕 */
smp_mb();
}
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count); /* 给tasklet的count减一 */
}
asklet_disable和tasklet_enable支持嵌套,但是需要成对使用。
2.2、系统如何管理tasklet?
系统中的每个cpu都会维护一个tasklet的链表,定义如下:
kernel/softirq.c
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
linux kernel中,和tasklet相关的softirq有两项,HI_SOFTIRQ用于高优先级的tasklet,TASKLET_SOFTIRQ用于普通的tasklet。
对于softirq而言,优先级就是出现在softirq pending register(__softirq_pending)中的先后顺序,位于bit 0拥有最高的优先级,也就是说,如果有多个不同类型的softirq同时触发,那么执行的先后顺序依赖在softirq pending register的位置,kernel总是从右向左依次判断是否置位,如果置位则执行。
2.3、如何定义一个tasklet?
用下面的宏定义来静态定义tasklet:
include/linux/interrupt.h
#define DECLARE_TASKLET(name, _callback) \
struct tasklet_struct name = { \
.count = ATOMIC_INIT(0), \
.callback = _callback, \
.use_callback = true, \
}
#define DECLARE_TASKLET_DISABLED(name, _callback) \
struct tasklet_struct name = { \
.count = ATOMIC_INIT(1), \
.callback = _callback, \
.use_callback = true, \
}
2.4、如何调度一个tasklet
为了调度一个tasklet执行,使用tasklet_schedule这个接口:
include/linux/interrupt.h
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
程序在多个上下文中可以多次调度同一个tasklet执行(也可能来自多个cpu core),不过实际上该tasklet只会一次挂入首次调度到的那个cpu的tasklet链表,也就是说,即便是多次调用tasklet_schedule,实际上tasklet只会挂入一个指定CPU的tasklet队列中(而且只会挂入一次),也就是说只会调度一次执行。这是通过TASKLET_STATE_SCHED这个flag来完成的,可以用下面的图来描述:
假设HW block A的驱动使用的tasklet机制并且在中断handler(top half)中将静态定义的tasklet(这个tasklet是各个cpu共享的,不是per cpu的)调度执行(也就是调用tasklet_schedule函数)。
当HW block A检测到硬件的动作(例如接收FIFO中数据达到半满)就会触发IRQ line上的电平或者边缘信号,GIC检测到该信号会将该中断分发给某个CPU执行其top half handler,假设这次是cpu0,因此该driver的tasklet被挂入CPU0对应的tasklet链表(tasklet_vec)并将state的状态设定为TASKLET_STATE_SCHED。HW block A的驱动中的tasklet虽已调度,但是没有执行,如果这时候,硬件又一次触发中断并在cpu1上执行,虽然tasklet_schedule函数被再次调用,但是由于TASKLET_STATE_SCHED已经设定,因此不会将HW block A的驱动中的这个tasklet挂入cpu1的tasklet链表中。
在分析底层的__tasklet_schedule函数:
kernel/softirq.c
void __tasklet_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}
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); // 保存IF标志的状态,并禁用本地中断 1
head = this_cpu_ptr(headp); // 为该tasklet分配per_cpu变量
t->next = NULL; // 2
*head->tail = t;
head->tail = &(t->next);
raise_softirq_irqoff(softirq_nr); // 触发软中断,让其在下一次do_softirq()的时候,有机会被执行 3
local_irq_restore(flags); 恢复前面保存的标志
}
(1)下面的链表操作是per-cpu的,因此这里禁止本地中断就可以拦截所有的并发。
(2)这里的三行代码就是将一个tasklet挂入链表的尾部。
(3)raise TASKLET_SOFTIRQ类型的softirq。
2.5、在什么时机会执行tasklet?
上面描述了tasklet的调度,当然调度tasklet不等于执行tasklet,系统会在适合的时间点执行tasklet callback function。由于tasklet是基于softirq的,因此,总结一下softirq的执行场景:
(1)在中断返回用户空间(进程上下文)的时候,如果有pending的softirq,那么将执行该softirq的处理函数。这里限定了中断返回用户空间也就是意味着限制了下面两个场景的softirq被触发执行:
(a)中断返回hard interrupt context,也就是中断嵌套的场景。
(b)中断返回software interrupt context,也就是中断抢占软中断上下文的场景。
(2)上面的描述缺少了一种场景:中断返回内核态的进程上下文的场景,需要详细说明。进程上下文中调用local_bh_enable的时候,如果有pending的softirq,那么将执行该softirq的处理函数。由于内核同步的要求,进程上下文中有可能会调用local_bh_enable/disable来保护临界区。
在临界区代码执行过程中,中断随时会到来,抢占该进程(内核态)的执行(注意:这里只是disable了bottom half,没有禁止中断)。
在这种情况下,中断返回的时候是否会执行softirq handler呢?当然不会,、disable了bottom half的执行,也就是意味着不能执行softirq handler,但是本质上bottom half应该比进程上下文有更高的优先级,一旦条件允许,要立刻抢占进程上下文的执行,因此,当立刻离开临界区,调用local_bh_enable的时候,会检查softirq pending,如果bottom half处于enable的状态,pending的softirq handler会被执行。
(3)系统太繁忙了,不断的产生中断,raise softirq,由于bottom half的优先级高,从而导致进程无法调度执行。这种情况下,softirq会推迟到softirqd这个内核线程中去执行。
对于TASKLET_SOFTIRQ类型的softirq,其handler是tasklet_action,我们来看看各个tasklet是如何执行的:
kernel/softirq.c
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ); //获取该cpu上的tasklet_vec链表
}
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(); //关本cpu中断 1
list = tl_head->head; //取出本cpu的tl的链表头
tl_head->head = NULL; // 将当前处理器上的该链表设置为NULL, 达到清空的效果。
tl_head->tail = &tl_head->head;
local_irq_enable();
//循环遍历获得链表上的每一个待处理的tasklet
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) { // ----- 2 判断该tasklet还没被执行
if (!atomic_read(&t->count)) { // ----- 3 判断是该tasklet不是可以执行(没disable)
if (!test_and_clear_bit(TASKLET_STATE_SCHED, // 检查是不是可以调度(执行)
&t->state))
BUG();
t->func(t->data); //执行绑定的任务函数
tasklet_unlock(t);
continue; //能处理的链表这里都会处理掉
}
tasklet_unlock(t);
}
local_irq_disable(); //下面执行的是暂时不能执行的链表,把这些链表挂到tl的尾部,下一次再执行
t->next = NULL;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(softirq_nr); //再次触发softirq,等待下一个执行时机
local_irq_enable();
}
}
(1)从本cpu的tasklet链表中取出全部的tasklet,保存在list这个临时变量中,同时重新初始化本cpu的tasklet链表,使该链表为空。由于bottom half是开中断执行的,因此在操作tasklet链表的时候需要使用关中断保护。
(2)tasklet_trylock主要是用来设定该tasklet的state为TASKLET_STATE_RUN,同时判断该tasklet是否已经处于执行状态,这个状态很重要,它决定了后续的代码逻辑。
include/linux/interrupt.h
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(); }
}
在调用tasklet_schedule函数将会使得该driver的tasklet挂入cpu1的tasklet链表中。由于cpu0在处理其他硬件中断,因此,cpu1的tasklet后发先至,进入tasklet_action函数调用,这时候,当从cpu1的tasklet摘取所有需要处理的tasklet链表中,HW block A对应的tasklet实际上已经是在cpu0上处于执行状态了。
在设计tasklet的时候就规定,同一种类型的tasklet只能在一个cpu上执行,因此tasklet_trylock就是起这个作用的。
(3)检查该tasklet是否处于enable状态,如果是,说明该tasklet可以真正进入执行状态了。主要的动作就是清除TASKLET_STATE_SCHED状态,执行tasklet callback function。
(4)如果该tasklet已经在别的cpu上执行了,那么将其挂入该cpu的tasklet链表的尾部,这样,在下一个tasklet执行时机到来的时候,kernel会再次尝试执行该tasklet,在这个时间点,也许其他cpu上的该tasklet已经执行完毕了。通过这样代码逻辑,保证了特定的tasklet只会在一个cpu上执行,不会在多个cpu上并发。
上面几句可以简化为如下:
在遍历执行时,在tasklet_trylock()和tasklet_unlock()这一段函数中,完成的功能是:首先会去检查count值时否为0,前面已经分析过,当值不为0的时候,说明该tasklet被禁止,如果没有被禁止,则执行其注册的函数,首先会检查tasklet_state的标志位是否是TASKLET_STATE_RUN状态,如果是,则表示该任务已经在别的处理器上运行,如果没有运行,则将其状态标志设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了,这就保证了在同一时间里,相同类型的tasklet只能有一个在运行。
最后说一下tasklet是在start_kernel初始化的时候就被初始化为前面看的这个action。
kernel/softirq.c
void __init softirq_init(void)
{
int cpu;
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;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
四、Tsklet提供的API
4.1、声明一个Tasklet
静态创建
声明一个tasklet,可以使用下面两个宏中的一个:
include/linux/interrupt.h
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
这两个宏都能根据给定的名字静态的创建一个tasklet_struct结构。当该tasklet被调度后,给定的函数func会被执行,data为其参数。这两个宏的区别在于前者前面一个宏把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态,另一个把引用计数器设置为1,所以该tasklet处于禁止状态。
动态创建
也可以使用一个间接的引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet_init()函数。
kernel/softirq.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->use_callback = false;
t->data = data;
}
EXPORT_SYMBOL(tasklet_init);
4.2、编写tasklet处理函数
因为是软中断实现的,这就意味着不能在tasklet处理函数中使用信号量或者一些阻塞的函数。两个相同的tasklet绝不会同时执行,所以,如果tasklet和其他的tasklet或者是软中断共享了数据,必须进行相应的锁保护。
4.3、调度
只有通过调度才能使tasklet有机会被执行,这就使用上面提到的tasklet_shedule()函数。
include/linux/interrupt.h
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
4.4、禁止或者激活一个tasklet
include/linux/interrupt.h
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count);
smp_mb__after_atomic();
}
可以用来禁止指定的tasklet,不过它无须再返回前等待tasklet执行完毕,这么做往往不太安全,因为无法估计该tasklet是否仍在执行。
include/linux/interrupt.h
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
}
tasklet_disable()函数来禁止某个指定的tasklet,如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。
include/linux/interrupt.h
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);
}
tasklet_enable()函数可以激活一个tasklet。
4.5、删除一个tasklet
通过调用tasklet_kill()函数从挂起的队列中去掉一个tasklet。该函数的参数是一个指向某个tasklet的tasklet_struct的指针。这个函数会等待tasklet执行完毕,然后再将它移除。该函数可能会引起休眠,所以要禁止在中断上下文中使用。
kernel/softirq.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))
wait_var_event(&t->state, !test_bit(TASKLET_STATE_SCHED, &t->state));
tasklet_unlock_wait(t);
tasklet_clear_sched(t);
}
EXPORT_SYMBOL(tasklet_kill);
五、举例
如何使用 Tasklet?
在 Linux 内核中,tasklet
是通过 tasklet_struct
结构体来定义的,可以通过 tasklet_init
来初始化任务,并通过 tasklet_schedule
来调度任务执行。以下是一个简单的例子,演示如何编写和使用 tasklet
。
例子:编写自己的 Tasklet 任务
假设我们要实现一个硬件中断处理程序,它通过 tasklet
延迟执行一些处理任务。我们首先需要定义一个 tasklet
,然后在中断上下文中调度它。
1. 定义 tasklet
任务
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
static void my_tasklet_function(unsigned long data)
{
pr_info("Tasklet executed: data = %lu\n", data);
}
static DECLARE_TASKLET(my_tasklet, my_tasklet_function, 1234);
这里,我们定义了一个 my_tasklet
,它的执行函数是 my_tasklet_function
,并传递了一个数据(1234
)作为参数。在 my_tasklet_function
中,我们只是简单地打印出接收到的数据。
2. 初始化和调度 Tasklet
通常,我们会在某个中断处理函数中调度这个 tasklet
:
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
pr_info("Interrupt occurred, scheduling tasklet...\n");
// 调度 tasklet
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
当硬件中断发生时,my_irq_handler
会被调用,然后 tasklet_schedule
函数会调度 my_tasklet
来执行。注意,这里的任务是异步的,它会在当前中断上下文完成后执行。
3. 模块初始化和清理
接下来,我们需要初始化和清理中断及 tasklet
:
static int __init my_module_init(void)
{
int ret;
pr_info("Module loaded\n");
// 假设我们请求一个中断(IRQ 1)
ret = request_irq(1, my_irq_handler, IRQF_SHARED, "my_irq", NULL);
if (ret) {
pr_err("Failed to request IRQ\n");
return ret;
}
return 0;
}
static void __exit my_module_exit(void)
{
pr_info("Module unloaded\n");
// 释放中断
free_irq(1, NULL);
// 清理 tasklet
tasklet_kill(&my_tasklet);
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Example of using Tasklet in Linux Kernel");