中断与上下文
系统调用,异常与中断
系统调用,异常与中断都会对系统产生由用户态到内核态的过程,但是其之间有着不同的过程和机制。系统调用和异常由cpu内部正在执行的指令产生;系统调用是用户程序调用OS提供的接口函数并执行svc(supervisor call)指令陷入内核态并执行相应功能函数,异常是系统内部产生的如缺页异常问题而陷入内核态。这两者都称为同步中断。此外,中断时由外部设备产生的不能预见的问题,所以称为异步中断,这三者都会使得用户态陷入内核态,并通过中断向量表来寻找中断服务程序,是执行系统调用还是中断处理。
中断与异常
需要明确的一点是CPU对于中断和异常的具体处理机制本质上是完全一致的
具体的处理过程如下:
- 中断响应的事前准备:
系统要想能够应对各种不同的中断信号,总的来看就是需要知道每种信号应该由哪个中断服务程序负责以及这些中断服务程序具体是如何工作的。系统只有事前对这两件事都知道得很清楚,才能正确地响应各种中断信号和异常。
系统将所有的中断信号统一进行了编号(一共256个:0~255),这个号称为中断向量,具体哪个中断向量表示哪种中断有的是规定好的,也有的是在给定范围内自行设定的。 中断向量和中断服务程序的对应关系主要是由IDT(中断向量表)负责。操作系统在IDT中设置好各种中断向量对应的中断描述符(一共有三类中断门描述符:任务门、中断门和陷阱门),留待CPU查询使用。而IDT本身的位置是由idtr保存的,当然这个地址也是由OS填充的。
中断服务程序具体负责处理中断(异常)的代码是由软件,也就是操作系统实现的,这部分代码属于操作系统内核代码。也就是说从CPU检测中断信号到加载中断服务程序以及从中断服务程序中恢复执行被暂停的程序,这个流程基本上是硬件确定下来的,而具体的中断向量和服务程序的对应关系设置和中断服务程序的内容是由操作系统确定的。
2) CPU检查是否有中断/异常信号
CPU在执行完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器(如:8259A)是否发送中断请求过来,如果有那么CPU就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量[2]。
对于异常和系统调用那样的软中断,因为中断向量是直接给出的,所以和通过IRQ(中断请求)线发送的硬件中断请求不同,不会再专门去取其对应的中断向量。
3) 根据中断向量到IDT表中取得处理这个向量的中断程序的段选择符
CPU根据得到的中断向量到IDT表里找到该向量对应的中断描述符,中断描述符里保存着中断服务程序的段选择符。
4) 根据取得的段选择符到GDT中找相应的段描述符
CPU使用IDT查到的中断服务程序的段选择符从GDT中取得相应的段描述符,段描述符里保存了中断服务程序的段基址和属性信息,此时CPU就得到了中断服务程序的起始地址。这里,CPU会根据当前cs寄存器里的CPL和GDT的段描述符的DPL,以确保中断服务程序是高于当前程序的,如果这次中断是编程异常(如:int 80h系统调用),那么还要检查CPL和IDT表中中断描述符的DPL,以保证当前程序有权限使用中断服务程序,这可以避免用户应用程序访问特殊的陷阱门和中断门[3]。
5) CPU根据特权级的判断设定即将运行的中断服务程序要使用的栈的地址
CPU会根据CPL和中断服务程序段描述符的DPL信息确认是否发生了特权级的转换,比如当前程序正运行在用户态,而中断程序是运行在内核态的,则意味着发生了特权级的转换,这时CPU会从当前程序的TSS信息(该信息在内存中的首地址存在TR寄存器中)里取得该程序的内核栈地址,即包括ss和esp的值,并立即将系统当前使用的栈切换成新的栈。这个栈就是即将运行的中断服务程序要使用的栈。紧接着就将当前程序使用的ss,esp压到新栈中保存起来。也就说比如当前在某个函数中,使用的栈,在中断发生时,需要切换新的栈。
6) 保护当前程序的现场
CPU开始利用栈保护被暂停执行的程序的现场:依次压入当前程序使用的eflags,cs,eip,errorCode(如果是有错误码的异常)信息。
官方文档[1]给出的栈变化的示意图如下:
7) 跳转到中断服务程序的第一条指令开始执行
CPU利用中断服务程序的段描述符将其第一条指令的地址加载到cs和eip寄存器中,开始执行中断服务程序。这意味着先前的程序被暂停执行,中断服务程序正式开始工作。
8) 中断服务程序处理完毕,恢复执行先前中断的程序
在每个中断服务程序的最后,必须有中断完成返回先前程序的指令,这就是iret(或iretd)。程序执行这条返回指令时,会从栈里弹出先前保存的被暂停程序的现场信息,即eflags,cs,eip重新开始执行。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上的进程,并恢复以前挂起的进程的运行。这被称作进程切换,任务切换或者上下文切换。
进程切换分两步:
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文
对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
上下文就是内核重新启动一个被抢占的进程所需的状态。他有一些对象的值组成,这些对象包括:
通用目的寄存器
浮点寄存器
程序计数器
用户栈
状态寄存器
内核栈
各种内核数据结构
内核数据结构,比如描绘地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。
这种决定就叫做调度,是由内核中称为调度器的代码处理的。
当内核选择一个新的进程运行时,我们就说内核调度了这个进程。在内核调度了一个新的进程运行后,他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
硬件上下文
尽管每个进程拥有自己的地址空间,但是所有进程必须共享cpu,因此在恢复进程执行前,内核必须确保每个寄存器的内容都装入了挂起时的值。这些寄存器的数据叫做硬件上下文。它也是进程上下文的子集。
另外进程切换必须在内核态执行,这就意味着,用户态进程使用的所有寄存器内容都已经保存在了内核堆栈上。