中断向量表
完全理解本节内容,需要先理解80386 CPU保护模式下段式映射过程[*]概念
中断:CPU外部硬件导致,比如网卡收发到报文、硬盘读写完成等
异常:CPU内部执行指令异常导致,比如除0、缺页等
陷阱:CPU内部执行到INT指令时导致,与异常的区别是陷阱是"主动"的,往往是用户程序通过系统调用进入的
这种区分只是从产生原因上划分的,可以不用太关心,它们(后面统称"中断")从产生,到CPU进内核执行完相应处理函数的过程基本一样,主要区别有两点:
① 中断与异常/陷阱区别:处理中断的过程中关中断,即将标志寄存器的IF标志位置0,而处理异常/陷阱时不改变IF标志位
② 异常与陷阱区别:异常处理完成,再重新执行一遍导致异常的指令(比如访问某个换出页面时导致缺页异常,内核恢复好映射后,再重新执行出错指令就没问题了,对于出错指令来看,好像什么都没发生过一样),陷阱处理完成,执行INT的下一条指令(gdb的断点功能就是用的INT 3指令)
[*]CPU硬件支持
作为一个学习者而不是设计者,可以先看一下硬件如何设计的,再回头理解为什么要这样设计。
在程序中,我们用一些变量表示程序当前的状态,比如全局变量running、stop,进入某个函数时的参数,其实变量本质上都要归根于硬件上,都是内存或寄存器的带电状态。通过了解硬件的设计,更能体会到这一点,硬件在作执行一些操作的过程中,"传参"都是靠寄存器或某个约定位置的内存。
80386 CPU为了支持现代意义的中断,添加了IDTR寄存器,指向一个中断向量表。类似于为了支持保护模式的段式内存管理,添加了GDTR/LDTR寄存器,指向段描述符表;为了支持页式内存管理,添加CR3寄存器,指向页目录表。
中断向量表中的每个64位都代表一个"门",门的种类分为“任务门”(101,忙闲忙,忙完刚休息一会又来任务了)、“中断门”(110,前面都是1,到最后时"断了")、“陷阱门”(111,陷阱下面放了一排刺)、“调用门”(100,每次考试都满分,diǎo爆了)。
以下是Linux内核中设置门(即为每个64位数据赋值)的函数(后面详细说明):
为什么要设计这么多种门呢?因为硬件工程师们很贴心呀{:qq13:},希望软件设计可以更简单,从而考虑了较多情况下的任务切换,并在硬件层就实现好了。
但是,如同GDTR/LDTR寄存器,Linux几乎不用LDTR,Linux也几乎不用任务门和调用门:
通过任务门切换进程时,硬件缺少充足的信息而做了很多冗余的操作,执行效率不够高,而进程切换又是一个非常频繁的操作,所以Linux内核进程切换没有利用任务门(详见《Linux内核源代码情况分析》第四章);
内核为了避开硬件复杂的设计,比如内核维护人员原本必须了解硬件的4种门,现在只需要了解2种。
[*]中断过程
① 根据IDTR寄存器指向的中断向量表,以及中断原因对应的中断向量,找到对应的门(比如内核启动时,将缺页异常对应的门,设置在了表中下标为14的地方,缺页异常发生时,CPU自动根据该门进入异常处理函数do_page_fault());
② 对比CPU当前执行权限CPL与门所要求的权限DPL(如同段式内存映射过程要对比段寄存器RPL与段描述符中的DPL,门的结构里面也有DPL),CPL≤DPL则可以穿过此门(值越小权限越高);
③ 每种门里面都有段选择码(16位),就可以把它当作真实段寄存器一样理解,并经过段式映射过程,找到一个段描述符(其中通过任务门找到的是TSS段),段描述符中的DPL也要拿来和CPL对比,DPL≤CPL则可以访问到对应的段(与段式映射过程中,段寄存器RPL≤段描述符DPL才能通过权限检查相反,因为现在是中断,想想通过系统调用可以从低权限的用户态进入高权限的内核态就明白了);
④ 描述一个任务,需要很多信息,比如该任务当前各个寄存器的值、当前的权限等,为此设计了TSS结构,穿过四种门,只有任务门段选择码找到的是TSS段,表示要切换到的新任务,而当前任务的TSS结构由新增的TR寄存器指向。而其它门只是用于从用户态切换到内核态,并不涉及任务的切换,其中的“段选择码+位移”指向一块代码,并且所要求的执行权限一般会更高。
注意:进程切换和用户态/内核态切换是两回事,一个进程从用户态切换到内核态,只是一个TSS中部分信息发生了变化(比如穿过陷阱门的过程中CPU自动修改CPL等),而进程切换是从一个TSS“跳到”另一个TSS,不光要根据另一个TSS的内容修改CPU的各种状态,TR寄存器也要指向新的TSS结构。
⑤ 步骤③中对比段描述符DPL与CPL,如果不一致,还要进行堆栈的切换,每个Linux进程在用户空间的栈和在内核空间的栈是不同的,只要切换前后不同的,都必须在切换前记录下来,并且是记录到切换后所能访问的空间,这样切换回之前的状态时,才能找到恢复的依据。
[*]中断向量表(IDT)初始化
上述只说明了如何利用IDT,那么IDT是如何生成的呢?
set_intr_gate()、set_trap_gate()、set_system_gate()、set_call_gate()调用的都是_set_gate()函数,结合门的结构,就可以理解该函数了,并且体会一下内核函数的精湛:
通过_set_gate()函数的参数也可以看出,各种门的区别就是类型码、dpl、偏移地址,比如set_system_gate(0x80, addr)→_set_gate(idt_table+0x80, 15/*0,D:1,type:111*/, 3/*dpl*/, addr)。
早期的计算机,将中断向量表大小设计为256个向量,经过几十年的发展,向量号有了“约定俗成”的含义,比如0x80用于系统调用,如今中断向量表仍然包含256个项,一个项即一个门,设置所有门,即是对每个门调用_set_gate()的过程:
0~0x20、0x80比较特殊,通过上述trap_init()函数可以看到它们的初始化,其余的都设置为中断门,并且处理函数地址都来自于interruput[]:
interrupt[]数组存的实际是IRQ_0x0?_interrupt()这些函数,它们的定义如下:
本篇文章只介绍了中断微量表的设置,以及根据"门"中的代码段+偏移跳转到IRQ_0x0?_interrupt(),或trap_init()设置的函数执行的过程。至于common_interrupt处的代码,到后面分析完整进入中断处理函数的过程时,再分析,比如从page_fault()函数到do_page_fault()函数,还要经历很长的过程呢。
顶楼主好东西 回复 2# mordorwww
{:yxh31:}
页:
[1]