中断与异常
中断通常被定义为一个事件:让事件改变处理器执行的指令顺序这样的事件,与CPU芯片内外部硬件电路产生的电信号相对应!
中断通常分为同步中断与异步中断:
同步中断指的是当指令执行时,由CPU控制单元产生的。之所以称为同步,是因为只有在一条指令终止执行后,CPU才会发出中断!
异步中断是由其他硬件设备依照CPU时钟信号随机产生的
在英特尔微处理器手册中:也会把同步和异步中断分别称为异常和中断
中断则是由间隔定时器或者io设备产生的,举个例子你敲击键盘的时候,你的一次按键就会引发一个中断,希望操作系统介入进行处理!
另一方面异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的!比如说内核通过发送一个信号来处理异常,或者内核执行恢复异常所需要的步骤,比如说缺页,比如说对内核服务的一个请求
中断信号的作用
顾名思义,中断信号提供了一种特殊的方式来让处理器转而去运行正常控制流之外的代码。当一个中断信号到达的时候CPU必须停止它当前所做的事情,转而切换去处理这些终端。为了做到这一点就需要把内核态堆栈保存PC当前的值,并且把中断相关类型的的一个地址放进程序计数器中。这样才会跳转去执行处理中断的代码。
中断处理器内核执行的最敏感的任务之一,因为它必须要满足:
-
让内核正打算去完成别的事情的,由于中断随时都会到来,因此内核的目标就是:让中断尽可能的处理完尽可能把更多的更详细的处理向后推,所以中断响应分为两个部分:
-
关键而紧急的部分这一部分,内核立即执行。
-
其推迟的部分,则是内核随后执行。
-
因为中断随时会到来,所以内核可能正在处理其中一个中断的时候,另一个中断又发生了。应该尽可能地允许这种情况发生,因为这将会保持更多的io设备处于忙状态。因此中断处理程序必须编写成可以使相应的内核控制路径以嵌套的方式进行,执行到最后一个内核控制路径终止时,内核可以恢复被中断进程的执行,或者如果中断信号已导致了重新调度,内核可以切换到另外的进程。
-
尽管内核在处理前一个中断的时候,可以接受新的中断,但在内核代码区中仍然存在着一些临界区,在这些临界区中中断必须被禁止
中断和异常
英特尔文档把中断和异常又分为了以下几类:
-
中断:
又分出两类即:
类型 | 说明 |
---|---|
可屏蔽中断 | IO设备发出的所有中断请求(Interruppt Request)都产生可屏蔽的中断,它处于两状态:屏蔽的和非屏蔽的,如果一个中断是被屏蔽的,那么控制单元会被它会忽略它 |
非可屏蔽中断 | 只有少数的几个危机事件是这样的,非屏蔽的中断总是由CPU辨认 |
异常:
异常分两类,处理器探测异常和编程异常。
处理器探测异常有三种
类型 | 说明 |
---|---|
故障 | 通常可以被纠正,一旦纠正,程序就可以在不失连贯性的情况下,重新开始!保存在EIP中的值就是引起故障的指令地址,因此当异常处理程序终止时,那条指令会被重新执行 |
陷阱 | 在陷阱执行后,立即报告内核把控制权返回给程序后,就可以继续它的执行而不失去连贯性。保存在EIP中的值是一个随后要执行的指令地址,只有当没有必要重新执行已中止的指令时,才会触发陷阱。陷阱的主要目的是为了调试程序 |
异常终止 | 异常终止指的是发生了一个严重的错误及控制单元出现了问题,不能在EIP寄存器中保存引发了这个异常指令所在的确切位置。异常终止用的报告严重的错误,如硬件故障或系统表中无效的值或不一样的值。 |
编程异常:在编程者发出请求时发生是由int或int3指令触发的当into(检查溢出)和bound(检查地址出界)指令检查的条件不为真时,会引发编程异常控制单元!编程异常当作陷阱来处理,编程异常也被称为软中断,这样的异常常有两种的用途执行:系统调用以及给调试程序通报一个特定的事件。
IRQ与中断
每个能够发出中断请求的硬件设备系都有一条名为IRQ的输出线,所有现有的IRQ线都与一个名为可编程中断控制器的硬件电路的输入引脚相连,可编程中断控制器执行下列动作:
-
监视IRQ线检查产生的信号,如果有两条或两条以上的IRQ线上产生信号,选择引脚编号较小的IRQ线
-
如果一个引发信号出现在线上,那么它会把接收到的信发信号转换成对应的向量,把这个向量存放在中断控制器的一个io端口,从而允许CPU通过数据总线读取此向量。把引发信号发送到处理器的INTR引脚,也就是产生了一个中断
-
等待直到CPU通过把这个中断信号写进可编程中断控制器的io端口号,来确认它,当这种情况发生时,清理INTR线。然后继续监视。
异常
这里给出一些常见的异常:
异 常 一 览 表 | 向量号 | 异常名称 | 异常类型 | 出错代码 | 相关指令 |
---|---|---|---|---|---|
0 | 除法出错 | 故障 | 无 | DIV,IDIV | |
1 | 调试异常 | 故障/陷阱 | 无 | 任何指令 | |
3 | 单字节INT3 | 陷阱 | 无 | INT 3 | |
4 | 溢出 | 陷阱 | 无 | INTO | |
5 | 边界检查 | 故障 | 无 | BOUNT | |
6 | 非法操作码 | 故障 | 无 | 非法指令编码或操作数 | |
7 | 设备不可用 | 故障 | 无 | 浮点指令或WAIT | |
8 | 双重故障 | 中止 | 有 | 任何指令 | |
9 | 协处理器段越界 | 中止 | 无 | 访问存储器的浮点指令 | |
0AH | 无效TSS异常 | 故障 | 有 | JMP、CALL、IRET或中断 | |
0BH | 段不存在 | 故障 | 有 | 装载段寄存器的指令 | |
0CH | 堆栈段异常 | 故障 | 有 | 装载SS寄存器的任何指令、对SS寻址的段访问的任何指令 | |
0DH | 通用保护异常 | 故障 | 有 | 任何特权指令、任何访问存储器的指令 | |
0EH | 页异常 | 故障 | 有 | 任何访问存储器的指令 | |
10H | 协处理器出错 | 故障 | 无 | 浮点指令或WAIT | |
11H—0FFH | 软中断 | 陷阱 | 无 | INT n |
中断向量表
中断描述符表IDT是一个系统表,它与每一个中断或者异常向量相联系
每一个向量在表中都有相应的中断或异常处理程序的入口地址,内核在允许中断发生前,必须适当初始化IDT
下图为IDT的结构表示图:
这些描述符是:
-
任务门
Task Gate
:当中断信号发生时,必须取代当前进程的那个进程的TSS选择符,存放在任务门中 -
中断门
Interrupt Gate
:包含段选择符和中断或异常处理程序的段内偏移量,当控制权转移到应适当的段时,处理器清IF标志,从而关闭即将会发生的可屏蔽中断。 -
陷阱门
Trap Gate
:与中断门相似只是控制权传递到一个适当的段时处理器不修改IF标志Linux使用中断门处理中断应用陷阱门处理异常
硬件处理
现在描述CPU控制单元是如何处理中断和异常的!
我们现在假定内核已经被初始化,因此CPU将会在保护模式下运行。
当执行了一条指令后,CS和EIP这对寄存器将会包含下一条将要执行的指令的逻辑地址。在处理那条指令之前控制单元会检查在运行前一条指令是否已经发生一个中断或者异常,如果发生了,那么它将会:
-
确定与中断或异常关联的向量(看一眼是哪个异常)
-
读由寄存器指向的
IDT
表中的i
项(肘!IDT表爆破) -
从
gdpr
寄存器中获取GDP的基地址,并在GDP中查找,以读取IDT表项的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址(看看这个程序在哪里) -
确信中断事由授权的中断发生源发出的,首先将当前特权级CPL与段描述符的描述符特权级DPL的相比较:如果CPL小于DPL了就会产生一个
general protection
异常,因为中断处理程序的特权不能低于引起中断的程序的特权!对于编程异常则需要做进一步的安全检查:比较CPL了与处于IDT中的门描述符的DPL了,如果DPL小于CPL了那么就产生一个general protection
,这最后一个检查可以避免用户应用程序访问特殊的陷阱门和中断门(配不配处理这个异常) -
检查是否发生了特权级变化:也就是说CPL是否不同于所选择的段描述符的DPL,如果是,控制单元必须开始使用与新的特权级相关的栈,通过执行以下步骤来做到这一点:
-
读TR寄存器以访问运行进程的TSS段
-
用于新特权级相关的栈段和栈指针的正确值装载SS和ESP寄存器这些值,可以在TSS中找到在新的段中
-
保存SS和ESP以前的值,这些值定义了与旧特训级相关的栈的逻辑地址。
-
-
如果故障已经发生,用引起异常的指令地址装载CS和EIP寄存器,从而使得这条指令能够再次被执行!
-
在栈中保存eflags,cs,EIP等内容,如果引用异常产生了一个硬件出错码,把它保存到栈中,装在CS和EIP寄存器其值分别为IDT表中第I项门描述服务的段选择符和偏移量字段,这些值给出了中断或者异常处理的第一条指令的逻辑地址!
控制单元所执行的最后一步就是跳转到这些异常处理程序,换句话说处理完中断信号后控制单元所执行的指令,就是被选中处理程序的第一条指令!
当中断或处理结束后和异常被处理结束后相应的处理程序必须参与生一条iret指令,他把控制权转交给被中断的进程。这将会迫使控制单元:
-
用保存在栈中的值装在CS或eflags寄存器,如果一个硬件出错码曾经被压入栈中,并且在EIP内容的上面,那么执行Iret指令前必须弹出这个硬件错误码(准备回家)
-
检查处理程序的CPL是否等于CS中的最低两位的值如果是iret终止执行,否则执行下一步。(看看特权级够不够)
-
从栈中装载SS和ESP寄存器因此返回到与特权级相关的栈(也就是恢复栈)
-
检查DS ES FS 以及 GS段寄存器的内容,如果其中一个寄存器包含选择符是一个段描述符,并且其的DPL值小于CPL了,那么我们会清理相应的段寄存器控制单元,这么做是为了防止用户态的程序利用内核以前所用的段寄存器,如果不清理这些寄存器,那么一些怀有恶意的用户态程序就有可能利用他们来访问内核地址。(安全处理,安全恢复环境)
中断和异常处理程序的嵌套执行
每个中断或异常都会引起一个内核控制路径,或者说当前的进程在内核态执行单独的指令序列。
内核控制路径可以任意嵌套!一个中断处理程序可以被另一个中断处理程序所中断,因此这样就引起了内核控制路径的嵌套执行。允许内核控制路径嵌套执行必须要付出相应的代价,也就是中断处理程序必须永不阻塞换,中断处理程序在运行期间是不能够发生进程切换。
基于以下两个主要原因,Linux交错执行内核控制路径:
-
为了提高可编程中断控制器和设备控制中器的吞吐量。假定设备控制器在一条线上产生了一条信号,pic把这个信号转换成一个外部中断,然后pic和控制设备器保持阻塞一直到pic从内核CPU处接收一条应答信息!由于内核控制路径的交错执行内核即使正在处理前一个中断也能够发送应答。
-
为了实现一种没有优先级的中断模型,每个C中断处理程序都可以被另一个中断处理程序所延缓,因此在硬件设备之间没必要预定义优先级,这简化了内核代码,也提高了内核的可移植性!
初始化IDT
Linux在基于Intel给出的三种门之外,还更加细分了他们:
中断门(interrupt gate):用户态的进程不能访问的一个lntel中断门(门的DPL字段为0)。所有的Linux中 断处理程序都通过中断门激活,并全部限制在内核态。
系统门(syslem gate):用户态的进程可以访问的一个Intel陷阱门(门的DPL字段为到.通过系统门来激 活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下.可以 发布into、 bound及int $Ox80三条汇编语言指令。
系统中断门(system interrupt gate):能够被用户态进程访问的Intel中断门(门的DPL字段为3). 与向量3相关的异常 处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3.
陷阱门(Irapgate):用户态的进程不能访问的一个Inte)陷阱门(f]的DPL字段为0). 大部分Linux异 常处理程序都通过陷阱门来激活.
任务门(task gate):不能被用户态进程访问的Intel任务门(门的DPL字段为0).Linux对"Doublefault" 异常的处理程序是由任务门激活的.
IDT会被初始化两次。第一次是在BIOS程序中,此时CPU还工作在实模式下。一旦Linux启动,IDT会被搬运到RAM的受保护区域并被第二次初始化,因为Linux不会使用任何BIOS程序。
IDT结构被存储在idt_table表中,包含256项。idt_descr变量存储IDT的大小和它的地址,在系统的初始化阶段,内核用来设置idtr寄存器,专用汇编指令是lidt。
内核初始化的时候,汇编函数setup_idt()用相同的中断门填充idt_table表的所有项,都指向ignore_int()中断处理函数:
setup_idt: lea ignore_int, %edx movl $(__KERNEL_CS << 16), %eax movw %dx, %ax /* = 0x0010 = cs */ movw $0x8e00, %dx /* 中断门,DPL=0 */ lea idt_table, %edi /* 加载idt表的地址到寄存器edi中 */ mov $256, %ecx rp_sidt: movl %eax, (%edi) /* 设置中断处理函数 */ movl %edx, 4(%edi) /* 设置段描述符 */ addl $8, %edi /* 跳转到IDT表的下一项 */ dec %ecx /* 自减 */ jne rp_sidt ret
中断处理函数ignore_int()
,也是一个汇编语言编写的函数,相当于一个null函数,它执行:
-
保存一些寄存器到堆栈中。
-
调用printk()函数打印
Unknown interrupt
系统消息`。 -
从堆栈中恢复寄存器的内容。
-
执行iret指令回到调用处。
正常情况下,此时的中断处理函数ignore_int()是不应该被执行的。如果在console或者log日志中出现Unknown interrupt
的消息,说明发生硬件错误或者内核错误。
完成这次IDT表的初始化之后,内核还会进行第二次初始化,用真正的trap或中断处理函数代替刚才的null函数。一旦这两步初始化都完成,IDT表就包含具体的中断、陷阱和系统门,用以控制每个中断请求。
中断处理
这里讨论三种中断类型:
IO中断,时钟中断。和处理器间中断io
中断处理程序必须足够灵活地给多个设备同时提供服务,比如说在PCI总线的体系架构中几个设备可以共享一个IRQ线,这也就意味着仅仅中断向量是并不能说明所有问题的!中断处理程序的灵活性是以两种不同的方式实现的:
IRQ共享
中断处理程序执行多个中断服务例程,每个中断服务例程是一个与单独设备相关的函数,因此不可能预先知道哪个特定的设备产生,因此每个IRQ,也就是中断服务例程,都会被执行验证它的设备是否需要关注,如果是,当设备产生中断时则需要执行相关的所有操作
IRQ动态分配
一条线可能在最后的时刻才会与一个设备驱动程序相关联,这样,即使几个硬件设备并不共享线,同一个向量也可以在这几个设备在不同时刻中使用。当一个中断发生时,并不是所有的操作都具有急迫性,,因此Linux会把紧随中断要执行的操作分为三类:
紧急的:这样的操作比如说对pic应答中断,对pic或设备控制器中编程重修改,由设备和处理器同时访问的数据结构这样的操作都可以很快的被执行,他们是紧急的,因为他们必须要尽快的执行紧急操作!要在一个中断处理程序内立即执行,而且是在禁止可屏蔽中断的情况下
非紧急的:这样的操作比如说修改那些只有处理器才会访问的数据结构,这样的操作必须也很快完成,因此它们由中断处理程序立即执行,但它们是在开中断的情况下执行的
非紧急可延迟的:比如说把缓冲区的内容拷贝到进程的地址空间中,这样的操作可能被延迟较长的时间间隔,而不会影响内核的操作!
不管引起中断的电路类型如何所有的io中断处理程序都执行四个基本的操作:
-
在内核态堆栈中保存的值与寄存器的内容
-
为正在给线服务的pic发送个应答,这将允许pic进一步发出中断
-
执行共享这个IRQ的所有设备都中断服务例程
-
跳到ret_from_intr的地址后终止
为一个IRQ可配置设备选择一条线,有三种方式:
-
设置一些硬件跳线跳接器
-
安装设备时执行一个实用程序,这样的程序可以让用户选择一个可用的RQ号或者探测系统自身以确定一个可用的IRQ号
-
在系统启动时执行一个硬件协议,外设宣布他们准备使用哪些中断线,然后协商一个最终的值以尽可能减少冲突,该过程一旦完成,每个中断处理程序都会通过访问设备的某个IO端口函数来读取所分配的IRQ
数据结构
对于每一个外设的IRQ都用 struct irq_desc 来描述,我们称之中断描述符(struct irq_desc)。linux kernel中会有一个数据结构保存了关于所有IRQ的中断描述符信息,我们称之中断描述符DB(上图中红色框图内)。当发生中断后,首先获取触发中断的HW interupt ID,然后通过irq domain翻译成IRQ number,然后通过IRQ number就可以获取对应的中断描述符
中断描述符
通用中断处理模块可以用一个线性的table来管理一个个的外部中断,这个表的每个元素就是一个irq描述符,在kernel中定义如下:
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = { [0 ... NR_IRQS-1] = { .handle_irq = handle_bad_irq, .depth = 1, .lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock), } };
系统中每一个连接外设的中断线(irq request line)用一个中断描述符来描述,每一个外设的 interrupt request line 分配一个中断号(irq number),系统中有多少个中断线(或者叫做中断源)就有多少个中断描述符(struct irq_desc)。NR_IRQS定义了该硬件平台IRQ的最大数目。
struct irq_desc { struct irq_data irq_data; unsigned int __percpu *kstat_irqs;------IRQ的统计信息 irq_flow_handler_t handle_irq;--------流控函数 struct irqaction *action; -----------处理函数 unsigned int status_use_accessors;-----中断描述符的状态,参考IRQ_xxxx unsigned int core_internal_state__do_not_mess_with_it; unsigned int depth;----------描述嵌套深度的信息 unsigned int wake_depth;--------电源管理中的wake up source相关 unsigned int irq_count; unsigned long last_unhandled; unsigned int irqs_unhandled; raw_spinlock_t lock; struct cpumask *percpu_enabled; #ifdef CONFIG_SMP const struct cpumask *affinity_hint;----和irq affinity相关,后续单独文档描述 struct irq_affinity_notify *affinity_notify; #ifdef CONFIG_GENERIC_PENDING_IRQ cpumask_var_t pending_mask; #endif #endif unsigned long threads_oneshot; atomic_t threads_active; wait_queue_head_t wait_for_threads; #ifdef CONFIG_PROC_FS struct proc_dir_entry *dir;--------该IRQ对应的proc接口 #endif int parent_irq; struct module *owner; const char *name; } ____cacheline_internodealigned_in_smp
响应函数 irqaction
在 irq_desc 中,struct irqaction *action,主要是用来存用户注册的中断处理函数,一个中断可以有多个处理函数 ,当一个中断有多个处理函数,说明这个是共享中断。所谓共享中断就是一个中断的来源有很多,这些来源共享同一个引脚。所以在irq_desc结构体中的action成员是个链表,以action为表头,若是一个以上的链表就是共享中断
struct irqaction { irq_handler_t handler; //等于用户注册的中断处理函数,中断发生时就会运行这个中断处理函数 unsigned long flags; //中断标志,注册时设置,比如上升沿中断,下降沿中断等 cpumask_t mask; //中断掩码 const char *name; //中断名称,产生中断的硬件的名字 void *dev_id; //设备id struct irqaction *next; //指向下一个成员 int irq; //中断号, struct proc_dir_entry *dir; //指向IRQn相关的/proc/irq/ };
中断数据 irq_data
中断描述符中应该会包括底层irq chip相关的数据结构,linux kernel中把这些数据组织在一起,形成struct irq_data,具体代码如下:
struct irq_data { u32 mask;----------TODO unsigned int irq;--------IRQ number unsigned long hwirq;-------HW interrupt ID unsigned int node;-------NUMA node index unsigned int state_use_accessors;--------底层状态,参考IRQD_xxxx struct irq_chip *chip;----------该中断描述符对应的irq chip数据结构 struct irq_domain *domain;--------该中断描述符对应的irq domain数据结构 void *handler_data;--------和外设specific handler相关的私有数据 void *chip_data;---------和中断控制器相关的私有数据 struct msi_desc *msi_desc; cpumask_var_t affinity;-------和irq affinity相关 };
操作合集 irq_chip
struct irq_chip { const char *name; unsigned int (*irq_startup)(struct irq_data *data);-------------初始化中断 void (*irq_shutdown)(struct irq_data *data);----------------结束中断 void (*irq_enable)(struct irq_data *data);------------------使能中断 void (*irq_disable)(struct irq_data *data);-----------------关闭中断 void (*irq_ack)(struct irq_data *data);---------------------应答中断 void (*irq_mask)(struct irq_data *data);--------------------屏蔽中断 void (*irq_mask_ack)(struct irq_data *data);----------------应答并屏蔽中断 void (*irq_unmask)(struct irq_data *data);------------------解除中断屏蔽 void (*irq_eoi)(struct irq_data *data);---------------------发送EOI信号,表示硬件中断处理已经完成。 int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);--------绑定中断到某个CPU int (*irq_retrigger)(struct irq_data *data);----------------重新发送中断到CPU int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);----------------------------设置触发类型 int (*irq_set_wake)(struct irq_data *data, unsigned int on);-----------------------------------使能/关闭中断在电源管理中的唤醒功能。 void (*irq_bus_lock)(struct irq_data *data); void (*irq_bus_sync_unlock)(struct irq_data *data); void (*irq_cpu_online)(struct irq_data *data); void (*irq_cpu_offline)(struct irq_data *data); void (*irq_suspend)(struct irq_data *data); void (*irq_resume)(struct irq_data *data); void (*irq_pm_shutdown)(struct irq_data *data); ... unsigned long flags; }