中断之原理篇
前言:
中断是计算机发展中一个重要的技术,它的出现很大程度上解放了CPU,提高了CPU的执行效率。
在中断出现之前,CPU对IO采用的是轮询的方式进行服务,这使的CPU纠结在某一个IO上,一直在等待它的响应,如果它不响应,CPU就在原地一直的等下去。这样就导致了其他IO口也在等待CPU的服务,如果某个IO出现了important or emergency affairs,CPU也抽不出身去响应这个IO。
为了解决这个纠结的问题就------>出现了中断
中断控制的主要优点是只有在IO接口需要服务时才去响应它,使得CPU很淡定的做它自己的事情,只有IO口有需求的时候才去响应它。同时中断中也设计了中断优先级,来处理一些很紧急的事件。
一.中断的基本知识
1.中断的概念:
所谓中断,是指CPU在正常运行程序时,由于程序的预先安排或内外部事件,引起CPU中断正在运行的程序,而转到发生中断事件程序中。这些引起程序中断的事件称为中断源。
其实从物理学的角度看,中断是一种电信号,由硬件设备产生,并直接送入中断控制器(如 8259A)的输入引脚上,然后再由中断控制器向处理器发送相应的信号。处理器一经检测到该信号,便中断自己当前正在处理的工作,转而去处理中断。此后,处理器会通知 OS 已经产生中断。这样,OS 就可以对这个中断进行适当的处理。不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识,这些值通常被称为中断请求线。
2.那么当产生一个中断时,CPU是如何识别的呢?
在Intel X86中可以支持256中向量中断,为了使处理器能使别每种中断源,给它们进行了编号----->叫做中断向量
3.这些中断向量在Linux中是如何分配的:
编号0~31的向量对应于异常和非屏蔽中断
编号32~47的向量(即由IO设备引起的中断)分配给屏蔽中断。
编号48~255的向量用来标示软中断。Linux用其中的128或0x80来实现系统调用
非屏蔽中断的向量和异常的向量是固定的。
4.异常和中断的区别:
1>异常:是指CPU内部出现的中断,即在CPU执行特定指令时出现的非法情况。同时异常也称为同步中断,因此只有在一条指令执行后才会发出中断 ,不可能在指令执行期间发生异常。
a.产生的原因:
程序的错误产生的(eg:除数为0)
内核必须处理的异常条件产生的(eg:缺页)
b.异常又分为故障和陷阱,它们都不使用中断控制器,也不能被屏蔽
C.X86处理处理器中大约有20中异常。Linux内核必须为每种异常提供一个专门的异常处理程序。
2>中断:也称为异步中断。因此它是由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能在指令之间发生。
a.中断又分为外部可屏蔽中断(INTR)和外部非屏蔽中断(NMI)
所用I0设备产生的中断请求均引起可屏蔽中断
硬件故障引起的故障则产生非屏蔽中断。
John说明:
在CPU执行一个异常处理程序时,就不再响应其他异常和中断请求服务.那么如果此时发生了一个异常,CPU不能去响应它,又不能把它的信息丢失该怎么办呢?
这是就用到了堆栈,把所有的信息压入栈。等当前异常处理后,才从堆栈中取出信息再响应刚才的异常。(当产生多个非屏蔽中断时,CPU的处理方法同上)
二APIC和8259A
中断的实现也需要硬件上的支持的,那么硬件上是如何支持中断的?
1.在X86计算机的 CPU 为中断只提供了两条外接引脚:NMI 和 INTR。
NMI 是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;
INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。
2.中断控制器
目前常见的中断控制器有可编程中断控制器8259A和高级可编程中断控制器(APIC)
1>8259A
PIC(Programmable Interrupt Controller)是由两片 8259A 的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达 8 个不同的 IRQ。因为从 PIC 的 INT 输出线连接到主 PIC 的 IRQ2 引脚,所以可用 IRQ 线的个数达到 15 个。
我们来看一个图:(进行看图说明)
a.第一级8259A是主中断控制器,它的第二个中断请求输入端与第二级8259A的中断输出端INT相连。
b.与中断控制器相连的每条线叫做中断线。要使用中断线,就要进行中断线的申请,即IRQ。
那么这条线的名字是啥勒----》中断号。
IRQ线是从0开始顺序编号的,所以第一条IRQ线就是IRQ0。
C.那么该中断号于我们上面所说的中断向量有什么关系呢
中断向量=中断号+32。
从此等式可以看出,第一个中断线(IR0)所对应的中中断向量是32.
由此可以得出:
(1)异常和非屏蔽向量是CPU 内部引起的中断
(2)向量32-47对应的是外部中断。
d.并不是每个设备都可以向中断线上发中断信号,只有对某一条确定的中断线拥有了控制权后,才可以向这条中断线上发送信号。
e.8259A中还有一个很重要的寄存器->8位的中断屏蔽寄存器->这个寄存器的作用是屏蔽中断。
8位的中断屏蔽寄存每一位对应8259A中的一条中断线,如果要禁用某条中断线,则把中断屏蔽寄存器的相应位置1,要启用则置0。
John哥说明:
屏蔽中断也可以从CPU的角度考虑,即清除eflag的中断标志位(IF),当IF位为0时禁止任何外部IO的中断请求,即关中断;
f.共享中断(一个很重要的概念,后面程序中会涉及到它)
由于计算机的外部设备越来越多,所以15条中断线已经不够用了。中断线是很宝贵的资源,为了更好的利用它,只有当设备需要中断的时候才申请占用一个IRQ,并且为了让更多的设备使用中断采取了在申请IRQ时采用共享中断的方式。
2>高级可编程中断控制器(APIC)
先看图再说:
1.8259A 只适合单 CPU 的情况,为了充分挖掘 SMP 体系结构的并行性,能够把中断传递给系统中的每个 CPU 至关重要。基于此理由,Intel 引入了一种名为 I/O 高级可编程控制器的新组件,来替代老式的 8259A 可编程中断控制器。该组件包含两大组成部分:一是“本地 APIC”,主要负责传递中断信号到指定的处理器;举例来说,一台具有三个处理器的机器,则它必须相对的要有三个本地 APIC。另外一个重要的部分是 I/O APIC,主要是收集来自 I/O 装置的 Interrupt 信号且在当那些装置需要中断时发送信号到本地 APIC,系统中最多可拥有 8 个 I/O APIC。
2.每个本地 APIC 都有 32 位的寄存器,一个内部时钟,一个本地定时设备以及为本地中断保留的两条额外的 IRQ 线 LINT0 和 LINT1。所有本地 APIC 都连接到 I/O APIC,形成一个多级 APIC 系统。
那么我们如何知道我们机子上使用的是那种中断控制器呢?
我们可以通过在终端出入命令:cat /proc/interrupts来查看
a.若看到列表中有IO-APIC,说明您的系统正在使用 APIC。
若看到 XT-PIC,意味着您的系统正在使用 8259A 芯片。
16位实地址模式的中断机制和32位保护模式的中断机制的最本质差别就是在保护模式心爱引入了中断描述表
在单处理器的系统中,第一列是中断号,第二列是CPU产生该中断的次数。最后一列是于这个中断相关的俄设备名字。这个名字是通过参数devname提供给函数request_irq()(下篇文章会对它讲解)
三.中断描述表
1.为什么引入
在实地址模式中,CPU把内存中从0开始的1kb空间作为一个中断向量表。表中的每个表项占四个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址就是相应中断处理程序的入口地址。
但是在保护模式下,由4个字节的表项构成的中断向量表已经不能满足要求了。在保护模式下,中断向量表中的表项由8个字节组成。此时他也有了新的名字---->中断描述表(Interrupt Descriptor Table,IDT),其中的每个表项叫做一个门描述符(great descriptor)
先来看图在说明:
1>DPL:段描述符的特权级
2>偏移量:入口函数地址的偏移量
3>P:表示段是否在内存中的标志
4>段选择符:入口函数所处代码段的选择符
5>D:标志位,1表示32位,0标示16位
6>xxx:3位门类型码
门类型符主要分为
a.中断门(interrupt gate):其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。
当控制权通过中断门进入中断处理程序时,处理器清IF标志即关中断这样就避免了中断嵌套的发生。
中断门中的DPL(请求特权级)为0,因此用户态中的进程不能访问中断门。所用的中断处理程序都由中断门激活,并全部限制在内核态。
b..陷阱门(tap gate)其类型码为111。它与中断门类似,唯一的区别是控制权通过陷阱门进入处理程序时保持IF标志位不变,即不关中断。
c.系统门(system gate):Linux内核特别设置的,用来让用户态的进程访问Intel的陷阱门。
系统门的DPL为3。系统调用就是通过系统门进入内核的。
2.在保护模式下,中断描述符表在内存的位置不再局限于从地址0开始的位置,而是可以放在内存的任何位置。
1>为了实现这个功能--->CPU中设计了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始位置。
2>中断描述表寄存器是一个48位的寄存器。它的低16为保存中断描述符表的大小,高32位保存中断描述表的基址。
3>看下图:
我们知道了中断描述表的功能和基本设置后,那么系统是是在何时给它初始化以及是如何给它初始化的呢?
首先Linux内核在系统的初始化阶段对中断进行初始化,其中包括有:初始化可编程控制器8259A;将中断描述符表的起始地址装入IDTR中,并初始化表中的每一项。
3.中断的初始化
1>用户进程可以通过INT指令发出一个中断请求,其中断请求向量在0~255之间。
那么如何防止用户使用INT指令模拟非法的中断和异常?
此时DPL就起作用了->将DPL置为0就可以了。
2>但是,有时候必须让用户进程能够使用内核所提供的功能(比如系统调用)也就是从用户态进入内核态,此时就可以通过把中断门或陷阱门的DPL置为3来实现。
3>当计算机在实模式时,中断描述符表被初始化,并由BIOS使用。
but,在进入了Linux内核时,中断描述符表就被移到内存的另一个区域,并为进入保护模式进行预初始化:
用汇编指令LIDT对中断描述符表寄存器IDTR进行初始化,即把IDTR置为0,然后把中断描述符表IDT的起始地址装入IDTR。
4>中断描述表的初始化
a.第一次初始化:用setup_idt()函数填充中断描述符表中的256个表项,填充时使用一个空的中断处理程序。因为现在还是在初始化阶段,还没有任何中断处理程序,因此,用这个空的中断处理程序来填充每个表项。
b.第二此初始化:内核在启用分页功能后对IDT进行第二此初始化。
此时,使用实际的陷阱和中断处理程序替换这个空的处理程序。一旦这个过程完成后,对于每个异常,IDT都包含一个专门的陷阱门或系统门,而对每个外部中断,IDT都包含专门的中断门。
上面提到了对IDT的初始化,那么我们就递归深入下来看看系统是如何对IDT表项进行设置的
4.IDT表项的设置
IDT表项的设置是通过_set_gate()函数来实现的。
1>插入一个中断门
调用 set_intr_gate(n,addr)函数来实现
此函数的功能是在IDT的第n个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址addr,DPL字段设置为0.
分析下的形参:
n:表示在第几个表项中插入一个中断门。
addr:表示偏移量,此处偏移量设置为中断处理程序的地址addr.
现在我们迭代深入,看下它的内部是如何实现的
330static inline void set_intr_gate(unsigned int n, void *addr) 331{ 332 BUG_ON((unsigned)n > 0xFF); 333 _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS); 334}
我们可以看到它里面有封装了两个函数:我门继续迭代深入。
a.我们先来看看第一个函数的原型和功能:
19#if (_MIPS_ISA > _MIPS_ISA_MIPS1) 20 21static inline void __BUG_ON(unsigned long condition) 22{ 23 if (__builtin_constant_p(condition)) { 24 if (condition) 25 BUG(); 26 else 27 return; 28 } 29 __asm__ __volatile__("tne $0, %0, %1" 30 : : "r" (condition), "i" (BRK_BUG)); 31} 32 33#define BUG_ON(C) __BUG_ON((unsigned long)(C)) 34
(1)我们可以看到BUG_ON()函数是一函数宏,系统最终调用的是__BUG_ON((unsigned long)(C))
函数:
__BUG_ON((unsigned long)(C))它的作用是判断n是否大于255,若大于255则出错。
此处的n表示在第n个表项中插入一个中断门(因为表项总共有255个,所以当n大于255时出错)。
(2) _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS);
我们来分析下它的形参:
第一个形参 n:表示在那个表项中插入一个中断门。
第二个形参 表示门描述符的类型,此处是插入一个中断门,所以它的形参是GATE_INTERRUPT。
第三个形参表示偏移量,设置为中断处理程序的地址addr。
第四个形参表示DPL,我们可以看到给它传递的是0,即避免中断嵌套的发生。
第五个形参表示IST(Interrupt Stack Table)共 3 位,表达 IST1 - IST7 共 7 个 Stack pointer第六个形参表示门中的段选择符。此处设置成内核代码的段选择符。
我们还可以继续深入到 _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS)函数中看它到底是如何实现功能的,但由于篇幅有限。占时不在继续深入。
2.插入一个陷阱门
调用 set_trap_gate(n,addr)函数来实现。
此函数的功能是:在IDT的第n个表项中插入一个陷阱门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址addr,DPL字段设置为0。
分析下的形参:
n:表示在第几个表项中插入一个中断门。
addr:表示偏移量,此处偏移量设置异常处理程序的地址addr.
现在我们迭代深入,看下它的内部是如何实现的 371static inline void set_trap_gate(unsigned int n, void *addr) 372{ 373 BUG_ON((unsigned)n > 0xFF); 374 _set_gate(n, GATE_TRAP, addr, 0, 0, __KERNEL_CS); 375}
我们可以看到它内部同样封装了两个函数BUG_ON()和_set_gate()函数,它们的功能和实现方法都在上面具体分析过,再次不在分析。
3.插入一个系统门
调用 set_system_gate(n,addr)函数来实现。
此函数的功能是在IDT的第n个表项插入一个系统门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常理程序的地址addr,DPL字段设置为3.(所以,系统调用在用户态下可以通过INT 0X80顺利通过系统门。从而进入内核态)
分析形参:
n:表示在第几个表项中插入一个中断门。
addr:表示偏移量,此处偏移量设置为异常处理程序的地址addr.
现在我们迭代深入,看下它的内部是如何实现的
365static inline void set_system_trap_gate(unsigned int n, void *addr) 366{ 367 BUG_ON((unsigned)n > 0xFF); 368 _set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS); 369} 370
我们可以看到它的内部实现还是和上面类似。唯一不同的是在_set_gate()函数中的两个参数不同。
一个参数是DPL被设置为03,还有一个是门类型的类型是GATE_TRAP