Linux中断子系统9

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的定义与特性

  1. 定义:tasklet是一种特定类型的软中断,它基于软中断机制实现,但相对于软中断来说更加灵活和易用。tasklet可以看作是一小段可执行的代码,通常以函数的形式出现。

  2. 特性

    • 一种特定类型的tasklet只能运行在一个CPU上,不能并行执行,只能串行处理。
    • tasklet是在两种软中断类型的基础上实现的,如果不需要软中断的并行执行特性,tasklet是一个很好的选择。
    • tasklet的执行效率较高,无需循环查表。
    • tasklet可以在运行时动态改变,比如添加模块时。

二、为什么需要tasklet?

linux内核为什么还要引入tasklet机制呢?主要原因是软中断的pending标志位也就32位,一般情况是不随意增加软中断处理的。而且内核也没有提供通用的增加软中断的接口。其次内,软中断处理函数要求可重入,需要考虑到竞争条件比较多,要求比较高的编程技巧。所以内核提供了tasklet这样的一种通用的机制。

tasklet对于softirq而言,带来了几个显著的好处,这些好处主要体现在灵活性、易用性和同步保护等方面。以下是对这些好处的详细解释:

  1. 动态分配与注册
    • tasklet可以动态地分配和注册,这意味着开发者可以在运行时根据需要创建和销毁tasklet,而无需在编译时静态地分配它们。
    • 相比之下,softirq通常是在编译期间静态分配的,其数量和类型在内核启动时就已经确定,无法在运行时动态改变。
  2. 易用性
    • tasklet提供了一种相对简单的编程接口,允许开发者通过定义处理函数和调度tasklet来管理中断处理任务。
    • 由于tasklet在底层实现了同步保护(即同一类型的tasklet不会在不同的CPU上并行执行),开发者在编写tasklet处理函数时无需过多考虑并发问题,从而降低了编程复杂度。
  3. 同步保护
    • tasklet在设计时考虑了同步保护的需求,确保同一类型的tasklet不会在不同的CPU上并行执行。
    • 这种同步保护机制有助于避免竞态条件和资源冲突,提高了系统的稳定性和可靠性。
    • 相比之下,softirq虽然也支持并行处理,但开发者在编写softirq处理函数时需要自行考虑重入问题和同步保护。
  4. 执行效率
    • tasklet的执行效率通常较高,因为它们是在软中断上下文中运行的,且不需要像进程那样进行复杂的上下文切换。
    • 此外,由于tasklet在调度时会被添加到相应的CPU的tasklet链表中,并在适当的时机由软中断处理函数执行,因此它们的执行时机和方式相对可控,有助于优化系统性能。
  5. 对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");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值