練習6:完善中斷初始化和處理 (需要編程)
請完成編碼工作和回答如下問題:
- 中斷描述符表(也可簡稱為保護模式下的中斷向量表)中一個表項占多少字節?其中哪幾位代表中斷處理代碼的入口?
- 請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。在idt_init函數中,依次對所有中斷入口進行初始化。使用mmu.h中的SETGATE宏,填充idt數組內容。每個中斷的入口由tools/vectors.c生成,使用trap.c中聲明的vectors數組即可。
- 請編程完善trap.c中的中斷處理函數trap,在對時鍾中斷進行處理的部分填寫trap函數中處理時鍾中斷的部分,使操作系統每遇到100次時鍾中斷后,調用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
【注意】除了系統調用中斷(T_SYSCALL)使用陷阱門描述符且權限為用戶態權限以外,其它中斷均使用特權級(DPL)為0的中斷門描述符,權限為內核態權限;而ucore的應用程序處於特權級3,需要采用`int 0x80`指令操作(這種方式稱為軟中斷,軟件中斷,Tra中斷,在lab5會碰到)來發出系統調用請求,並要能實現從特權級3到特權級0的轉換,所以系統調用中斷(T_SYSCALL)所對應的中斷門描述符中的特權級(DPL)需要設置為3。
要求完成問題2和問題3 提出的相關函數實現,提交改進后的源代碼包(可以編譯執行),並在實驗報告中簡要說明實現過程,並寫出對問題1的回答。完成這問題2和3要求的部分代碼后,運行整個系統,可以看到大約每1秒會輸出一次”100 ticks”,而按下的鍵也會在屏幕上顯示。
提示:可閱讀小節“中斷與異常”。
中斷描述符表
操作系統是由中斷驅動的,用於當某事件發生時,可以主動通知cpu及os進行處理,主要的中斷類型有外部中斷、內部中斷(異常)、軟中斷(陷阱、系統調用)。
- 外部中斷:用於cpu與外設進行通信,當外設需要輸入或輸出時主動向cpu發出中斷請求;
- 內部中斷:cpu執行期間檢測到不正常或非法條件(如除零錯、地址訪問越界)時會引起內部中斷;
- 系統調用:用於程序使用系統調用服務。
當中斷發生時,cpu會得到一個中斷向量號,作為IDT(中斷描述符表)的索引,IDT表起始地址由IDTR寄存器存儲,cpu會從IDT表中找到該中斷向量號相應的中斷服務程序入口地址,跳轉到中斷處理程序處執行,並保存當前現場;當中斷程序執行完畢,恢復現場,跳轉到原中斷點處繼續執行。
IDT的表項為中斷描述符,主要類型有中斷門、陷阱門、調用門,其中中斷門與陷阱門格式如下所示:
中斷門與陷阱門作為IDT的表項,每個表項占據8字節,其中段選擇子和偏移地址用來代表中斷處理程序入口地址,具體先通過選擇子查找GDT對應段描述符,得到該代碼段的基址,基址加上偏移地址為中斷處理程序入口地址。
初始化IDT
vectors.S文件為各中斷處理程序的入口,示例如下:
.text
.globl __alltraps
.globl vector0
vector0:
pushl $0
pushl $0
jmp __alltraps
.globl vector1
vector1:
pushl $0
pushl $1
jmp __alltraps
// 省略
# vector table
.data
.globl __vectors
__vectors:
.long vector0
.long vector1
.long vector2
__vectors在數據段,是存儲了各中斷處理程序入口地址的數組,每一個中斷處理程序依次將錯誤碼、中斷向量號壓棧(一些由cpu自動壓入錯誤碼的只壓入中斷向量號),再調用trapentry.S中的 __alltraps過程進行處理。
根據中斷門、陷阱門描述符格式使用SETGATE宏函數對IDT進行初始化,在這里先全部設為中斷門,中斷處理程序均在內核態執行,因此代碼段為內核的代碼段,DPL為內核態的0。
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void idt_init(void) {
extern uintptr_t __vectors[];
for (int i = 0; i < 256; i++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
lidt(&idt_pd);
}
在進行中斷處理時,會保存現場,將返回地址、中斷號、相關寄存器等數據保存到trapframe結構中:
/* registers as pushed by pushal */
struct pushregs {
uint32_t reg_edi;
uint32_t reg_esi;
uint32_t reg_ebp;
uint32_t reg_oesp; /* Useless */
uint32_t reg_ebx;
uint32_t reg_edx;
uint32_t reg_ecx;
uint32_t reg_eax;
};
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
__alltraps為各中斷處理程序的前置代碼,用於繼續在棧中完成trapframe結構,依次壓入ds、es、fs、gs、通用寄存器,並將數據段切換為內核數據段(代碼段在IDT初始化過程中設置為內核代碼段),最后壓入trapframe結構體指針作為trap函數的參數,再調用trap函數完成具體的中斷處理,代碼如下:
__alltraps:
# push registers to build a trap frame
# therefore make the stack look like a struct trapframe
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es
# push %esp to pass a pointer to the trapframe as an argument to trap()
pushl %esp
# call trap(tf), where tf=%esp
call trap
處理時鍾中斷
trap_dispatch函數根據trapframe獲取中斷號去處理相應中斷,處理時鍾中斷的代碼如下:
void trap(struct trapframe *tf) {
// dispatch based on what type of trap occurred
trap_dispatch(tf);
}
/* trap_dispatch - dispatch based on what type of trap occurred */
static void trap_dispatch(struct trapframe *tf) {
char c;
switch (tf->tf_trapno) {
case IRQ_OFFSET + IRQ_TIMER:
ticks++;
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;
}
}
中斷返回
trap函數執行完中斷處理程序后,恢復現場,重新彈出各寄存器值,iret指令彈出cs、eip、eflags,跳轉到之前中斷的地方繼續執行。
注:此篇並未考慮特權級變化時的中斷處理情況
執行結果
參考
- 操作系統真相還原
- ucore文檔-中斷與異常
- xv6-chinese文檔-中斷、陷入、驅動程序