您的位置:首页 > 其它

中断上半部,下半部/软中断/tasklet/工作队列

2018-01-21 16:19 253 查看
转自:http://blog.csdn.net/jin13277480598/article/details/51019762

本文回答了为什么引入中断上部分、下部分以及上半部和下半部各自的分工;同时重点分析了下半部的三种机制及tasklet和工作队列的使用模块,能对整个框架有一个清晰的认识。


1. 为什么引入中断上半部、下半部

(1)为了解决一个矛盾体:又想中断处理程序运行快,又想中断处理程序完成的工作量多。 

(2)中断处理程序本身局限性,使得它只能完成整个中断处理流程的上半部分。

这些局限包括: 

①中断处理程序以异步方式执行,可能打断其他重要代码(包括其他中断程序)的执行,故中断处理程序执行速度越快越好。 

②当中断处理程序执行时,如其没有设置了中断屏蔽,同级的其他中断被屏蔽;设置后,其他中断都会屏蔽,硬件与操作系统无法通信。故其速度越快越好。 

③中断处理程序往往需要对硬件操作,所以它们通常要求速度越快越好。 

④中断处理程序在程序上下文运行,所以不能阻塞。这限制了它们所做的事情。 

引入中断上半部、下半部之后,称为“顶半部”的部分,是实际响应中断的例程,也就是用request_irq注册的中断例程;而所谓”底半部”是一个被顶半部调度,并在稍后安全的时间内执行的例程。


2.中断上半部、下半部的比较

上(顶)半部[中断处理程序]下(底)半部[推后处理程序]
完成尽可能少的紧急硬件操作完成延缓的耗时操作,中断事情的大部分任务
关中断可以开中断,允许中断请求
内核立即运行稍后完成


3.怎样区分上半部、下半部工作

没有严格规则,以下可供借鉴: 

①任务对时间敏感,上半部; 

②任务和硬件相关,上半部; 

③任务不想被中断打断,上半部; 

④其他所有任务,下半部。


4.下半部机制的引入

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行”登记中断”的工作。”登记中断”意味着将底半部处理程序挂到设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
现在,中断处理工作的重心就落到了底半部上头,需要它来完成中断事件的绝大多数任务。下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了。下半部执行的关键在于当它们运行时候,允许响应所有的中断。

但是内核到底什么时候执行下半部,以何种方式组织下半部?这就涉及到下半部机制。
1
2
3
4
5


5.下半部机制

在2.6这个当前版本中,内核提供了三种不同形式的下半部实现机制:软中断、tasklet和工作队列。
在实际中,软中断使用得比较少,而tasklet是下半部更加常用的一种形式,但是tasklet是通过软中断实现的。
1
2
3


5. 1软中断

出现在内核代码中的术语”软中断”常常表示可延迟函数(包括软中断和tasklet)的所有总类,即有时候软中断也包括tasklet(应该具体看上下文理解软中断的意思)。
1
2

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前只有两个子系统直接使用软中断:网络和SCSI。Linux2.6使用有限个软中断,在2.6.11中经查得为32个。 

软中断的其他特性: 

1. 软中断是在编译期间静态分配的。 

2. 产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。 

3. 可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作), 因此也需要使用自旋锁来保护其数据结构。 

4. 软中断运行在中断上下文,软中断处理函数中不能睡眠。 

5. 在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体 包含软中断处理函数指针和传递给该函数的指针的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。


5.2 tasklet


5.2.1 tasklet特性

tasklet是在软中断之上实现的(实际上tasklet只是在软中断的基础上添加了一定的机制)。tasklet是IO驱动程序中实现可延长函数的首选方法。tasklet是建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ软中断之上。
tasklet通常是底半部的优选机制;因为这种机制非常快,但是所有的tasklet代码必须是原子的。
1
2
3


5.2.2 tasklet和软中断的区别

由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。Tasklet的串行化使tasklet函数不必可重入,简化设备开发者的工作。

它具有以下特性: 

1. tasklet可以被多次调度,但是调度不会累积。即实际只会运行一次。 

2. 一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。不会有同一个tasklet的多个实例并行的运行,因为它们只运行一次。 

3. 类型不同的tasklet可以在几个CPU上并行执行。 

4. 软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。 

5. tasklet运行在中断上下文,tasklet处理函数中不能睡眠。


5.2.3 tasklet和软中断的选择

大多数情况下都是用tasklet,只有在哪些执行频率很高和连续性要求很高的情况下才需要使用软中断。写驱动时候,基本都用tasklet,使用相对方便。
1
2


5.2.4 tasklet的使用及模块

/*tasklet的使用简单,我们只需要定义tasklet及其函数,并将两者关联即可,如:*/

void my_tasklet_handler(unsigned long data);/*定义一个处理函数*/
DECLARE_TASKLET(my_tasklet,my_tasklet_handler,dev);
/*定义一个tasklet结构my_tasklet,与my_tasklet_handler(dev)函数相关联*/
/*需要调度时候tasklet引用一个tasklet_schedule()函数*/
tasklet_schedule(&my_tasklet)

相关模块:
/* 定义tasklet和底半部函数并把它们关联*/
void my_tasklet_handler(unsigned long data);
DECLARE_TASKLET(my_tasklet,my_tasklet_handler,0);

/*中断处理下半部,即我们要在下半部处理的程序*/
void my_tasklet_handler(unsigned long data)
{...}

/*中断上半部*/
irqreturn_t xxx_interrupt(int irq , void *dev_id)
{
...
tasklet_schedule(&my_tasklet)
}

/*设备驱动模块加载函数*/
int _init xxx_init(void)
{
...
/*申请中断*/
result = request_iq(xxx_irq , xxx_interrupt, 0 ,"xxx",NULL);
...
return IRQ_HANDLED;

/*设备驱动模块卸载函数*/
void _exit xxx_exit(void)
{
...
/*释放中断*/
free_irq(xxx_irq,xxx_interrupt);
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42


5.3 工作队列


5.3.1 工作队列的特性

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


5.3. 2工作队列和tasklet的选择

那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。 

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


5.3.3 工作队列的使用及模块

/*工作队列可以调度和睡眠。定义一个工作队列和一个底半部函数:*/
struct work_struct my_wq;
void my_wq_handler(struct work_struct *work);

/*通过INIT_WORK()可以初始化这个工作队列并将其与底半部处理函数绑定*/
INIT_WORK(&my_wq,my_wq_handler)

/*用于调度工作队列的函数*/
schedule_work(&my_wq);
/*my_wq马上会被调度,一旦其所在的处理器上的my_wq线程被唤醒,它就会被执行,有时候希望它经过一段时间再执行,使用:schedule_delayed_work(&my_wq,delay);*/

相关模块:
/* 定义工作队列和底半部函数并把它们关联*/
struct work_struct my_wq;
void my_wq_handler(struct work_struct *work);

/*中断处理下半部*/
void my_wq_handler(struct work_struct *work)
{...}

/*中断上半部*/
irqreturn_t xxx_interrupt(int irq , void *dev_id)
{
...
schedule_work(&my_wq);
...
return IRQ_HANDLED;

}

/*设备驱动模块加载函数*/
int _init xxx_init(void)
{
...
/*申请中断*/
result = request_iq(xxx_irq , xxx_interrupt, 0 ,"xxx",NULL);
...

/*初始化工作队列,相比tasklet模块,多了初始化工作*/
INIT_WORK(&my_wq,my_wq_handler)

/*设备驱动模块卸载函数*/
void _exit xxx_exit(void)
{
...

/*释放中断*/
free_irq(xxx_irq,xxx_interrupt);

...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

参考文献: 

《Linux设备驱动开发详解》 

《 Linux内核设计与实现》 

《深入理解Linux内核》 

《Linux设备驱动程序》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: