深入理解linux内核第三版(三)中断和异常
中断:也叫异步中断,是由外设产生的。
异常:也叫同步中断,是由CPU产生的,是指令执行过程中产生的。
中断信号的作用:中断信号提供了一种特殊的方式,使处理器转而去运行正常控制流之外的代码。
0 硬中断和软中断
硬件中断面对CPU,软中断对内核,信号对某个进程中断。
硬件中断是由外设引起的,软中断是执行中断指令产生的
硬中断的中断号是由中断控制器提供的;软中断是的中断号是由int指令直接给出的比如int 0x80
因为操作系统阻止用户进程直接访问内核代码,那么应用程序如何访问这些受保护的资源呢?此时,软中断就派上用场了,linux中通过int 0x80实现系统调用。
通常,一个事件的发生,需要硬中断和软中断配合才能完成,比如说,网卡有数据到来,网卡会将信号发送给中断控制器8259A,然后8259A产生一个中断信号发送给CPU,这是硬中断。因为网络数据处理需要一定的时间,因此,内核会使用int XXX产生一个软中断,用来处理网络数据包。
00 实模式下和保护模式下中断向量表
实模式下,中断向量表存放在0x00000到0x003ff(共1kb)内存区间内,共256个中断向量。比如我们熟知的int 0x13,用于调用BIOS程序读取硬盘的。
保护模式下,0x00000到0x003ff已经被其他程序覆盖,见https://www.cnblogs.com/zhenjingcool/p/15972761.html,从0开始处存放的是页目录表。保护模式下不存在中断向量表,转而代之的是idt表。idt表的位置由idtr寄存器指定。
idt表共有256项,其中断号如下所示
-
0x00 - 0x1F:共32个。这些中断号是由CPU保留的,用于处理异常和硬件中断。例如,0x00是用于处理除零错误,0x06是用于处理无效操作码等。
-
0x20 - 0x2F:共16个。这些中断号是用于处理可编程中断控制器(PIC)的中断请求(IRQ)。每个中断号对应一个特定的硬件设备,如键盘、定时器、串口等。
-
0x80:这是用于系统调用的中断号,通过触发中断0x80,用户程序可以请求操作系统提供特定的服务和功能。
-
0x8E:这是用于处理页故障(Page Fault)的中断号。当程序访问未映射的内存或非法的内存访问时,会触发页故障中断,操作系统可以通过处理该中断来进行内存管理。
-
0x90:这是用于处理SIMD浮点异常的中断号。当程序执行SIMD浮点指令时,如果发生异常,会触发该中断,操作系统可以通过处理该中断来进行浮点异常处理。
这里我们着重说一下中断号0x80,这个中断号用于系统调用,是在main.c中的sched.c中定义的。其中有一行代码 set_system_gate(0x80,&system_call); 在idt中增加了该项。
高于0x2F的中断非常少,我们常见的就是系统调用0x80
1 中断和异常的分类
中断:
- 可屏蔽中断:如果可屏蔽中断masked置位,则cpu忽略它
- 不可屏蔽中断:表示这个中断不可忽略,cpu必须识别并处理它
异常:
- 故障(fault):eip指向发生故障的指令,当异常处理程序终止时,重新执行eip所指向的指令。
- 陷阱(trap):eip指向下一条指令,当异常处理程序终止时,执行eip指向的下一条指令。
- 异常中止(abort):发生一个严重错误,异常处理程序会强制终止当前进程。
- 编程异常:也叫软中断
2 IRQ
外设和PIC(可编程中断控制器)的连线,称为IRQ线。
PIC的I/O端口通过数据总线和CPU的INTR引脚相连
PIC的作用:
- 监视IRQ线
- 如果一个IRQ线上有引发信号,则把这个引发信号转换为对应的向量。
- 把这个向量存放在PIC的I/O端口,从而允许CPU通过数据总线读取此向量
- 引发信号到达CPU的INTR引脚,产生一个中断。
- 等待CPU的中断应答。当CPU处理了中断后,会将确认信号写到PIC的一个I/O端口。PIC读取此确认信号,清INTR线。
- 返回第1步
IRQ线是从0开始编号的,默认的向量号是IRQ+32.
可以对PIC进行编程,进而有选择的禁止某一条IRQ线。禁止的中断信号不会丢失,一旦IRQ线解禁,PIC又会把他们发送到CPU。(应该是特定的外设具有缓存功能,才使得中断屏蔽取消后中断信号才能够继续被传送)
我们还可以设置eflag的IF标志为0,这样所有可屏蔽中断都将会被CPU暂时忽略,但是这和上面提到的有选择的禁止某一条IRQ线有本质区别。
3 IDT(中断描述符表)
中断描述符表存放中断描述符。每一个中断描述符8字节。因为系统中最多256个中断向量,所以,中断描述符表最大为256*8=2048字节。
中断描述符表寄存器idtr指定了idt的线性基址和限长。在开中断之前,必须使用lidt指令初始化idtr。
中断描述符类型:任务门、中断门、陷阱门。linux利用中断门处理中断,利用陷阱门处理异常,根据第40-42位决定是哪种门,101对应任务门,110对应中断门,111对应陷阱门。其格式如下所示:
4 中断和异常的硬件处理
cs可eip寄存器包含了下一条将要执行的指令的逻辑地址。如果发生一个中断或异常,那么控制单元执行下列操作:
- 确定中断向量i
- 读取由idtr寄存器指向的idt表中的第i项,获取到段选择符和偏移量
- gdtr寄存器获取gdt的基地址。并根据上一步获取的段选择符和偏移量定位到gdt中的某一项。这一步是确定中断或异常处理程序的地址。
- 保存上下文,具体是原进程的eflags、cs、eip入栈,其中eip中的指令是中断处理程序退出时需要执行的原代码中的下一条指令
- 硬件出错码入栈
- 装在中断处理程序的cs和eip寄存器,其值分别是idt表中第i项的段选择符和偏移量。这些值给出了中断或异常处理程序第一条指令的地址。
可参考:https://www.cnblogs.com/zhenjingcool/p/15999402.html,这篇文章介绍了0.11内核中断和异常处理过程。
5 中断和异常处理程序的嵌套执行
我们可以这样理解中断或者异常,每个中断或者异常其实是当前进程的一部分,或者说代表了当前进程在内核态执行的一些指令序列。
每个中断或者异常都会引起一个内核控制路径,内核控制路径可以任意嵌套,一个中断处理程序可以被另一个中断处理程序“中断”。
需要注意的是,如果发生嵌套执行,中断处理程序必须永不阻塞,换句话说,中断处理程序运行期间不能发生进程切换。也就是说中断处理程序不能做任何耗时操作,必须小巧而精妙,耗时的操作需要放在其他地方执行,发生中断时,必须立即处理,然后立即应答。
事实上,内核控制路径恢复执行时需要的所有数据都存放在当前进程的内核态堆栈中。
上面说的中断处理程序必须永不阻塞是对发生嵌套的情况下说的。如果发生嵌套,中断处理程序可以发生阻塞。
一个中断处理程序既可以抢占其他的中断处理程序,也可以抢占异常处理程序。相反,异常处理程序从不抢占中断处理程序。
内核控制路径是交替执行的,比如内核正在处理前一个中断,也能发送另一个中断的应答。
7 初始化idt
idt有两次初始化,第一次初始化是在实模式下初始化的,比如linux0.11内核中,初始化idt是在head.s中进行的,参考https://www.cnblogs.com/zhenjingcool/p/15972761.html,这里调用setup_idt进行初始化,初始化后所有中断的中断处理程序都指向同一处(ignore_int)。
在linux0.11内核中,idt第二次初始化是在trap.c中进行的,参考https://www.cnblogs.com/zhenjingcool/p/15999402.html,这里会使用不同的中断处理程序替换ignore_int。
8 异常处理
异常处理是比较迅速的,一般,当一个异常发生时,内核向引起异常的进程发送一个信号,比如一个SIGFPE信号,异常处理就阶段性结束了,进程此时可能处于阻塞状态等待异常处理程序的结果。
耗时的操作将在异常处理程序中进行,当异常处理程序执行完毕,引起异常的进程将会关注这个信号,以便根据异常处理结果作进一步动作。
异常处理过程参考https://www.cnblogs.com/zhenjingcool/p/15999402.html,由于linux0.11内核代码比较简单,在这篇文章中介绍了0.11内核异常处理的详细过程。(0.11内核不区分异常和中断,都用一样的代码处理)
所有的异常处理程序都有类似的结构。比如:
_overflow: //4号中断 pushl $_do_overflow jmp no_error_code
其中jmp error_code或者jmp no_error_code是一个统一的标号,在其中进行现场保护以及调用实际的中断处理程序的操作。
进入和离开异常处理程序:
当异常发生时,内核向当前进程发送一个信号。代码如下
current -> thread.error_code = error_code; current -> thread.trap_no = vector; force_sig(sig_number, current);
异常处理程序刚一结束,当前进程就关注这个信号。该信号要么在用户态由进程自己的信号处理程序来处理,要么由内核来处理(一般是杀死进程)
9 中断处理
正如前面介绍的那样,当处理异常时,内核只要给引起异常的进程发送一个Unix信号就能够处理大多数异常,要采取的行动被延迟到异常处理程序执行完毕后。因此内核能很快的处理异常。
但是,这种方法不适合中断,因为经常会出现一个进程被挂起很久后中断才到达的情况。因此一个完全无关的进程可能正在运行。所以给当前进程发送一个Unix信号是毫无意义的。
在计算机中IRQ的使用有如下两种形式
- IRQ共享,中断处理程序执行多个中断服务例程(ISR)。因此一个设备发出的中断,可能会执行多个设备对应的ISR。
- IRQ动态分配,例如软盘设备只有在被访问时才分配IRQ线给它。同一个IRQ向量可以由几个设备在不同时刻共享。
当中断发生后,中断处理程序分为3种类型
- 紧急的。他们必须被尽快的执行,要在关中断的情况下在一个中断处理程序内立即执行。比如对PIC应答中断、修改由设备和CPU共同访问的数据结构。
- 非紧急的。要在开中断的情况下立即执行。比如按下一个键后去读键盘扫描码。
- 非紧急可延迟的。这些操作可能被延迟较长时间,目标进程将会等待数据。比如把缓冲区的内容拷贝到某个进程地址空间。非紧急可延迟的操作由独立的函数来执行,将在“软中断和tasklet”中介绍
IO中断处理程序执行步骤:
- 在内核态堆栈中保存寄存器内容
- 应答PIC,允许PIC发出其他中断
- 执行中断服务例程ISR
- 跳到ret_from_intr()的地址后终止。
10中断向量
linux中的中断向量如下图展示
外设的IRQ可以分配的中断向量范围是:32-238(除128外)。其他中断向量不可用于外设中断。
内核必须在启动中断前确定IRQ号与设备之间的对应关系,否则,内核将不知道哪个中断向量对应哪个设备。
待续...