簡(jiǎn)介
1.基于Linux0.11代碼進(jìn)行分析。
2.中斷類(lèi)型分類(lèi)以及具體的中斷。
3.中斷向量的注冊(cè)。
4.中斷處理流程。
5.各類(lèi)型中斷的具體執(zhí)行流程。
中斷的類(lèi)型及具體的種類(lèi)

1.可屏蔽硬件中斷。優(yōu)先級(jí)較低,可以被忽略或者延后處理,通常有鍵盤(pán),打印機(jī)。
2.不可屏蔽硬件中斷。CPU必須無(wú)條件響應(yīng),優(yōu)先級(jí)非常的高,通常有電源斷電,內(nèi)存校驗(yàn)錯(cuò)誤。
3.軟件中斷--錯(cuò)誤。缺頁(yè)異常??jī)?nèi)存訪問(wèn)時(shí)產(chǎn)生缺頁(yè)異常中斷,在中斷處理程序中實(shí)際分配內(nèi)存,然后在缺頁(yè)中斷處理完成后,繼續(xù)訪問(wèn)內(nèi)存。
4.軟件中斷--陷阱。常見(jiàn)的例子有系統(tǒng)調(diào)用,int 0x80,首先會(huì)調(diào)用中斷處理程序,處理完成后,會(huì)繼續(xù)執(zhí)行后面的指令。
5.軟件中斷--放棄。常見(jiàn)的例子有除零,該錯(cuò)誤發(fā)生后,調(diào)用中斷處理程序,中斷處理程序中會(huì)產(chǎn)生SIGFPE信號(hào),程序可通過(guò)注冊(cè)對(duì)應(yīng)的信號(hào)處理函數(shù)處理該信號(hào)。
中斷向量的注冊(cè)
1.源碼在head.s這個(gè)文件中。
2.0x20-0x2f是硬件中斷,在head.s中初始化為ignore_int后,后續(xù)的硬件初始化過(guò)程中會(huì)初始化其中斷處理函數(shù)。
3.中斷向量存儲(chǔ)在全局的中斷向量數(shù)組結(jié)構(gòu)中,該數(shù)組長(zhǎng)度256,每個(gè)元素8個(gè)字節(jié),在head.s文件中定義。在system.h文件中,定義了設(shè)置該數(shù)組的接口。代碼如下:
// head.s
.align 2
.word 0
idt_descr:
.word 256*8-1 # idt contains 256 entries
.long idt
// system.h
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
#define set_intr_gate(n,addr) \
_set_gate(&idt[n],14,0,addr)
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
4.在head.s中,調(diào)用startup_32函數(shù)過(guò)程中調(diào)用setup_idt函數(shù)將全局的中斷描述符初始化為ignore_int。代碼如下
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt,%edi // edi寄存器指向idt數(shù)組的起始地址
mov $256,%ecx // 循環(huán)256次
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi // 下標(biāo)遞增
dec %ecx
jne rp_sidt
lidt idt_descr
ret
5.在trap.c文件中,調(diào)用trap_init函數(shù)注冊(cè)軟件中斷。在sched.c中調(diào)用sched_init注冊(cè)2個(gè)調(diào)度相關(guān)的中斷,0x20硬件時(shí)鐘中斷,0x80系統(tǒng)調(diào)用中斷。其余的硬件中斷在硬件初始化時(shí)注冊(cè)。以下是部分代碼:
void sched_init(void)
{
set_intr_gate(0x20,&timer_interrupt);
set_system_gate(0x80,&system_call);
}
void trap_init(void)
{
int i;
// 設(shè)置除操作出錯(cuò)的中斷向量值。
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下面把int17-47的陷阱門(mén)先均設(shè)置為reserved,以后各硬件初始化時(shí)會(huì)重新設(shè)置自己的陷阱門(mén)。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
// 設(shè)置協(xié)處理器中斷0x2d(45)陷阱門(mén)描述符,并允許其產(chǎn)生中斷請(qǐng)求。設(shè)置并行口中斷描述符。
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21); // 允許8259A主芯片的IRQ2中斷請(qǐng)求。
outb(inb_p(0xA1)&0xdf,0xA1); // 允許8259A從芯片的IRQ3中斷請(qǐng)求。
set_trap_gate(39,¶llel_interrupt); // 設(shè)置并行口1的中斷0x27陷阱門(mén)的描述符。
}
tips:如果想要知道中斷處理函數(shù)在哪里注冊(cè),注冊(cè)的函數(shù)是什么,可搜索system.h文件中的定義的_set_intr_gate, _set_trap_gate, _set_system_gate函數(shù)被調(diào)用的地方。
中斷的處理流程
在分析和閱讀源碼前,先嘗試思考通用的中斷處理邏輯。
1.硬件中斷。硬件中斷通常來(lái)自于外部硬件觸發(fā)。此時(shí)進(jìn)程可能在任意一個(gè)狀態(tài)(用戶態(tài)執(zhí)行用戶代碼,或者在執(zhí)行中斷)。如果是在執(zhí)行中斷,那么就應(yīng)該判斷當(dāng)前正在執(zhí)行的中斷和觸發(fā)中斷的優(yōu)先級(jí),然后確定是否要打斷正在執(zhí)行的中斷。
2.軟件中斷。軟件中斷來(lái)自用戶代碼主動(dòng)調(diào)用產(chǎn)生,所以此時(shí)應(yīng)該是在用戶態(tài)執(zhí)行用戶代碼。
3.用戶態(tài)下的中斷應(yīng)該有一樣的通用流程,大致是,保存當(dāng)前正在執(zhí)行代碼的上下文,切換到內(nèi)核態(tài)調(diào)用中斷處理函數(shù),完成后回到用戶態(tài)恢復(fù)上下文,然后繼續(xù)執(zhí)行。

4.中斷處理是在內(nèi)核態(tài)下運(yùn)行,因此使用的是內(nèi)核的堆棧,如果中斷時(shí)正在運(yùn)行用戶態(tài)的代碼,那么在切到內(nèi)核態(tài)后,將當(dāng)時(shí)的上下文寄存器等信息存在內(nèi)核中的堆棧中。示意圖如下:

5.當(dāng)在中斷情況下發(fā)生高優(yōu)先級(jí)的中斷時(shí),會(huì)在中斷過(guò)程中使用的堆棧的基礎(chǔ)上再次保存中斷的上下文,然后執(zhí)行更高優(yōu)先級(jí)的中斷。堆棧示意圖如下:

中斷的具體執(zhí)行
1.源碼主要在asm.s和trap.c這2個(gè)文件中。
2.在調(diào)用具體的中斷處理函數(shù)前,都會(huì)將段寄存器和ip寄存器入中斷棧,這是中斷打斷的正在運(yùn)行的進(jìn)程的上下文。然后將實(shí)際要執(zhí)行的用C實(shí)現(xiàn)的中斷處理函數(shù)push入棧,再將普通的寄存器如eax,ebx等寄存器入棧,下一步將一些段寄存器入棧,最后將返回后執(zhí)行的指令的棧地址入棧。

3.無(wú)返回值的中斷以int 0x1,除0的中斷舉例,代碼如下:
divide_error:
pushl $do_divide_error # 這里push實(shí)際要調(diào)用的函數(shù),下一條指令又將其和eax寄存器的值交換。
# 其目的是為了代碼復(fù)用,其它中斷可以直接調(diào)用no_error_code代碼段
# do_divide_error在traps.c中實(shí)現(xiàn)
no_error_code: # 這里是五出錯(cuò)號(hào)處理的入口處。
xchgl %eax,(%esp) # _do_divide_error的地址→eax,eax被交換入棧
pushl %ebx #保存打斷進(jìn)程的寄存器上下文
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds # !!16位的段寄存器入棧后也要占用4個(gè)字節(jié)。
push %es
push %fs
pushl $0 # "error code" #將數(shù)值0作為出錯(cuò)碼入棧
lea 44(%esp),%edx # 取對(duì)堆棧中原調(diào)用返回地址處堆棧指針位置,并壓入堆棧。
pushl %edx
movl $0x10,%edx # 初始化段寄存器ds、es和fs,加載內(nèi)核數(shù)據(jù)段選擇符 0x10是內(nèi)核棧的段起始地址
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
# 下行上的 * 號(hào)表示調(diào)用操作數(shù)指定地址處的函數(shù),稱為間接調(diào)用。這句的含義是調(diào)用引起本次
# 異常的C處理函數(shù),例如do_divide_error等。
call *%eax
addl $8,%esp
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax # 彈出原來(lái)eax中的內(nèi)容
iret # 返回時(shí)會(huì)取出棧中保存的被打斷進(jìn)程的eip寄存器的值 繼續(xù)執(zhí)行后續(xù)的指令
4.其它的類(lèi)似的無(wú)返回值的中斷處理函數(shù)如下:
debug:
pushl $do_int3 # _do_debug C函數(shù)指針入棧
jmp no_error_code # 復(fù)用no_error_code代碼段
5.含error_code的中斷和有error_code的中斷類(lèi)似,這里就不粘貼重復(fù)的代碼了,可以參考源碼中的asm.s文件。把握住中斷處理的核心邏輯,保存上下文,處理中斷,恢復(fù)上下文,以及參考源碼中的system.h, asm.s, traps.c這幾個(gè)文件。
6.深究int 0x1除0中斷的處理。
void do_divide_error(long esp, long error_code)
{
die("divide error",esp,error_code);
}
static void die(char * str,long esp_ptr,long nr)
{
long * esp = (long *) esp_ptr;
int i;
printk("%s: %04x\n\r",str,nr&0xffff);
// 下行打印語(yǔ)句顯示當(dāng)前調(diào)用進(jìn)程的CS:EIP、EFLAGS和SS:ESP的值。
// EIP:\t%04x:%p\n - esp[1]是段選擇符(cs),esp[0]是eip.
// EFLAGS:\t%p\n - esp[2]是eflags
// ESP:\t%04x:%p\n - esp[4]是源ss,esp[3]是源esp
printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",
esp[1],esp[0],esp[2],esp[4],esp[3]);
printk("fs: %04x\n",_fs());
printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));
if (esp[4] == 0x17) {
printk("Stack: ");
for (i=0;i<4;i++)
printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
printk("\n");
}
str(i); // 取當(dāng)前運(yùn)行任務(wù)的任務(wù)號(hào)
printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);
for(i=0;i<10;i++)
printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
printk("\n\r");
// 前面都是打印調(diào)試信息 這里才是真正的處理邏輯 在這里是直接退出 錯(cuò)誤碼為11
do_exit(11); /* play segment exception */
}
總結(jié)
中斷處理的分析到這里就告一段落了。首先要對(duì)中斷進(jìn)行分類(lèi),并且了解每種類(lèi)型的中斷具體有哪些類(lèi)型。然后了解內(nèi)核是如何處理中斷的,中斷處理函數(shù)使用的棧都是內(nèi)核的內(nèi)存空間,在執(zhí)行具體的處理函數(shù)前,要先保存被中斷的進(jìn)程的上下文,然后再調(diào)用具體的處理函數(shù),處理完成后再恢復(fù)被中斷進(jìn)程的上下文以繼續(xù)運(yùn)行。在這里要說(shuō)明下,內(nèi)核中的棧每次起始地址都是一樣的,這是因?yàn)檎{(diào)用結(jié)束后,要么不會(huì)返回,要么返回后棧又變空了,所以內(nèi)核棧是可以重復(fù)利用的。