中断的本质是一种特殊的电信号,由硬件发向处理器,处理器接收到中断后会做出相应的处理
中断由硬件产生并直接送入中断控制器的输入引脚,中断控制器的作用是使用复用技术将多路终端管线只通过一个和处理器相连接的管线与处理器通信
不同设备对应的中断不同,而每个中断都通过一个唯一的数字标志,这样处理器才能给不同的中断对应不同的中断处理程序
这些中断值被称为中断请求(IRQ)线,每个IRQ线都会被关联一个数值量
与异常的区别
异常在产生时必须考虑与处理器时钟同步,实际上异常也被叫做同步中断,异常由处理器产生而非硬件
许多处理器体系结构处理异常的方式与处理中断的方式十分类似
内核响应中断时会执行一个函数,被称为中断处理程序(interrupt handler)或者中断服务例程(interrupt service routine,ISR),产生中断的每个设备都有一个相应的中断处理程序
一个设备的中断处理程序是它设备驱动程序的一部分
在Linux中,它就是普通的C函数,只不过这些函数必须按照特定的类型声明,以便内核能够以标准的方式传递处理程序的信息
与其他内核函数的区别在于,中断处理程序是内核用来响应中断的,运行于中断上下文(或原子上下文,表示这其中的代码不可阻塞)
中断处理程序应当快速的响应,快速的执行,快速的恢复
有的中断处理程序所面临的任务也是工作量很大的,比如网络设备的中断处理程序,会把来自硬件的网络数据包拷贝到内存
我们既想要中断处理程序运行的快,又想它完成的工作量多,这两个目的显然是有所抵触的
所以Linux内核将中断处理切为两个部分
上半部(top half)是接收到中断就立即执行的,只做有严格时限的工作,这些工作是在所有中断被禁止的情况下所作的
下半部(bottom half)中执行那些允许稍后完成的工作,在合适的时机执行中断的下半部
这样的处理是与单片机的中断处理不相同的,单片机的中断服务函数在触发中断后就立刻被执行,所以说如果中断中做的事情较多的话对整个系统的影响是很大的
调用request_irq
声明在linux/interrupt.h
extern int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev);
irq为中断号
handler是一个指针,指向这个中断的实际处理程序
flags为一个或多个标志位
name是设备的ASCII文本表示
dev用于共享中断线,如果不需要共享中断线时传NULL,如果需要共享的话就必须传递唯一的信息。当一个中断处理程序需要什邡市,dev提供唯一的标志信息(cookie)以便从共享中断线的诸多中断程序汇总删除指定的那一个
该函数成功执行后会返回0,并且中断处理程序被注册,否则会返回错误值
extern void free_irq(unsigned int, void *);
如果中断线不是共享的那么删除处理程序的同时禁用这条中断线,如果是共享的则删除dev所对应的处理程序,在删除最后一个处理程序时才会禁用中断线
一个中断处理程序的声明:static irqreturn_t intr_handler(int irq,void *dev)
irq为中断的中断号,这个参数已经没有什么实际的用处了
dev是一个通用指针,在共享时使用
返回值是一个特殊的类型irqreturn_t,可能返回两个特殊的值IRQ_NONE和IRQ_HANDLED,IRQ_NONE表示该中断对应的设备并不是在注册处理函数期间指定的产生源,IRQ_HANDLED表示被正确的调用
中断处理函数扮演什么角色取决于产生中断的设备和该设备为什么要发送中断,起码它要告诉产生中断的设备它已经收到了中断
重入:Linux中的中断处理程序是无需重入的,在处理某条中断线的中断时,相应的中断线在所有的处理器上都会被屏蔽掉,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断
中断上下文具有严格的时间限制,因为它打断的其他的代码,所以它不可以睡眠,也尽量不使用循环去处理繁重的工作,中断处理程序必须尽可能的迅速、简洁,尽量吧工作从中断处理程序中分离出来,放在稍后的下半部中执行
中断处理程序拥有自己的程序栈,每个处理器一个,大小为一页
存放系统中与中断相关的统计信息
执行与中断处理密切相关但中断处理程序本身不执行的工作,理想情况下,最好中断处理程序将所有工作都交给下半部分执行
一些好的建议:
如果一个任务对时间非常敏感,将其放在中断处理程序中执行
如果一个任务和硬件相关,将其放在中断处理程序中执行
如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行
其他所有任务考虑放置在下半部执行
下半部执行的关键在于他们运行的时候允许响应所有的中断
上半部屏蔽一些或所有中断,快速执行,下半部稍后执行且期间可以响应所有的中断,这种设计可以使中断的屏蔽时间尽可能的短,提高系统的响应能力
处理方法:BH机制、任务队列、软中断和tasklet、工作队列
2.3版本引入了软中断和tasklet;2.5版本BH接口被弃置,任务队列被工作队列取代
软中断用的比较少,tasklet是下半部更常用的一种形式,但是tasklet是通过软中断实现的
软中断是在编译期间静态分配的,不像tasklet那样能动态的注册和注销
软中断的结构
struct softirq_action
{
void (*action)(struct softirq_action *);
};
一个包含32个以上结构体的数组static struct softirq_action softirq_vec[NR_SOFTIRQS]
最多只能有32个软中断,这是一个定值,在2.6版本中只用到了9个
软中断的处理程序
void softirq_handler(struct softirq_action *)
参数为softirq_action结构体的指针
一个注册的软中断必须在被标记后才会执行,这被称作触发软中断,通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行
软中断保留给系统中对时间要求最为严格以及最重要的下半部使用,目前,只有网络和SCSI直接使用软中断,内核定时器和tasklet都是建立在软中断上的,如果你想加入一个新的软中断应当先考虑为什么用tasklet实现不了,tasklet可以动态生成,由于它们对加锁的要求不高,所以使用起来很方便,性能也非常不错,对于时间要求严格并能自己高效地完成加锁工作的应用,软中断会是正确的选择
触发软中断:raise_softirq()可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入使用
在中断处理函数中触发软中断是最常见的形式,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出,内核在执行完中断处理程序以后马上会调用do_softirq()函数,软中断执行中断处理程序留给它的剩余任务
利用软中断实现的一种下半部机制,接口更简单,锁保护要求较低
tasklet由两类软中断代表:HI_SOFTIRQ、TASKLET_SOFTIRQ
两者的唯一区别在于HI_SOFTIRQ优先级更高
tasklet结构体
struct tasklet_struct
{
struct tasklet_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表示正在运行(只有在多处理器系统才有用)
count引用计数器,如果不是0则表示tasklet被禁止,不允许执行,只有当它为0时才被激活
已调度的tasklet存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet),两个数据结构都是由tasklet_struct结构体构成的链表,链表中每个节点代表一个不同的tasklet
tasklet由tasklet_schedule()和tasklet_hi_schedule()来调度,接收一个指向tasklet_struct结构体的指针作为参数
tasklet_schedule()的实现细节:
1、检查tasklet的状态是否为TASKLET_STATE_SCHED,如果是说明已经被调度,直接返回
2、调用__tasklet_schedule()
3、保存中断状态,然后禁止本地中断,这样能保证当tasklet_schedule()处理这些tasklet时处理器上的数据不会弄乱
4、把需要调度的tasklet加到每一个处理器一个的tasklet_vec或tasklet_hi_vec链表的表头
5、唤醒TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_tasklet时就会执行该tasklet
6、恢复中断到原状态并返回
do_tasklet会执行相应的软中断处理程序,而这两个处理程序tasklet_action和tasklet_hi_action就是tasklet处理的核心,他们的细节:
1、禁止中断,并检索tasklet_vec或tasklet_hi_vec链表
2、将当前处理器上的该链表设置为NULL,达到清空的效果
3、允许响应中断
4、循环遍历获得链表上每一个待处理的tasklet
5、如果是多处理器系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否正在其他处理器上运行,如果它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去(同一时间相同类型的tasklet只能有一个执行)
6、如果这个tasklet没有执行,就将其状态设置为TASKLET_STATE_RUN,禁止别的处理器去执行它
7、检查count值是否为0,确保tasklet没有被禁止,如果tasklet被禁止了,则跳到下一个挂起的tasklet去
8、执行tasklet的处理程序
9、运行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志
10、重复执行下一个tasklet直至没有剩余的等待处理的tasklet
一个tasklet总在调度它的处理器上执行--这是希望能更好地利用处理器的高速缓存
内核中软中断的实现方法是不会立即处理重新触发的软中断,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载,这些线程在最低的优先级上运行(nice值为19),这样能避免他们跟其他重要的任务抢夺资源,但它们最终肯定会被执行
每个处理器都有一个这样的线程,叫做ksoftirqd/n,区别在于n,对应处理器的编号,只要有待处理的软中断,ksoftirqd就会调用do_softirq去处理他们
可以把工作推后,交由一个内核线程去执行——这个下半部总是会在进程上下文中执行,工作队列允许重新调度甚至是睡眠
选择工作队列还是软中断/tasklet的判断依据就是推后执行的任务是否需要睡眠,需要就选工作队列,不需要就选择后者
工作队列子系统是一个用于创建内核线程的接口,它创建的这些内核线程叫做工作者线程(worker thread),工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作
缺省工作者线程叫做events/n,n为处理器编号,如果不是一个驱动程序或者子系统需要一个单独的属于它自己的内核线程,最好使用缺省线程
软中断提供的执行序列化的保障最少,就要求软中断处理函数必须格外小心地采取一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行。对于时间要求严格和执行频率很高的应用来说,它执行得也最快
tasklet接口简单而且两个相同类别的tasklet不能同时执行,实现起来也更简单,是有效地软中断,但不能并发运行,驱动开发者应尽可能选择tasklet而不是软中断
如果需要把任务推后到进程上下文中完成,选择工作队列,如果推后到进程上下文中(需要睡眠)不是必须的,选择软中断和tasklet更好,工作队列造成的开销最大,因为它要牵扯到内核线程甚至是上下文切换
易用性:工作队列>tasklet>软中断