中断:
不同的设备对应的中断不同,而每个中断都通过一个惟一的数字标识。从而使得操作系统能够对中断进行区分,并知道哪个设备产生了哪个中断。这样,操作系统才能给不同的中断提供不同的中断处理程序。这些中断值通常被称为中断请求(IRQ)线。通常IRQ都是一些数值量。例如在PC机上,IRQ0是时钟中断,IRQ1是键盘中断。但并非所有中断号都是这样严格定义。例如,对于连接到PCI总线上的设备而言,中断是动态分配的。硬件设备生成中断的时候并不考虑与处理器的时钟同步,也就是说中断随时都可以产生。
异常:
异常与中断不同,它在产生时必须考虑与处理器时钟同步。异常也常常称为同步中断。在处理器执行到由于编程失误导致的错误(被0除),或是在执行期间出现特殊情况(例如缺页),必须靠内核来处理的时候,处理器就会产生一个异常。
上半部与下半部:
我们的目的是即让程序运行得快,又要完成的工作量多,这两个目的显然有所抵触。所以我们把中断处理切为两个部分。上半部做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部。
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags, const char *devname, void *dev_id)
在给定的中断线上注册一个给定的中断处理程序。讲几个重要的参数。
flags可以为0也可以是:
SA_INTERRUPT:表示在本地处理器上,快速中断处理程序在禁止所有中断的情况下进行。而默认情况下(没有这个标志),除了正在运行的中断处理程序对应的那条中断线被屏蔽外,其他所有中断线都是激活的。除了时钟中断外,绝大多数中断都不使用此标志。
SA_SAMPLE_RANDOM:此标志表明这个设备产生的中断对内核熵池有贡献。内核熵池负责提供从各种随机事件导出的真正的随机数。
SA_SHIRQ:此标志表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个中断处理程序必须指定这个标志;否则,在每条中断线上只能有一个处理程序。
dev_id:主要用于共享中断线。dev_id对应共享中断线上的某个中断处理程序。如果无需共享中断线,那么该参数给NULL就OK了。平常我们一般通过它传递驱动程序的设备结构。
这个函数不能在中断上下文中调用,因为它会睡眠。
free_irq(unsigned int irq, void *dev_id)
如果指定中断线不是共享的,那么删除中断处理程序,并将禁用这条中断线。如果是共享的,则仅删除dev_id对应的中断处理程序,而这条中断线本身只有在删除了最后一个中断处理程序时才会被禁用。
软中断的执行过程:
当某一软中断发生后,首先要设置对应的中断标记位,触发中断事务,然后唤醒守护线程去检测中断状态寄存器,如果查询发现某一软中断事务被触发,那么通过软中断向量表调用软中断服务程序。这就是软中断的过程,与硬件中断唯一不同的地方是从中断标记到中断服务程序的映射过程。在CPU的硬件中断发生之后,CPU需要将硬件中断请求通过向量表映射成具体的服务程序,这个过程是硬件自动完成的,但是软中断不是,其需要守护线程去实现这一过程,这也就是软件模拟的中断,故称之为软中断。
为什么要用下半部:
1.因为中断运行的时候当前的中断线在所有处理器上都会被屏蔽,所以我们希望尽量减少中断处理程序中需要完成的工作量。
2.中断处理程序有时需要与其他程序--甚至是其他的中断处理程序--异步执行。
基于以上两个原因,我们必须尽力缩短中断处理程序的执行,解决方法就是把一些工作放到以后去做。这个“以后”时间是指系统不太繁忙并且中断处理程序已经返回的时候。
软中断、tasklet、工作队列的选择:
软中断和tasklet的最大区别就是:它们同样都禁止本地中断,不允许睡眠,不同点就在于多个处理器可以同时执行同一个软中断,而不可以同时执行同一个tasklet。
软中断:
1.软中断是在编译期间静态分配的,不像tasklet那样能被动态地注册或去除。
2.对时间要求严格的。目前只有两个子系统直接使用软中断--网络和SCSI。
3.允许响应中断,但它自己不能休眠。
4.当前处理器上的软中断被禁止,但其他处理器上仍可以响应软中断。如果同一个软中断在它执行时再次被触发了,那么另外一个处理器可以同时运行其处理程序。这就意味着任何共享数据--甚至是仅在软中断处理程序内部使用的全局变量--都需要严格的锁保护。
5.注册软中断处理程序,open_softirq(int nr, void (*action)(struct softirq_action *));第一个参数是软中断索引号,第二个参数是处理函数。
6.触发软中断,raise_softirq(unsigned int nr);将一个软中断设置为扶起状态,在下次调用do_softirq()函数时投入运行。该函数在触发一个软中断前先要禁止中断,触发后再恢复回原来的状态。
如果中断本来就已经被禁止,那么可以调用raise_softirq_irqoff(unsigned int nr);
软中断都是在中断处理程序(也就是上半部)中被触发的。
tasklet:
1.tasklet可以动态生成,它对加锁要求不高,所以使用起来方便。如果对时间要求严格并且能自己高效地完成加锁工作,软中断会是正确的选择。
2.同一个处理程序的多个实例不能在多个处理器上同时运行。
3.每一个tasklet对应着一个struct tasklet_struct结构体。因为tasklet是通过软中断实现的,所以它本身也是软中断。tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者唯一区别就是前者比后者优先执行。
4.静态创建一个tasklet:
#define DECLARE_TASKLET(name, func, data) //把引用计数设置为0,tasklet处于激活状态
#define DECLARE_TASKLET_DISABLED(name, func, data) //把引用计数设置为1,tasklet处于禁止状态
动态创建一个tasklet:
tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
5.编写自己的tasklet处理程序,void tasklet_handler(unsigned long data)
编写的时候注意tasklet不能睡眠,这意味着不可以使用信号量或其他什么阻塞式函数。由于tasklet运行时允许响应中断,所以如果tasklet和中断处理程序之间共享数据的话要做好预防工作(比如屏蔽中断然后获取一个锁)。
6.tasklet由tasklet_schedule()和tasklet_hi_schedule()函数触发。已触发的tasklet存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级tasklet)中。这两个数据结构都是由tasklet_struct结构体构成的链表。
7.tasklet中断处理程序,tasklet_action()和tasklet_hi_action()是tasklet处理的核心。所有的tasklet都通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ两个软中断来实现。当一个tasklet被触发时,内核就会唤起这两个软中断中的一个,然后会调用tasklet_action()或tasklet_hi_action()函数来处理已触发的tasklet,并且这两个函数保证同一时间里只有一个给定类别的tasklet被执行(但其他不同类型的tasklet可以同时执行)。
ksoftirqd:
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。
由于软中断被触发的频率有时可能很高,另外一方面软中断还可以自行重复触发(就是当一个软中断执行的时候,它可以重新触发自己以便再次得到执行),这样处理器就会一直忙于处理软中断,导致用户进程无法获得足够的处理时间,因而处理饥饿状态。
如果不处理立刻重新触发的软中断,而是放到下一次中断返回的时候,这就等于说一定要等一段时间,新的或重新触发的软中断才能被执行。这样就能保证用户空间的进程不会处于饥饿状态,但如果在等的这段时间内系统处于空闲状态的话,立即处理软中断才是比较好的办法,所以它又可能让软中断处于饥饿状态,并且根本没有好好利用闲置的系统资源。
Linux中采用的方案是不立即处理重新触发的软中断。当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。所以便有了ksoftirqd线程。这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。但它们最终肯定会被执行,所以,这个折中方案能够保证在软中断负担很重的时候用户程序不会因为得不到处理而处于饥饿状态,也能保证大量的软中断会得到及时处理。
工作队列:
把工作推后,由一个内核线程去执行,允许重新调度和睡眠。
静态创建:
DECLARE_WORK(name, void (*func)(void *), void *data);
动态创建:
INIT_WORK(struct work_struct *work, void(*func) (void *), void *data);
工作队列处理函数:
void work_handler(void *data);这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下允许响应中断,要注意的是,尽管处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间。也只有在此时它才会映射用户空间的内存。
对工作进行调度:
schedule_work(&work);
上述整理自《Linux内核设计与实现》
----------------------------------------------------------------------
中断上下文为什么不能进行调度(不能睡眠)?
中断处理与进程切换有一个明显的差异:由中断或异常处理程序执行的代码不是一个进程,更确切的说,它是一个内核控制路径,代表当前进程在内核态执行单独的指令序列。内核控制路径可以任意嵌套,一个中断处理程序可以被另一个中断处理程序“中断”。允许内核控制路径嵌套必须付出代价,那就是中断处理程序必须永不阻塞,也就是说,不能发生进程切换。嵌套的内核控制路径恢复执行时需要的所有数据都存放在内核态堆栈中,这个栈毫无疑义的属于当前进程。
中断调用过程:
1.进入中断处理程序--->2.保存关键上下文---->3.开中断(sti指令)--->4.进入中断处理程序的handler--->5.关中断(cli指令)---->6.写EOI寄存器(表示中断处理完成)---->7.开中断。
在关中断的时候不能sleep,因为时钟中断无法触发. 但不是所有情况下, 在关中断时sleep都会导致系统死掉, 在SMP的情况下, 可能系统不会死掉.
中断handler不是没有上下文, 而是没有固定的上下文,中断的上下文就是抢占的任务A的上下文。
如果使用被抢占的任务作为上下文:一,自身的处理无法得到实时保障,导致系统不确定性, 二,任务受到影响.
比方说在IRQ1线上来了个中断,执行到此中断handler时睡眠了。任何时候,系统中任何任务都有可能再次发起中断,如果发起的中断请求正好又经过IRQ1,那么因为此中断请求的handler之前睡眠了,所以此任务也因此而睡眠,所以任务就会受到影响,而且中断handler的实时性也得不到保障。所以不能睡眠。
其实不是不能睡眠,而是为了实现睡眠需要做很多工作,增加了内核的复杂性。还有睡眠之后不是不能被唤醒,可以被唤醒。因为中断handler时是开中断的,可以被时钟中断唤醒啊。例如:
schedule_timeout_interruptible(1000)
这样当前任务就被加入到定时器中, 定时器软中断到了1000ms就会把handler抢占的任务唤醒.
解决:
给中断handler提供固定的内核线程上下文!
这样, 中断不能sleep, 但中断的handler可以sleep!
为每个中断号创建一个内核任务, 中断入口函数do_irq只是唤醒相应的中断任务, 中断任务去执行相应的handler.
好处:
提高了系统的实时性.
坏处:
降低了中断, 软中断的实时性, 所以不是所有的中断handler都可以在固定内核任务上下文中处理. 一般来说, 时钟中断必须保证其实时性, 所以留在中断上下文中.
详细讨论亦可见帖:
上面是之前梳理的,现在看来有点乱,结论还是不明了,今天又搜了一下,感觉这个答案比较靠谱:
Linux是以进程为调度单位的,调度器只看到进程内核栈,而看不到中断栈。在独立中断栈的模式下,如果linux内核在中断路径内发生了调度(从技术上讲,睡眠和调度是一个意思),那么linux将无法找到“回家的路”,未执行完的中断处理代码将再也无法获得执行机会。
这个结论来自:http://blog.csdn.net/maray/article/details/5770889
阅读(2120) | 评论(0) | 转发(2) |