中断_中断下半部的一些分析

这几天看软中断看得有些头晕了,因为《情景分析》中总是将软中断和tasklet混在一起,让我搞不懂软中断和tasklet到底是什么关系,看了diytvg y
的博客之后有些豁然开朗的感觉。
http://blog.chinaunix.net/space.php?uid=25014876&do=blog&id=100005
刚开始的疑惑是这样的。
在do_softirq中
主体为
h = softirq_vec;
do {
if(pending & 1){
h->action(h);
}
h++;
}
其中static struct softirq_action softirq_vec[32];
那么跟tasklet有何关系呢?
在softirq_init中
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
将tasklet挂在softirq_vec[32]数组中下标TASKLET_SOFTIRQ和HI_SOFTIRQ的两个软中断向量之上的,
所以从某种意义上将,tasklet是建立在softirq之上的。
其处理函数分别是tasklet_action和tasklet_hi_action;
内核为每一个处理函数维持一个队列,分别是tasklet_vec和tasklet_hi_vec.

__tasklet_schedule就是将一个tasklet_struct挂到tasklet_vec队列上去,

同样,__tasklet_hi_schedule就是将 一个tasklet_struct挂到tasklet_hi_vec队列上去,
而tasklet_action就是执行tasklet_vec队列上的tasklet_struct的回调函数。

好了,废话不多说了,看diytvgy网友的文章吧。

一、什么是下半部


中断是一个很霸道的东西,处理器一旦接收到中断,就会打断正在执行的代码,调用中断处理函数。如果在中断处理函数中没有禁止中断,该中断处理函数执行过程中仍有可能被其他中断打断。出于这样的原因,大家都希望中断处理函数执行得越快越好

另外,中断上下文中不能阻塞,这也限制了中断上下文中能干的事。

基于上面的原因,内核将整个的中断处理流程分为了上半部和下半部。上半部就是之前所说的中断处理函数,它能最快的响应中断,并且做一些必须在中断响应之后马上要做的事情。而一些需要在中断处理函数后继续执行的操作,内核建议把它放在下半部执行。

拿网卡来举例,在linux内核中,当网卡一旦接受到数据,网卡会通过中断告诉内核处理数据,内核会在网卡中断处理函数(上半部)执行一些网卡硬件的必要设置,因为这是在中断响应后急切要干的事情。接着,内核调用对应的下半部函数来处理网卡接收到的数据,因为数据处理没必要在中断处理函数里面马上执行,可以将中断让出来做更紧迫的事情。


可以有三种方法来实现下半部:软中断、tasklet和等待队列。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


二、软中断


软中断一般很少用于实现下半部,但tasklet是通过软中断实现的,所以先介绍软中断。字面理解,软中断就是软件实现的异步中断,它的优先级比硬中断低,但比普通进程优先级高,同时,它和硬中断一样不能休眠


软中断是在编译时候静态分配的,要用软中断必须修改内核代码。


kernel/softirq.c中有这样的一个数组:

51 static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

内核通过一个softirq_action数组来维护的软中断NR_SOFTIRQS是当前软中断的个数,待会再看他在哪里定义。


先看一下softirq_action结构体:

/*include/linux/interrupt.h*/

265 struct softirq_action

266 {

267 void (*action)(struct softirq_action *); //软中断处理函数

268 };

一看发现,结构体里面就一个软中断函数,他的参数就是本身结构体的指针。之所以这样设计,是为了以后的拓展,如果在结构体中添加了新成员,也不需要修改函数接口。在以前的内核,该结构体里面还有一个data的成员,用于传参,不过现在没有了。


接下来看一下如何使用软中断实现下半部

一、要使用软中断,首先就要静态声明软中断:

/*include/linux/interrupt.h*/

246 enum

247 {

248 HI_SOFTIRQ=0, //用于tasklet的软中断,优先级最高,为0

249 TIMER_SOFTIRQ, //定时器的下半部

250 NET_TX_SOFTIRQ, //发送网络数据的软中断

251 NET_RX_SOFTIRQ, //接受网络数据的软中断

252 BLOCK_SOFTIRQ,

253 TASKLET_SOFTIRQ, //也是用于实现tasklet

254 SCHED_SOFTIRQ,

255 HRTIMER_SOFTIRQ,

256 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

257 //add by xiaobai 2011.1.18

258 XIAOBAI_SOFTIRQ, //这是我添加的,优先级最低

259

260 NR_SOFTIRQS, //这个就是上面所说的软中断结构体数组成员个数

261 };

上面通过枚举定义了NR_SOFTIRQS(10)个软中断的索引号,优先级最高是0HI_SOFTIRQ),最低是我刚添加上去的XIAOBAI_SOFTIRQ,优先级为9


二、定义了索引号后,还要注册处理程序。

通过函数open_sofuirq来注册软中断处理函数,使软中断索引号与中断处理函数对应。该函数在kernel/softirq.c中定义:

/*kernel/softirq.c */

321 void open_softirq(int nr, void (*action)(struct softirq_action *))

322 {

323 softirq_vec[nr].action = action;

324 }

其实该函数就是把软中断处理函数的函数指针存放到对应的结构体中,一般的,我们自己写的模块是不能调用这个函数的,为了使用这个函数,我修改了内核:

322 void open_softirq(int nr, void (*action)(struct softirq_action *))

323 {

324 softirq_vec[nr].action = action;

325 }

326 EXPORT_SYMBOL(open_softirq); //这是我添加的,导出符号,这样我编写的程序就能调用

在我的程序中如下调用:

/*6th_irq_3/1st/test.c*/

13 void xiaobai_action(struct softirq_action *t) //软中断处理函数

14 {

15 printk("hello xiaobai!\n");

16 }

。。。。。。。。

48 open_softirq(XIAOBAI_SOFTIRQ, xiaobai_action);


三、在中断处理函数返回前,触发对应的软中断。

在中断处理函数完成了必要的操作后,就应该调用函数raise_sotfirq触发软中断,让软中断执行中断下半部的操作。

/*kernel/softirq.c*/

312 void raise_softirq(unsigned int nr)

313 {

314 unsigned long flags;

315

316 local_irq_save(flags);

317 raise_softirq_irqoff(nr);

318 local_irq_restore(flags);

319 }

所谓的触发软中断,并不是指马上执行该软中断,不然和在中断上执行没什么区别。它的作用只是告诉内核:下次执行软中断的时候,记得执行我这个软中断处理函数。

当然,这个函数也得导出符号后才能调用:

/*kernel/softirq.c*/

312 void raise_softirq(unsigned int nr)

313 {

314 unsigned long flags;

315

316 local_irq_save(flags);

317 raise_softirq_irqoff(nr);

318 local_irq_restore(flags);

319 }

320 EXPORT_SYMBOL(raise_softirq);

在我的程序中如下调用:

/*6th_irq_3/1st/test.c*/

18 irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数

19 {

20 printk("key down\n");

21 raise_softirq(XIAOBAI_SOFTIRQ);

22 return IRQ_HANDLED;

23 }


经过三步,使用软中断实现下半部就成功了,看一下完整的函数:

/*6th_irq_3/1st/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <linux/interrupt.h>

5

6 #define DEBUG_SWITCH 1

7 #if DEBUG_SWITCH

8 #define P_DEBUG(fmt, args...) printk("<1>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args)

9 #else

10 #define P_DEBUG(fmt, args...) printk("<7>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args)

11 #endif

12

13 void xiaobai_action(struct softirq_action *t) //软中断处理函数

14 {

15 printk("hello xiaobai!\n");

16 }

17

18 irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数

19 {

20 printk("key down\n");

21 raise_softirq(XIAOBAI_SOFTIRQ); //触发软中断

22 return IRQ_HANDLED;

23 }

24

25 static int __init test_init(void) //模块初始化函数

26 {

27 int ret;

28

29 /*注册中断处理函数:

30 * IRQ_EINT1:中断号,定义在"include/mach/irqs.h"

31 * irq_handler:中断处理函数

32 * IRQ_TIRGGER_FALLING:中断类型标记,下降沿触发中断

33 * ker_INT_EINT1:中断的名字,显示在/proc/interrupts等文件中

34 * NULL;现在我不使用dev_id,所以这里不传参数

35 */

36 ret = request_irq(IRQ_EINT1, irq_handler,

37 IRQF_TRIGGER_FALLING, "key INT_EINT1", NULL);

38 if(ret){

39 P_DEBUG("request irq failed!\n");

40 return ret;

41 }

42

43 /*fostirq*/

44 open_softirq(XIAOBAI_SOFTIRQ, xiaobai_action); //注册软中断处理程序

45

46 printk("hello irq\n");

47 return 0;

48 }

49

50 static void __exit test_exit(void) //模块卸载函数

51 {

52 free_irq(IRQ_EINT1, NULL);

53 printk("good bye irq\n");

54 }

55

56 module_init(test_init);

57 module_exit(test_exit);

58

59 MODULE_LICENSE("GPL");

60 MODULE_AUTHOR("xoao bai");

61 MODULE_VERSION("v0.1");

注意。在上面的程序,只是为了说明如何实现上下半步,而我的中断上下半步里面的操作是毫无意义的(只是打印)。上下半步的作用我在一开始就有介绍。

接下来验证一下:

[root: 1st]# insmod test.ko

hello irq

[root: 1st]# key down //上半部操作

hello xiaobai! //下半部操作

key down

hello xiaobai!

key down

hello xiaobai!

[root: 1st]# rmmod test

good bye irq


上面介绍,触发软中断函数raise_softirq并不会让软中断处理函数马上执行,它只是打了个标记,等到适合的时候再被实行。如在中断处理函数返回后,内核就会检查软中断是否被触发并执行触发的软中断。

软中断会在do_softirq中被执行,其中核心部分在do_softirq中调用的__do_softirq中:

/*kernel/softirq.c*/

172 asmlinkage void __do_softirq(void)

173 {

。。。。。。

194 do {

195 if (pending & 1) { //如果被触发,调用软中断处理函数

196 int prev_count = preempt_count();

197

198 h->action(h); //调用软中断处理函数

199

200 if (unlikely(prev_count != preempt_count())) {

201 printk(KERN_ERR "huh, entered softirq %td %p"

202 "with preempt_count %08x,"

203 " exited with %08x?\n", h - softirq_vec,

204 h->action, prev_count, preempt_count());

205 preempt_count() = prev_count;

206 }

207

208 rcu_bh_qsctr_inc(cpu);

209 }

210 h++; //下移,获取另一个软中断

211 pending >>= 1;

212 } while (pending); //大循环内执行,知道所有被触发的软中断都执行完

。。。。。。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


三、tasklet


上面的介绍看到,软中断实现下半部的方法很麻烦,一般是不会使用的。一般,我们使用tasklet——利用软中断实现的下半部机制


在介绍软中断索引号的时候,有两个用于实现tasklet的软中断索引号:HI_SOFTIRQTASKLET_SOFTIRQ。两个tasklet唯一的区别就是优先级的大小,一般使用TAKSLET_SOFTIRQ


先看一下如何使用tasklet,用完之后再看内核中是如何实现的:

步骤一、编写tasklet处理函数,定义并初始化结构体tasklet_struct

内核中是通过tasklet_struct来维护一个tasklet,介绍一下tasklet_struct里面的两个成员:

/*linux/interrupt.h*/

319 struct tasklet_struct

320 {

321 struct tasklet_struct *next;

322 unsigned long state;

323 atomic_t count;

324 void (*func)(unsigned long); //tasklet处理函数

325 unsigned long data; //给处理函数的传参

326 };


所以,在初始化tasklet_struct之前,需要先写好tasklet处理函数,如果需要传参,也需要指定传参,你可以直接传数据,也可以传地址。我定义的处理函数如下:

/*6th_irq_3/2nd/test.c*/

15 void xiaobai_func(unsigned long data)

16 {

17 printk("hello xiaobai!, data[%d]\n", (int)data); //也没干什么事情,仅仅打印。

18 }


同样,可以通过两种办法定义和初始化tasklet_struct

1、静态定义并初始化

/*linux/interrupt.h*/

328 #define DECLARE_TASKLET(name, func, data) \

329 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

330

331 #define DECLARE_TASKLET_DISABLED(name, func, data) \

332 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT

上面两个函数都是定义一个叫nametasklet_struct,并指定他的处理函数和传参分别是funcdata。唯一的区别是,DCLARE_TASKLET_DISABLED初始化后的处于禁止状态,暂时不能被使用。

2、动态定义并初始化

跟以往的一样,需要先定义结构体,然后把结构体指针传给tasklet_init来动态初始化:

/*kernel/softirq.c*/

435 void tasklet_init(struct tasklet_struct *t,

436 void (*func)(unsigned long), unsigned long data)


在我的程序中,使用动态定义并初始化:

/*6th_irq_3/2nd/test.c*/

13 struct tasklet_struct xiaobai_tasklet; //定义tasklet结构体

32 tasklet_init(&xiaobai_tasklet, xiaobai_func, (unsigned long)123);

我这里的传参直接传一个数值123。这操作也相当于:

DECLEAR_TASKLET(xiaobai_tasklet, xiaobai_func, (unsigned long)123);


步骤二、在中断返回前调度tasklet


跟软中断一样(其实tasklet就是基于软中断实现),这里说的调度并不是马上执行,只是打个标记,至于什么时候执行就要看内核的调度。

调度使用函数tasklet_schedule或者tasklet_hi_schedule,两个的区别是一个使用TASKLET_SOFTIRQ,另一个使用HI_SOFTIRQ。这两个函数都是一tasklet_struct指针为参数:

/*linux/interrupt.h*/

365 static inline void tasklet_schedule(struct tasklet_struct *t)

373 static inline void tasklet_hi_schedule(struct tasklet_struct *t)


在我的函数中,使用tasklet_schedule

/*6th_irq_3/2nd/test.c*/

23 tasklet_schedule(&xiaobai_tasklet);


步骤三、当模块卸载时,将tasklet_struct结构体移除:

/*kernel/softirq.c*/

447 void tasklet_kill(struct tasklet_struct *t)

确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行程序会休眠,等待直到它执行完毕


另外,还有禁止与激活tasklet的函数。被禁止的tasklet不能被调用,直到被激活:

/*linux/interrupt.h*/

386 static inline void tasklet_disable(struct tasklet_struct *t) //禁止

393 static inline void tasklet_enable(struct tasklet_struct *t) //激活


最后附上程序:

/*6th_irq_3/2nd/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <linux/interrupt.h>

5

。。。。省略。。。。

13 struct tasklet_struct xiaobai_tasklet; //定义tasklet结构体

14

15 void xiaobai_func(unsigned long data)

16 {

17 printk("hello xiaobai!, data[%d]\n", (int)data);

18 }

19

20 irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数

21 {

22 printk("key down\n");

23 tasklet_schedule(&xiaobai_tasklet);

24 return IRQ_HANDLED;

25 }

26

27 static int __init test_init(void) //模块初始化函数

28 {

29 int ret;

30

31 /*tasklet*/

32 tasklet_init(&xiaobai_tasklet, xiaobai_func, (unsigned long)123);

33

41 ret = request_irq(IRQ_EINT1, irq_handler,

42 IRQF_TRIGGER_FALLING, "key INT_EINT1", NULL);

43 if(ret){

44 P_DEBUG("request irq failed!\n");

45 return ret;

46 }

47

48 printk("hello irq\n");

49 return 0;

50 }

51

52 static void __exit test_exit(void) //模块卸载函数

53 {

54 tasklet_kill(&xiaobai_tasklet);

55 free_irq(IRQ_EINT1, NULL);

56 printk("good bye irq\n");

57 }

58

59 module_init(test_init);

60 module_exit(test_exit);


最后验证一下,还是老样子,上下半步只是打印一句话,没有实质操作:

[root: 2nd]# insmod test.ko

hello irq

[root: 2nd]# key down //上半部操作

hello xiaobai!, data[123] //下半部操作

key down

hello xiaobai!, data[123]

[root: 2nd]# rmmod test

good bye irq


既然知道怎么使用tasklet,接下来就要看看它是怎么基于软中断实现的

中断_中断下半部的一些分析_第1张图片

上面说明的是单处理器的情况下,如果是多处理器,每个处理器都会有一个tasklet_vectasklet_hi_vec链表,这个情况我就不介绍了。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


四、总结


这节介绍了如何通过软中断(tasklet也是软中断的一种实现形式)机制来实现中断下半部。使用软中断实现的优缺点很明显:

优点:运行在软中断上下文,优先级比普通进程高,调度速度快。

缺点:由于处于中断上下文,所以不能睡眠。


也许有人会问,那软中断和tasklet有什么区别?

个人理解,tasklet是基于软中断实现的,基本上和软中断相同。但有一点不一样,如果在多处理器的情况下,内核不能保证软中断在哪个处理器上运行(听起来像废话),所以,软中断之间需要考虑共享资源的保护。而tasklet,内核可以保证,两个同类型(TASKLET_SOFTIRQHI_SOFTIRQ)的tasklet不能同时执行,那就说明,同类型tasklet之间,可以不考虑同类型tasklet之间的并发情况。


一般的,优先考虑使用tasklet

6.1下半部

下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。对于在上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:(1)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。(2)如果一个任务和硬件相关,将其放在中断处理程序中执行。(3)如果一个任务要保证不被其它中断打断,将其放在中断处理程序中执行。(4)其它所有任务,考虑放在下半部执行。当我们开始尝试写自己的驱动程序的时候,读一下别人的中断处理程序和相应的下半部会令你受益匪浅。现在的问题是:下半部具体放到以后的什么时候去做呢?下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让他们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上执行。下半部执行的关键在于当它们运行的时候,允许响应所有中断。

 

6.1.1为什么要用下半部?

因为在中断处理程序运行的时候,当前的中断线会被屏蔽,如果一个处理程序是SA_INTERRUPT类型,它执行的时候会禁止所有本地中断(而且把本地中断线全局屏蔽掉),再加上中断处理程序要与其它程序——甚至是其它的中断处理程序——异步执行。

具体放到以后什么时候去做呢?

在这里,“以后”仅仅用来强调不是“马上”而已,下半部并不需要指明一个确切时间,只是把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了,通常下半部在中断处理程序一返回就会马上执行,下半部执行的关键在于当它们运行的时候,允许响应所有的中断。

 

6.2软中断

软中断是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的。

异步通知所基于的信号也类似于中断。

硬中断是外部设备对CPU的中断

软中断通常是硬中断服务程序对内核的中断。

信号则是由内核(或其它进程)对某个进程的中断。

 

6.2.1软中断的实现

       软中断是在编译期间静态分配的。不像tasklet那样能被动态的注册或去除。软中断由softirq_action结构表示,它定义在<linux/interrupt.h>中:

struct softirq_action {

             void( *action)(struct softirq_action *);         

/*待执行的函数*/

         Void*date;             /传递给函数的参数*/

                                  } ;

在kernel/softirq.c中定义了一个包含有32个该结构体的数组。     

static strcut softirq_action softirq_vec[32]; 每个注册的软中断都占据该数组中的一项。

(1)       软中断处理程序:

软中断处理程序action的函数原型如下:

void softirq_handler(struct softirq_action *)

当内核运行一个软中断处理程序的时候,它就会执行这个action函数,其唯一的参数为指向相应的softirq_action结构体的指针。

个软中断不会抢占另外一个软中断,实际上,唯一可以抢占软中断的是中断处理程序,不过,其它的软中断——甚至是相同类型的软中断——可以在其它处理器上同时执行。

(2)       执行软中断:

一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。软中断被标记后,可以用softirq_pending()检查到这个标记并按照索引号将softirq_pending()的返回值的相应位置1。

在合适的时刻,该软中断就会运行,在下列地方,待处理的软中断会被检查和执行:

在处理完一个硬中断以后

在ksoftirqd内核线程中

在那些显式检查和执行待处理的软中断的代码中,如网络子系统中

不管是用什么办法唤起,软中断都要在do_softirq()中执行,该函数很简单,如果有待处理的软中断,do_softirq()会遍历每一个,调用它们的处理程序。

软中断在do_softirq()中执行。do_softirq()经过简化后的核心部分:

u32 pending = sofeirq_pending(cpu);

if(pending) {

struct softirq_action *h = softirq_vec;

softirq_pending(cpu) = 0;

do {

       if(pending&1) h->action(h);    //调用action函数

       h++;

      pending>>=1;

       }while(pending);

}

 

6.2.2使用软中断

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。内核定时器和tasklets都是建立在软中断上的,如果你想加入一个新的软中断,首先要想想为什么用tasklet实现不了,tasklet可以动态生成,由于它们对加锁的要求不高,所以使用起来也很方便,当然,对于时间要求养并能自己高效的完成加锁工作的应用,软中断会是正确的选择。

1、  分配索引:在编译期间,可以通过<linux/interrupt.h>中定义的一个枚举类型来静态的声明软中断。

2、  注册处理程序:接着,在运行时通过调用open_softirq()注册软件中断处理程序,该函数有三个参数:索引号、处理函数和data域存放的数值。例如网络子系统,通过以下方式注册自己的软中断:

open_softirq(NET_TX_SOFTIRQ, net_tx_action,NULL);

open_softirq(NET_TX_SOFTIRQ, net_rx_action,NULL);

软中断处理程序的执行的时候,允许响应中断,但自己不能睡眠。

3、  触发你的软中断:

通过在枚举类型的列表中添加新项以及调用open_softirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让他在下次调用do_softirq()函数时投入运行。一个例子:

raise_softirq(NET_TX_SOFTIRQ);

这会触发NET_TX_SOFTIRQ软中断。它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断前要禁止中断,触发后再恢复回原来的状态。在中断处理程序中触发软中断是最常见的形式。这样,内核在执行完中断处理程序后,马上就会调用do_softirq。于是软中断开始执行中断处理程序留给它去完成的剩余任务。

 

6.3 Tasklet

tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。它和软中断本质上很相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。

软中断和tasklet怎样选择呢?

通常你应该用tasklet,软中断一般用的很少,它只在那些执行频率很高和连续性要求很高的情况下才需要,而tasklet却有更广泛的用途。

 

6.3.1 Tasklet的实现

因为tasklet是通过软中断实现的,所以它本身也是软中断。

(1)tasklet结构体:tasklet由tasklet_struct结构表示。每个结构体单独代表一个tasklet,它在<linux/interrupt.h>中定义:

struct tasklet_struct {

            struct task_struct   *next;      /*指向链表中的下一个tasklet */

            unsigned   long   state;         /* tasklet的状态 */

             atomic_t    count;                 /* 引用计数器 */

             void (*func) (unsigned long);     /* tasklet处理函数 */

            unsigned long data;                  /*给tasklet处理函数的参数 */

};

结构体中的func成员是tasklet的处理程序,data是它唯一的参数。state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值TASKLET_STATE_SCHED表明tasklet已经被调度,正准备投入运行,TASKLET_STATE_RUN表示该tasklet正在运行。只有count为0时,tasklet才被激活,否则被允许,不允许执行。

 

调度tasklet

已调度的tasklet存放在两个单处理器数据结构:tasklet_vec和task_hi_vec中。它们都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。

tasklet是由tasklet_schedule()和tasklet_hi_schedule()函数进行调度的,它们接受一个指向tasklet_struct结构的指针作为参数。

Tasklet的实现通过软中断来实现的,tasklet_schedule()调度函数执行一些初始工作,紧接着唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行tasklet。

那么do_softirq()函数什么时候执行呢?

do_softirq()会尽可能早的在下一个合适的时机执行,由于大部分tasklet和软中断都是在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq()的最佳时机。因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq会执行相应的软中断处理程序。

Tasklet_action()和Tasklet_hi_action()两个处理程序就是tasklet处理的核心。

总结:所有的Tasklets都通过重复运用TASKLET_SOFTIRQ或HI_SOFTIRQ这两个软中断实现,当一个tasklet被调度时,内核就会唤起这两个软中断中的一个,随后,该软中断会被特定的函数处理,执行所有已调度的tasklet,这个函数保证同一时间里只有一个给定类别的tasklet会被执行(但其它不同类型的tasklet可以同时执行),所有这些复杂性都被一个简洁的接口隐藏起来了。

 

6.3.2使用tasklet

声明你自己的tasklet

可以静态创建,也可以动态创建,分别对应直接引用和间接引用。选择哪种方式取决于你到底是有(或者是想要)一个对tasklet的直接引用还是间接引用,静态创建一个tasklet(也就是有一个直接引用),使用下面<linux/interrupt.h>中定义的两个宏中的一个:

 

DECLARE_TASKLET(name, func, data)

实现了定义名称为name的tasklet并将其与func这个函数绑定,而传入这个函数的参数为data。

DECLARE_TASKLET_DISABLED(name, func, data);

DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);运行代码实际上等价于:

struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), my_tasklet_handler, dev };    这样就创建了一个名为my_tasklet,处理程序为tasklet_handler并且已经被激活的tasklet。

还可以通过一个间接引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet:

Tasklet_init(t,tasklet_handler,dev);/*动态而不是静态创建*/

 

编写你自己的tasklet处理程序

必须符合规定的函数类型:                                               

void tasklet_handler(unsigned long data)

因为是靠软件中断实现,所以tasklet不能睡眠,这意味着你不能在tasklet中使用信号量或者其它什么阻塞式的函数。

 

调度你自己的tasklet

通过调用task_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行。

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

 

下面我们看一下软中断和tasklet的异同:

在前期准备工作上,首先要给软中断分配索引,而tasklet则要用宏对处理程序声明。在给软中断分配索引后,还要通过open_softirq()函数来注册处理程序。这样看来,tasklet是一步到位,直接到了处理函数,而软中断需要做更多工作。接下来软中断要等待触发(raise_softirq()或raise_softirq_irqoff),而tasklet则是等待tasklet_schedule()和tasklet_hi_schedule()对其进行调度。两者虽然在命名上不同,但殊途同归,最终的结果都是等待do_softirq()去执行处理函数,即将下半部设置为待执行状态以便稍后执行。另外,在tasklet的tasklet_schedule()中,需要完成的动作之一便是唤起(触发)TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,说明tasklet仍然是基于软中断的。在进入do_softirq()之后,所做的工作仍然有所不同,不再论述。

软中断和工作队列都是异步发生的(就是说,在中断处理返回的时候)

 

6.4工作队列

 

工作队列(work queue)是另外一种将工作推后执行的形式,他和我们前面讨论过的其他形式完全不同。工作队列可以把工作推后,交由一个内核线程去执行——这个下半部总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的是工作队列允许重新调度甚至是睡眠。

如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列,它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠,这意味着你在你需要获得大量的内存时,在你需要获取信号量时,在你需要执行阻塞式的IO操作时,它都会非常有用,如果你不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet吧!

 

6.4.1工作队列的实现

工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的其他任务。它创建的这些内核线程被称作工作者线程。工作队列可以让驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个默认的工作者线程来处理这些工作。因此,工作队列最基本的表现形式就转变成了一个把需要推后执行的任务交给特定的通用线程这样一个接口。

 

表示线程的数据结构

工作者线程用workqueue_struct结构表示:

struct workqueue_struct {

            struct cpu_workqueue_struct cpu_wq[NR_CPUS];

            const char *name;

           struct   list_head   list;

          };

该结构内是一个由cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c中,数组的每一项对应一个系统中的处理器。每个工作者线程都对应这样的cpu_workqueue_struct结构体。cpu_workqueue_struct是kernel/workqueue.c中的核心数据结构:   

struct cpu_workqueue_struct     {

                 spinlock_t    lock;             /* 锁定以便保护该结构体 */

                 long    romove_sequeue; /* 最近一个被加上的(下一个要运行的) */

                 long   insert_sequeue;     /*下一个要加上的   */

                wait_queue_head_t    more_work;

                wait_queue_head_t     work_done;

                struct   workqueue_struct   *wq;           /* 有关联的workqueue_struct结构 */

                task_t    *thread;                                 /* 有关联的线程 */

               int   run_depth;                                     /* run_workqueue()循环深度   */

               };

由此可以看出,每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的线程。

表示工作的数据结构

        所有工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数(worker_thread)开始休眠。当有操作被插入到队列的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续睡眠。

工作用<linux/workqueue.h>中定义的work_struct结构体表示:  

struct    work_struct {

                      unsigned   long   pending;        /* 这个工作是否正在等待处理 */

                     struct   list_head entry;             /* l连接所有工作的链表 */

                    void   (* func) (void *);               /* 处理函数 */

                   void   *wq_data;                         /* 内部使用 */

                    struct timer_list timer;               /* 延迟的工作队列所用到的定时器 */

};

这些结构体被连接成链表,在每个处理器的每种类型的队列都对应这样一个链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。当工作完毕时,他会将相应的work_struct对象从链表中移去,当链表上不再有对象的时候,它就会继续睡眠。

 

6.4.2使用工作队列

(1)创建推后的工作

首先要做的是实际创建一些需要推后执行的工作。可以通过DECLARE_WORK在编译时静态的创建该结构体:

DECLARE_WORK(name, void (*func) (void *), void *data);

这样就会静态的创建一个名为name,处理函数为func,参数为data的work_struct结构体。也可以在运行时通过指针创建一个工作:

INIT_WORK(struct work_struct *work, void (*func)(void *), void   *data);

这样就动态的初始化了一个由work指向的工作。

(2)工作队列的处理函数

原型是:void   work_handler(void   *data)

这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中,默认情况下,允许响应中断,并且不持有任何锁,如果需要,函数可以睡眠,注意的是,尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射,通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。

(3)对工作进行调度

现在工作已经创建,我们可以调度它了,要把给定工作的处理函数提交给默认的events工作线程,只需调用: schedule_work(&work); work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。

(4)刷新操作

刷新工作队列的函数就是确保在卸载模块之前,要确保一些操作已经执行完毕了,该函数如下:

Void flush_scheduled_work(void);

该函数会一直等待,直到队列中所有对象都被执行以后才返回,在等待所以待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。

 

(5)创建新的工作队列

当缺省的队列不能满足你的需要时,你应该创建一个新的工作队列和与之对应的工作者线程。

 

 

 

6.5下半部之间的选择

1 从设计的角度考虑

软中断提供的执行序列化的保障最少,这就要求软中断必须采取一些步骤确保共享数据的安全。如果被考察的代码本身多线索化的工作就做得非常好,软中断就很好,对于时间要求严格和执行频率很高的话,它执行的也快。如果代码本身多线索化的工作就做得不充分,就选择tasklet比较好,由于两个同种类型的tasklet不能同时执行,实现起来也很简单一些。

2 如果你需要把任务推到进程上下文中完成,只能选择工作队列。

如果不需要睡眠,那么软中断和工作队列就更合适。工作队列造成的开销最大,因为他要牵扯到内核线程甚至是上下文切换。

3 说到易用性,工作队列最好,使用缺省的events队列简直不费吹灰之力。接下来就tasklet。他的的接口很简单,最后才是软中断,它必须静态创建。

 

6.6在下半部之间加锁

使用tasklet的一个好处是在于它自己负责执行的序列化保障,两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行,意味着你无须考虑相同类型的tasklet内部的同步问题。当然,tasklet之间的同步(两个不同类型的tasklet共享同一数据时)需要正确使用锁机制。

因为软中断根本不保障执行序列化,(即使相同类型的软中断也有可能有两个实例在同时执行)所以所有的共享数据都需要合适的锁。

如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。

如果中断上下文和一个下半部共享数据,在访问数据之前,你需要禁止中断并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。

任何在工作队列中被共享的数据也需要使用锁机制,其中有关锁的要点和在一般内核代码中没什么区别,因为工作队列本来就是在进程上下文中执行的。

 

禁止下半部

一般单纯禁止下半部的处理是不够的,为了保证共享数据的安全,更常见的做法是先得到一个锁然后在禁止下半部的处理,驱动程序中通常使用的都是这种方法。


你可能感兴趣的:(中断_中断下半部的一些分析)