进程上下文和中断上下文是操作系统中很重要的两个概念,这两个概念在操作系统课程中不断被提及,是最经常接触、看上去很懂但又说不清楚到底怎么回事。造成这种局面的原因,可能是原来接触到的操作系统课程的教学总停留在一种浅层次的理论层面上,没有深入去研究。
处理器总处于以下状态中的一种:
1、内核态,运行于进程上下文,内核代表进程运行于内核空间;
2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;
3、用户态,运行于用户空间。
用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
关于进程上下文LINUX完全注释中的一段话:
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程的执行。
这个问题有很多人问过,我看了下linux得内核代码,原因如下当然我不能保证一定对,如果有牛人理解得更好,欢迎指正)
1.中断处理的时候,不应该发生进程切换,因为在中断context中,唯一能打断当前中断handler的只有更高优先级的中断,它不会被进程打断(这点对于softirq,tasklet也一样,因此这些bottom half也不能休眠),如果在中断context中休眠,则没有办法唤醒它,因为所有的wake_up_xxx都是针对某个进程而言的,而在中断context中,没有进程的概念,没有一个task_struct(这点对于softirq和tasklet一样),因此真的休眠了,比如调用了会导致block的例程,内核几乎肯定会死.
2.schedule()在切换进程时,保存当前的进程上下文(CPU寄存器的值、进程的状态以及堆栈中的内容),以便以后恢复此进程运行。中断发生后,内核会先保存当前被中断的进程上下文(在调用中断处理程序后恢复);
但在中断处理程序里,CPU寄存器的值肯定已经变化了吧(最重要的程序计数器PC、堆栈SP等),如果此时因为睡眠或阻塞操作调用了schedule(),则保存的进程上下文就不是当前的进程context了.所以不可以在中断处理程序中调用schedule()。
3.2.4内核中schedule()函数本身在进来的时候判断是否处于中断上下文:
if(unlikely(in_interrupt()))
BUG();
因此,强行调用schedule()的结果就是内核BUG,但我看2.6.18的内核schedule()的实现却没有这句,改掉了.
4.中断handler会使用被中断的进程内核堆栈,但不会对它有任何影响,因为handler使用完后会完全清除它使用的那部分堆栈,恢复被中断前的原貌.
5.处于中断context时候,内核是不可抢占的,因此,如果休眠,则内核一定挂起.
---------------------------------------------------------------------------
在windows NT内核中,这个也一样,和linux内核不一样的一个地方是:NT内核发明了irql概念,内核是可抢占的,但在内核中只能是高irql的抢占低IRQL,所有的中断处理程序运行在DIRQL上,而进程调度运行在dispatch level上,这个比DIRQL低,此外相当于linux中的softirq机制的dpc机制实现bottom half的处理,但dpc也是运行在dispatch level上的,因此它和中断handler一样不能被进程调度抢占,这样如果在这些例程中休眠或者调用可能block的函数,则内核一定hang,windows NT在这种情况下会调用KeBugCheckEx()挂起机器,BSOD了.
中断嵌套
由于中断入口路径往往会分配一个pt_regs结构,并且一般来说,中断处理程序也会消耗一部分堆栈空间。如果嵌套中断太多
就有可能出现内核堆栈(一般都是固定大小,M. Christophe Avoinne, David Decotigny和Thomas Petazzoni在i386-KOS上实验动态大小的内核堆栈,有兴趣可以查阅相当资料)溢出的情况。幸运的是,对于大多数平台来说,中断嵌套都不会太多。比如在某个平台下,访问未对齐的数据会调用未对齐中断处理程序,而这个程序又可能触发一个TLB缺失错误,整个过程总的嵌套深度一般不会超过3。但是设备的中断是个例外。如果不仔细设计,设备中断嵌套可能导致重新进入内核几十次甚至上百次,这就极有可能溢出内核堆栈。这种情况下,Linux内核可以设计一些原则,比如对同一通道的中断进行严格串行话,另外也可以利用硬件的一些机制,比如i386平台上的LAPIC的TPR任务优先级寄存器限制中断嵌套的次数(i386-linux在初始化时设置其为允许所有的中断通过,然后再也没有更改过,这似乎无法有效的控制中断嵌套的次数)。也有人提出全局禁止中断嵌套,但Andrea Ancangeli持不同意见,他认为有些许多中断处理函数在执行时都是允许中断的,这实际上就可以看作是这些函数默许中断嵌套的存在(不然为什么要开中断呢),如果一个中断处理函数的执行时间过长(比如IDE I/O可能在中断上下文中执行3ms之久)则允许中断嵌套有利于整个系统的性能。
也就是说目前Linux内核是倾向于允许中断嵌套的,但是如何防止深度嵌套的机制我还不是很清楚。各类平台各类中断控制器应该都能提供这样的方法,只是在i386平台上,我还没有找到Linux内核相应的动作。
异常
异常的处理过程中又产生了异常(比如在溢出的处理过程中又产生了溢出),我们应该怎么处理?
溢出是一个异常,那么它可以嵌套异常么?可以嵌套中断么?(当然,这里我们把他们两个看成是并列的两个概念)我们如果在处理异常过程中,可否切换到其它进程(也许因为它的时间片已经用完)?如果可以,那么异常中不是嵌套中断了么?如果不能,不知道掉电了怎么办?难到还不中断?
异常是处理器内产生的突发事件,与程序执行是同步的;中断是外部设备产生的,具有不可预测性。中断既可以产生于用户态,也可以产生于核心态(中断嵌套);而异常通常产生在用户态,所以异常通常是一级的(不知道允许嵌套不?)。所以得出这样的结论:中断可以打断异常(核心态的中断,比如,时钟中断),而异常不可以打断中断。
为什么异常不可以屏蔽?是因为理论上不可屏蔽(异常时处理器处于不正常状态,执行不下去了),还是机制上我们没有那种机制(象屏蔽中断那样置上一个位)?既然不可以屏蔽(假设该命题成立),那么中断时怎么保证不发生异常(比如,缺页时发生溢出或者出界)?如发生,上面的命题怎么办?
异常和中断,哪个紧迫性更强一点?还是要根据具体的事件具体处理?
在linux2.6中,中断可以抢占异常,但是异常不能抢占中断.异常处理程序处于进程上下文,可以切换到其他进程.异常不可以屏蔽是由处理器设计决定的,它不受if标志影响.
异常和中断相比,通常中断紧迫性更强.