内核软中断之tasklet机制

1. 软中断IRQ简介

软中断(SoftIRQ)是内核提供的一种基于中断的延时机制, Linux内核定义的软中断有以下几种:

enum
{
	HI_SOFTIRQ=0,			/*高优先级的tasklet*/
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	TASKLET_SOFTIRQ,  /*普通tasklet*/
	SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
	HRTIMER_SOFTIRQ,
#endif
};

此外内核维持一个全局的IRQ数组,用来记录不同的软中断的详细信息:

static struct softirq_action softirq_vec[32]  __cacheline_aligned_in_smp;

该全局变量的类型为struct softirq_action, 它所有IRQ中断处理操作的函数原型. 也就是说系统维持的全局变量记录的是每一种IRQ的中断处理函数信息, 但它的结构非常简单:

struct softirq_action
{
	void	(*action)(struct softirq_action *); /*中断处理函数指针*/
	void	*data; /*传递的参数*/
};

由于内核开发者们不建议我们添加自定义软中断, 为此专门提供了tasklet的机制,我们可以通过tasklet来实现我们自己的中断处理任务。tasklet是内核提供开发者的、基于软中断的任务延时机制。tasklet只是内核定义的几种softirq中的一种,设备驱动程序往往通过tasklet来实现某些延时操作。在软中断中tasklet分为两类:一类为高优先级的HI_SOFTIRQ; 另一类为TASKLET_SOFTIRQ。下面详细介绍tasklet的相关信息:

2. tasklet结构体之tasklet_struct

struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};
序号变量名作用
1next用来连接系统中的tasklet对象,构成链表
2state记录tasklet的状态: TASKLET_STATE_SCHED表示tasklet已经被提交。TASKLET_STATE_RUN表示tasklet正在执行(只用于SMP)
3count0:enable, 可以被调度执行。 1: disable, 不可执行 ;
4functasklet执行体,由开发者实现
5data给func传递的参数

3. tasklet机制初始化

Linux系统在初始化的过程中,通过调用softirq_init函数来为HI_SOFTIRQTASKLET_SOFTIRQ安装上函数执行体,分别为tasklet_hi_actiontasklet_action

void __init softirq_init(void)
{
	open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}

所谓安装执行函数,就是在IRQ全局变量上,将这两个中断对应的成员参数(一个回调函数指针、一个传递的参数)进行初始化,而这部分是通过open_softirq()来实现的。open_softirq()函数实现十分简单:

static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
	softirq_vec[nr].data = data;
	softirq_vec[nr].action = action;
}

通过上述两个函数的执行,便完成了tasklet机制的初始化任务,即:

softirq_vec[TASKLET_SOFTIRQ].action = tasklet_hi_action;
softirq_vec[HI_SOFTIRQ].action = tasklet_action;

4. tasklet的初始化

tasklet对象的初始化方法有两种,一种为静态方式,一种为动态方式

4.1 tasklet对象静态初始化

tasklet对象的声明由两个函数,声明对象时需要将tasklet的对象名name, 延时操作函数func, 以及需要传递的参数进行实现。

#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 }

使用DECLARE_TASKLET声明的对象处于enable状态,使用DECLARE_TASKLET_DISABLED声明的对象处于disable状态。

4.2 tasklet对象动态方式初始化

动态方式初始化则是直接定义一个tasklet_struct 变量,然后使用tasklet_init()函数完成初始化操作。

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;
}

5. tasklet对象提交

所谓tasklet对象提交,实际上就是将声明的tasket加入到tasklet_vec管理的链表中, tasklet_vec是一个per-CPU类型的变量,用来将系统中所有通过tasklet_schedule函数提交的tasklet对象构成链表。(linux-2.6.22)

void fastcall __tasklet_schedule(struct tasklet_struct *t)
{
	unsigned long flags;

	local_irq_save(flags);
	t->next = __get_cpu_var(tasklet_vec).list; /*使用头插法插入一个新节点 */
	__get_cpu_var(tasklet_vec).list = t;
	raise_softirq_irqoff(TASKLET_SOFTIRQ);
	local_irq_restore(flags);
}
static inline void tasklet_schedule(struct tasklet_struct *t)
{
	if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))/*调度位为0,再提交;如果为1,则证明已经提交,无需再提交*/
		__tasklet_schedule(t);
}
  • test_and_set_bit(nr,vaddr) 函数:将vaddr的值修改为nr, 并返回vaddr未修改时的值。
  • local_irq_save: 关闭本地中断响应。
  • local_irq_restore: 打开本地中断。
  • 注意: 调度时的条件是state未被设置位TASKLET_STATE_SCHED; 而tasklet在执行权限是通过count来判断的,count=0时该tasklet才可以被执行

解析如下:

  • 调度tasklet对象时,如果state位不为TASKLET_STATE_SCHED,表示该tasklet对象尚未被调度,则将state值置为TASKLET_STATE_SCHED(如果state的值已经为TASKLET_STATE_SCHED,表示已经被调度,则无需再重新调度);
  • 关闭本地中断,然后将tasklet对象插入到tasklet_vec链表上。Linux2.6.22内核采用的为头插法,有的Linux版本采用的为尾插法 ,同时struct tasklet_head 的成员有所不同,
  • 发送软件中断请求,raise_softirq_irqoff(TASKLET_SOFTIRQ) 来通知内核当前的处理器上有个==“TASKLET_SOFTIRQ”== 正等待处理,它是使用一个整型变量是上的位来表示该位上是由有待处理的IRQ操作,0表示没有,1表示有。
  • 重新使能本地中断。

5. tasklet内部执行体

tasklet任务被tasklet_schedule提交到系统相应的链表上后,系统会在遍历该链表然后依次执行tasklet任务。由于HI_SOFTIRQTASKLET_SOFTIRQ两个中断只是优先级的不同,其他在处理上基本一致。这里只是介绍TASKLET_SOFTIRQ的执行体tasklet_action

static void tasklet_action(struct softirq_action *a)
{
	struct tasklet_struct *list;
	local_irq_disable();
	list = __get_cpu_var(tasklet_vec).list;     /*从tasklet_vec上取下链表,实际是链表的头指针*/
	__get_cpu_var(tasklet_vec).list = NULL; /*清空tasklet_vec链表,实际只是清list指针*/
	local_irq_enable();
	while (list) {
		struct tasklet_struct *t = list;
		list = list->next;
		if (tasklet_trylock(t)) {
			if (!atomic_read(&t->count)) {/*count=0  使能*/
				if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))/*清空调度位,返回原来值*/
					BUG();/*如果原来为0,属异常情况,应结束退出*/
				t->func(t->data);
				tasklet_unlock(t);
				continue;
			}
			tasklet_unlock(t);
		}

		local_irq_disable();
		t->next = __get_cpu_var(tasklet_vec).list;/*重新插入到tasklet_var中*/
		__get_cpu_var(tasklet_vec).list = t;
		__raise_softirq_irqoff(TASKLET_SOFTIRQ);
		local_irq_enable();
	}
}
  • 在关闭本地中断的前提下,将当前cpu上待处理的tasklet_vec 链表头指针移到临时变量list记录下来,然后将tasklet链表清空,之后重新使能本地中断。重新本地中断的目的是为了可以使系统重新调度新的tasklet任务到tasklet_vec 链表中;
  • 遍历该链表
    • 使用tasklet_trylock来判断当前的tasklet任务是否在其他cpu上进行处理;
      • 获取当前tasklet任务的count值, 如果count=0,表示当前任务(enable)是可以被调度执行的。如果该值为1,则表示当前的tasklet任务(disable)不可调度;
      • 检测state的TASKLET_STATE_SCHED位,如果为1,使正常提交的;但如果为0,则表示当前的处理函数正在调度一个没有提交的tasklet,这是种异常情况,直接退出返回;
      • 执行tasklet注册的执行体
      • 解锁,并重新执行下一个tasklet任务。
    • 如果tasklet_trylock的返回值为0,表示该任务可能在其他cpu上进行处理,那么需要重新将该tasklet任务重新提交到tasklet_vec链表上,然后触发软中断等待下次软中断重新执行;
  • 从上述代码中可以知道,一个tasklet任务被执行完之前,它的的state上的TASKLET_STATE_SCHED被清空,这就是说除非这个tasklet被重新提交,否则下次软中断是不会再次执行该tasklet任务的。这是一种one-shot特性:提交一次,调度一次,运行完后,从CPU的tasklet_var链表上删除,除非该tasklet任务再次被提交
  • 在执行tasklet时,中断是被重新使能的,这是软中断设计时的初衷。如果执行期间,有其他软中段到来,且新的tasklet也是在该CPU上,那么新的tasklet会被调度到tasklet_vec , 此时并不会影响正在执行的list链表,已经执行的tasklet任务的TASKLET_STATE_SCHED位被清空,而新的tasklet任务的TASKLET_STATE_SCHED被使能,新的tasklet会在下一次IRQ调度中被执行

6. tasklet其他操作

6.1 tasklet_disable

tasklet_disabletasklet_disable_nosync 可以使tasklet任务无法被调度后运行,实际上最关键的操作为atomic_inc(&t->count); 因为count=0时(enable),tasklet才可以被调用运行。

static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
	atomic_inc(&t->count);
	smp_mb__after_atomic_inc();
}
static inline void tasklet_disable(struct tasklet_struct *t)
{
	tasklet_disable_nosync(t);
	tasklet_unlock_wait(t);
	smp_mb();
}

6.2 tasklet_enable

tasklet_enable 是通过将tasklet对象中的count-1来实现tasklet可以被调度运行的。
注:一个处于disable状态的tasklet可以被提交到tasklet_vec中,但不会被调度执行

static inline void tasklet_enable(struct tasklet_struct *t)
{
	smp_mb__before_atomic_dec();
	atomic_dec(&t->count);
}
6.3 tasklet_kill

该函数用来清除一个tasklet对象上的TASKLET_STATE_SCHED的状态位,使IRQ不再能够调度运行此任务。如果当前的tasklet任务正在被运行,那么tasklet_kill将忙等直到该任务执行完毕;如果一个对象已经被提交到系统但尚未调度运行,那么tasklet_kill将会睡眠直到tasklet从tasklet_vec链表中删除。因此该函数是一个可能被阻塞的函数。

  • 由此可以看出,一个tasklet被调度后,一定会被执行一次。
void tasklet_kill(struct tasklet_struct *t)
{
	if (in_interrupt())
		printk("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);
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叨陪鲤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值