中断可以为外部中断,异常,陷阱。在这里主要谈外部中断的处理过程,其中异常,陷阱的处理过程比较简单(相比外部中断,没有中断控制器,没有中断的注册,管理简单)。
中断号(irq号):在实地址模式时,irq号就是中断向量号。i386最多有256个中断,其中0-31由是异常、陷阱等,由系统使用,32-255为外部分硬件使用。
中断向量表:在实地址模式下,在内存的0处开始保存中断服务程序的入口地址,入口地址叫作中断向量,由中断向量在内存中顺序组成的表叫中断向量表。由于中断向量表的局限性,不能支持操作系统。所以在x86CPU中被中断描述表代替。
中断描述符表IDT(interrupt descriptor table):在保护模式下,中断描述符表代替了中断向量表。
中断的初始化(内核,系统来完成)
中断是一个软件硬件相配合一起完成的事情。硬件所完成的事情就是向中断控制器发送一个信号。中断控制器接收到此信号后,设置与此引脚相关的寄存器,再根据优先级判断后,将中断(其实是中断的irq号)提交到CPU。CPU在执行完一条指令后,会检查是否有中断来了。如果有,则开始中断的一系列工作。同时硬件的一些操作需要的数据结构由软件来完成初始化(内核在trap_init,init_IRQ初始化),当硬件从一个电信号的中断形式一步步地找到中断服务程序的入口时,下面的工作由软件来做了。
CPU自动完成的工作:
1.从IDTR寄存器中找到IDT(interrupt descriptor table,中断描述符表)表的地址(注意此地址只有内核可以得到);IDT地址+irq号*sizeof(表项)就得到了此irq号的中断描述符(也称为门描述符,以下用“门”)。
下两图为门的结构图
段描述符结构图:

2. 从门描述符中得到一些信息,中断服务程序代码所在的段的段选择码和32位的位移。
3.使用段选择码,在GDT表中找到段描述符。
4.从段描述符中得到段的段基地址(这个也是由内核在trap_init,init_IRQ中),大小等。
5.有了段基地址+32位的offset = 虚拟地址(也是一个内核使用的地址),得到中断服务程序。无论中断,异常陷阱都有这五个部分。
5.1 CPL与段描述符的DPL进行比较,如果CPL值(保存于CS寄存器的低两位)不小于DPL值,则进行下一步判断;下一步判断是:CPL要与门的DPL进行比较。CPL值>=DPL值,也就是说CPL的级别低于DPL才能通过此门。两步判断有一步不通过,都会产生一个“通用保护”的异常。
CS等段寄存器的结构图。图中RPL就是CPL。
完成以上检查权级别,发现如果特权发生变化,表示从用户态进入内核态(中断发生时用户进程占用CPU)。没有变化表示中断发生在内核态(中断发生时,内核正在占用CPU)。
因为内核的DPL总是0.用户进程的DPL总是3.所以特权从3变成0.表示从用户态进行内核态。
进一步说从用户态进行内核态,则要切换堆栈。
切换堆栈需要以下步骤完成:
1.读TR寄存器,得到当前进程的TSS段。
31....16 |
15...0 |
段内偏移值 | ||||
I/O位图基地址 |
|
T |
0x64 | |||
|
LDT段选择符 |
0x60 | ||||
|
GS |
0x5c | ||||
|
FS |
0x58 | ||||
|
DS |
0x54 | ||||
|
SS |
0x50 | ||||
|
CS |
0x4C | ||||
|
ES |
0x48 | ||||
EDI |
0x44 | |||||
ESI |
0x40 | |||||
EBP |
0x3C | |||||
ESP |
0x38 | |||||
EBX |
0x34 | |||||
EDX |
0x30 | |||||
ECX |
0x2C | |||||
EAX |
0x28 | |||||
EFLAGS |
0x24 | |||||
EIP |
0x20 | |||||
页目录基地址寄存器CR3(PDBR) |
0x1C | |||||
|
SS2 |
0x18 | ||||
ESP2 |
0x14 | |||||
|
SS1 |
0x10 | ||||
ESP1 |
0x0C | |||||
|
SS0 |
0x08 | ||||
ESP0 |
0x04 | |||||
|
前任务链接(TSS选择子) |
0x00 |
2.从中得到此进程的内核堆栈的地址。图中ss0,esp0。把SS寄存器和ESP寄存器(用户堆栈)保存到ss0,esp0所指的堆栈中。然后把ss0和esp0装载到ss寄存器和esp寄存器。
完成后如下图的第一个大括号。
以上动作没有用到一个通用寄存器,全部由CPU硬件完成。软件没有参与。只是在系统初始化时,为系统把IDT表,GDT表准备好(初始化表中的内容)就可以了。以上五步骤只说明了操作的主要方向,其中还有一些操作细节(门的DPL判断等)没有特别得说明。
系统的工作:
6.中断服务程序是是一汇编程序。
.section .init.rodata,"a"
ENTRY(interrupt)
.text
.p2align 5
.p2align CONFIG_X86_L1_CACHE_SHIFT
ENTRY(irq_entries_start)
RING0_INT_FRAME
vector=FIRST_EXTERNAL_VECTOR
.rept (NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7
.balign 32
.rept7
.if vector < NR_VECTORS
.if vector <> FIRST_EXTERNAL_VECTOR
CFI_ADJUST_CFA_OFFSET -4
.endif
1:pushl_cfi $(~vector+0x80)/* Note: always in signed byte range */
.if ((vector-FIRST_EXTERNAL_VECTOR)%7) <> 6
jmp 2f
.endif
.previous
.long 1b
.text
vector=vector+1
.endif
.endr
2:jmp common_interrupt
.endr
END(irq_entries_start)
.previous
END(interrupt)
7.进入common_interrupt,根据irq号在软件维护的一组结构体中找irq的处理程序。
common_interrupt:
addl $-0x80,(%esp)/* Adjust vector into the [-256,-1] range */
SAVE_ALL /* 保护现场 */
TRACE_IRQS_OFF
movl %esp,%eax
call do_IRQ /* 进入C语言的函数do_IRQ */
jmp ret_from_intr
ENDPROC(common_interrupt)
.macro SAVE_ALL
cld
PUSH_GS
pushl_cfi %fs
/*CFI_REL_OFFSET fs, 0;*/
pushl_cfi %es
/*CFI_REL_OFFSET es, 0;*/
pushl_cfi %ds
/*CFI_REL_OFFSET ds, 0;*/
pushl_cfi %eax
CFI_REL_OFFSET eax, 0
pushl_cfi %ebp
CFI_REL_OFFSET ebp, 0
pushl_cfi %edi
CFI_REL_OFFSET edi, 0
pushl_cfi %esi
CFI_REL_OFFSET esi, 0
pushl_cfi %edx
CFI_REL_OFFSET edx, 0
pushl_cfi %ecx
CFI_REL_OFFSET ecx, 0
pushl_cfi %ebx
CFI_REL_OFFSET ebx, 0
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
SET_KERNEL_GS %edx
.endm
8.进入do_IRQ函数。