前言
各种不同架构的CPU都有自己的异常处理机制,并且中断属于异常的一种(中断异常)。那么Linux作为一个通用的操作系统,它是如何用同一套机制来处理这些不同架构处理器的异常和中断的呢?这篇文章就来了解和学习这个问题。
一、预备知识点
进程和线程
在Linux中,进程是资源分配的单位,线程是任务调度的单位;线程是属于进程的,一个进程可以包含多个线程;同一个进程里面的线程共享进程的资源(比如全局变量,文件句柄等)。
因为线程是任务调度的单位,所以真正执行程序代码的是线程。而函数调用和任务调度都需要保存现场到栈中,所以每个线程都有自己的栈,用于保存和恢复现场。
异常和中断
发生异常的时候,CPU会停下当前正在处理的工作,跳转到异常对应的异常处理程序去处理异常。异常处理程序代码不属于任何进程和线程,它是由异常来激活并由硬件强制跳转执行的。
中断属于异常的一种,叫做中断异常,是由外部中断引起的一种异常。由于外部中断有很多,但是都跳转到中断异常,所以在中断异常处理程序里面需要通过中断号判断是哪个外部中断引起的中断异常,然后跳转到对应的中断处理程序。如果该外部中断是一个共享中断(连接多个外部中断源,比如多个GPIO引脚,每个引脚都可出发这个中断),在中断处理程序里面还需要判断每个中断源的中断标志位,进而判断是否是这个中断源引发的中断。
如果需要更详细了解ARM架构的异常处理机制,请参考《九、异常处理》。
保存和恢复现场
现场就是指程序运行的上下文,对于一个像Linux这样的多任务操作系统,上面可能同时运行很多应用程序,每个应用程序都是一个进程,每个进程可能包含很多个线程,并且可能还会有各种异常和中断来打断程序的运行。
Linux的任务调度单位为线程,这些不同的线程在运行的过程中都会用到CPU的内部寄存器来进行各种运算。当从线程A调度到线程B的时候,线程A需要将自己用到的内部寄存器内容保存到自己的栈中,以免这些寄存器的内容被线程B破坏,这就是保存现场。当调度回线程A的时候,先从线程A的栈中恢复寄存器的值,然后再接着运行线程A的代码,这就是恢复现场。
异常的发生会强制CPU去执行异常处理程序,这些处理程序中也会用到这些内部寄存器。所以在进入异常处理程序的时候,也需要保存发生异常时的现场(假设异常发生的时候是线程A在运行,那么保存的就是线程A的现场);在退出异常处理程序的时候,需要恢复现场。
同样,函数的调用和返回也需要保存和恢复现场,因为不同的函数里面的代码也会用到这些内部寄存器。
二、在Linux中使用中断的两个原则
如下是Linux系统中进程调度和中断处理的时间线示意图:

可以看到发生中断的时候会打断正在运行的进程转而去运行中断处理程序;中断处理程序执行完了之后会重新调度(如果进程A的时间片没有用完就回到A进程继续运行,如果进程A的时间片已经用完了就切换到其它的进程)。如果中断处理程序很耗时间,那么进程A和B就长时间得不到运行,用户的直观感受就是程序变得很卡顿,特别是对于GUI或者人机交互类的设备是不可接受的。
Linux不允许中断嵌套,这出于以下几个考量:
-
在经典的Linux配置中,每个进程都会有一个小的内核栈(通常8K或16K),中断处理程序就借用当前被中断进程的内核栈的顶部一小块空间来运行。如果允许嵌套中断,则每一次嵌套都会在同一个内核栈上占用新的栈帧,嵌套过多就会导致这个内核栈溢出。
-
简化同步和锁设计:如果中断允许嵌套,这意味有可能需要在中断里面使用
spinlock这样的同步和互斥机制来保护临界区,这很可能会引起死锁(假设中断A获取了自旋锁,然后被中断B打断,而中断B也试图获取同一个锁,此时就会导致死锁)。 -
允许中断嵌套会使得中断的响应时间和执行时间变得不可预测,最坏情况下的延迟(所有中断都嵌套发生)会变得非常大。
基于以上几个原因,Linux在处理中断的时候,干脆直接关掉CPU的全局中断开关(屏蔽所有的中断,中断处理结束的时候再恢复),这样就不用处理嵌套中断了。
综上,在Linux中使用中断需要遵循两个原则:中断处理程序要又短又快 + 中断不能嵌套。中断不能嵌套很简单,直接关掉CPU的全局中断开关就行了;所以需要重点解决的是怎么让中断处理程序又短又快,为了达到这个目的,Linux对中断的处理也在随着CPU架构的发展而不断演进。
三、Linux系统对中断处理的演进
中断程序本身就又短又快
如果中断处理程序里面只是简单设置了一些标志位,或者修改了一些变量,那么这个中断处理程序本身就是很快就能执行完的。对于这样的中断处理程序,可以直接使用如下API进行注册:
c
// 返回值:0表示注册成功;其它负值表示注册失败,如果返回-EBUSY表示中断已经被注册了
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
irq - 中断对应的中断号
handler - 中断处理函数
name - 中断名字,设置后可以在/proc/interrupts文件中看到对应的中断
dev - 用来同一个共享中断的不同中断源
flags - 中断标志,可以使用|进行组合,常用的有如下一些:
IRQF_SHARED,表示这是一个共享中断,也即对应多个外部中断源。这意味着该中断号不是某个设备的独占资源,多个设备驱动程序可以注册使用同一个中断号的中断处理程序。当共享中断发生时,内核按注册顺序调用该中断号下的各个中断处理程序,每个中断处理程序需要检查是否是自己的设备产生了中断。IRQF_ONESHOT,主要用于线程化中断处理(threaded IRQ),确保在中断处理线程完成前,该中断不会被再次触发(重入)。其原理是:在执行线程化中断处理程序的过程中,禁用该IRQ的中断线(通过中断控制器),直到线程化中断处理程序完成运行并明确重新启用它。IRQF_TRIGGER_NONE,中断触发类型已经由其它方式预先配置好,驱动程序信任并接受当前的配置,不改变它。IRQF_TRIGGER_RISING,上升沿触发。IRQF_TRIGGER_FALLING,下降沿触发。IRQF_TRIGGER_HIGH,高电平触发。IRQF_TRIGGER_LOW,下降沿触发。
未完待续。。。