3.中断与异常 2009-12-24 16:25 391人阅读 评论(0) 收藏
中断是一种改变CPU指令执行序列的行为,一般分cpu产生的和外设产生的两种,分别叫同步中断与异步中断,或叫异常与中断。中断处理与进程切换貌似相似,实则大不相同,用于中断或异常处理的代码并非是进程,而是一个“内核控制路径”。中断分为可屏蔽、不可屏蔽,异常则分为:1.故障:发生后仍返回刚执行的指令,即处理前eip指向下条再保存(缺页异常);2.陷阱:发生后返回发生前的下条指令;3.异常中止:eip中不能存值了,出大错了,这种情况就强行运行一个异常处理程序,杀死进程。中断与异常用0-255之间的数表示,intel称它们为向量。
每个可发出中断的设备有至少一条叫IRQ的输出线,它们连着一个可编程中断控制器(PIC),PIC监视各个输入口,若相应口来了信号,则将IRQ信号转换为中断向量,再将中断向量发到CPU的INTR引脚。等待CPU将这个信号回送到PIC的某个I/O脚作为确认。可对PIC编程以禁止某IRQ信息发往CPU,但一旦再激活,PIC仍会将刚才的IRQ发往CPU,要注意这与CPU的cli,sti不同。
80x86大约有20种异常。内核必须为它们提供处理程序。中断描述符(IDT)是一个表格,可以通过它查到一个中断向量(中断或异常的)对应的处理程序。8086中,它直接存储CS:IP,但保护模式下,仅CS:IP表达不了所要的信息,现在它每一项用8个字节表示。所以最多要256X8个字节。8086中,它们存在0x0处,现在可以随意存放,只要将表格的地址存在idtr寄存器中。而256项有中断也有异常,所以表项是不一样的。分为三种:任务门、中断门、陷阱门。顾名思义,门gate说明表项中还不是地址,只是通往地址的“门”,到真实的处理程序地址还有一层间接性。任务门指它放的是新进程的TSS,其余段空着。其它两种情况类似于GDT,放段选择符与段内偏移量。区别在于中断门在控制权转移时(不是中断处理执行时)清IF屏蔽中断,陷阱门不处理。
中断与异常的硬件处理大致如下:首先,一条指令结束后,cs,eip指向下条指令,这时控制单元检查是否有中断或异常发生。若有,则确定向量(PIC直接送来),结合idtr找到第i项,获得段描述符,从gdtr寄存器获得相应段描述符段地址,进行特权级匹配检查。比较当前CPL与IDT中DPL,若CPL小,说明试图从内核态访问用户态,产生异常(general protection),因为中断处理程序不可能在用户态。相反,若CPL大于DPL,说明目前在用户态,准备进入内核态执行中断处理,那么必须使用进程的内核栈,上章所述,这些信息存在tr寄存器指示的tss段中。在内核栈中压入首先压入当前用户态的ss,esp。如果是故障,将cs,eip指向上条指令,再将cs,eip,eflags,硬件出错码入栈。最后从IDT->GDT中取得中断处理程序的cs,eip执行。执行完后,指令iret会使之前cs,eip,eflags先出栈,并再次检查目前CPL与弹出的cs中CPL,若不一致,说明内核栈中还有用户栈的地址,则再让ss,esp出栈(这说明进入中断之前在用户态,栈不一样)。最后,清一下仍在内核态的段选择符,防止出意外。思考,进程在中断或异常处理程序中,有相关上下文保存在内核栈中,如果此时发生进程切换,那进程内核栈就被换掉了,中断处理程序是否无法返回了?中断发生可以发生在内核态,但异常中除了缺页,都在用户态发生。
以上是80x86硬件中断异常,具体到linux中,异常发生时,内核栈存相关regs,调用C程序处理,用ret_form_exception()返回。任务门时,CPU在私有栈中执行异常处理,相应的参数从regs处取,而C处理程序开始时会将相关的出错码,向量存在进程描述符中,这样,异常处理终止后,进程从描述符中拿走参数并处理。异常处理程序还要检查异常发生在用户态还是内核态,如果是内核态,则要看是否系统调用参数出错,否则它认为内核出bug,do_exit()终止。异常处理程序仅把相关参数放到进程描述符中就返回,让之后进程自己处理,所以很快。但中断不可以这样,因为异常发生时可以保证,当前进程就是引发异常的进程,而中断则不是,即中断即使想存,也不知道存在哪。
中断向量一般是IRQn+32,因为前32个用来放异常(系统调用异常在128)。IRQ可共享或动态分配设备。新安装时,老设备要手动设置跳线或是执行一个程序,让它来选IRQ号,或者是系统启动时,执行一个硬件协议,让外设们协商解决,之后中断程序通过I/O端口来读IRQ。
内核为每个中断向量描述符设计一个irq_dest数据结构,按数组来组织。结构中包含了handler指出外设信息;action指针指向一个链表,其中是各个中断处理程序(因为可共享,所以可能有多个)。回到之前讨论的内核栈,异常时,很多reg都存放在内核栈。其实,准确地说是放在异常栈上。而异常栈就由内核栈的thread_union实现。现在中断发生,也得有中断栈。但编译时,thread_union可8K或4K,当8K时,将进程内核栈用于中断与异常,但4K时,考虑到中断可嵌套,仍那么处理可能不够。于是将中断栈从进程独立出来,设置成与CPU相关,且单独占一个页框。Cpu其实对于硬中断、软中断(后面介绍)各有一个中断栈,分别是hard_irq_stack与soft_irq_stack。并且在页底部也存有当前进程的thread_info。
这时,thread_info就与cpu相关。分配4K内核栈情况下,硬中断发生时必须将内核栈的控制转到与cpu关联的硬中断栈,在硬中断栈中进行中断处理。至此,上述思考有个初步答案,即中断或异常生发时,至少硬中断发生时进程不可以被切换,因为相关的数据存在与cpu关联的硬中断栈中,切换了,无论如何都找不到了。
因为中断时不可以进程切换,所以进程时间片会被中断打乱,为此必须尽量减少中断处理占用的时间。于是将不紧急的中断处理任务从中断处理程序中提取出来,延后执行。Linux将中断发生后要执行的操作分三类:1是紧急的,如对PIC的应答。这种要在中断处理程序中完成,并且要在屏蔽中断下进行。2是非紧急的。如修改cpu相关数据结构,它也要在中断处理程序中完成,但它不需关中断,即这种中断可以再被中断。3是可延迟的。如将缓冲区内容拷贝到相关进程的地址空间。它们可以由独立的函数在中断处理程序完成后的某时进行,这样就缩短了内核处理中断的时间。Linux2.6有两种实现这种函数的机制,是可延迟函数与工作队列函数。在内核中,可延迟函数就叫软中断。还有一种可延迟函数叫tasklet,也由软中断实现。软中断可以重入,需要自旋锁保护。而tasklet串行执行,不要保护。软中断在内核初始化时或加载模块时静态分配,问题是它延迟到何时运行?在运行之前,它先要调用raise_softing()函数激活,以宣示我可以运行了。激活可以在任何时刻进行。激活后,每次do_IRQ()完成I/O中断处理后、处理完本地定时中断后都会运行它。如果内核很闲,中断少,那么软中断会得不到执行机会;或者如果软中断太多如网卡以高频来数据,那就一直在处理软中断,反而会使用户进程瘫痪。为平衡这两者,引入一个内核线程ksoftirqd,在软中断处理中,循环十次,如果还有软中断来,就开动这个内核线程以低优先级在后台慢慢跑,来完成之后的软中断激活处理。关于异常处理时可否切换进程,后面再说。
版权声明:本文为博主原创文章,未经博主允许不得转载。