位于./src/geekos/crc32.c
/*
* This routine writes each crc_table entry exactly once,
* with the correct final value. Thus, it is safe to call
* even on a table that someone else is using concurrently.
*/
void Init_CRC32(void) {
unsigned int i, j;
ulong_t h = 1;
crc_table[0] = 0;
for (i = 128; i; i >>= 1) {
h = (h >> 1) ^ ((h & 1) ? POLYNOMIAL : 0);
/* h is now crc_table[i] */
for (j = 0; j < 256; j += 2*i)
crc_table[i+j] = crc_table[j] ^ h;
}
}
这个函数好简单啊,最终填充了crc_table数组,看注释说是为了保护并发而设置的校验码。。。
----------------------------------------------------------------------------------------
马上下一个Init_TSS()
位于./src/geekos/tss.c
/*
* Initialize the kernel TSS. This must be done after the memory and
* GDT initialization, but before the scheduler is started.
*/
void Init_TSS(void)
{
s_tssDesc = Allocate_Segment_Descriptor();
KASSERT(s_tssDesc != 0);
memset(&s_theTSS, '\0', sizeof(struct TSS));
Init_TSS_Descriptor(s_tssDesc, &s_theTSS);
s_tssSelector = Selector(0, true, Get_Descriptor_Index(s_tssDesc));
Load_Task_Register();
}
第一个函数Allocate_Segment_Descriptor()我们已经在前面详细说过了,就是分配了可用的一个描述符。
看下一句 KASSERT(s_tssDesc != 0)确保返回了可用的描述符指针。
再下一句 Init_TSS_Descriptor(s_tssDesc, &s_theTSS);
位于./src/geekos/segment.c
/*
* Initialize a TSS descriptor.
*/
void Init_TSS_Descriptor(struct Segment_Descriptor* desc, struct TSS* theTSS)
{
Set_Size_And_Base_Bytes(desc, (ulong_t) theTSS, sizeof(struct TSS));
desc->type = 0x09; /* 1001b: 32 bit, !busy */
desc->system = 0;
desc->dpl = 0;
desc->present = 1;
desc->reserved = 0;
desc->dbBit = 0; /* must be 0 in TSS */
}
Init_TSS_Descriptor初始化TSS段的描述符
TSS任务状态段用于任务的切换。
看下一句
s_tssSelector = Selector(0, true, Get_Descriptor_Index(s_tssDesc));
Selector得到TSS段描述符s_tssDesc的在全局描述符表GDT中的索引
/**
* Construct a segment selector.
* @param rpl requestor privilege level; should be KERNEL_PRIVILEGE
* for kernel segments and USER_PRIVILEGE for user segments
* @param segmentIsInGDT true if the referenced segment descriptor
* is defined in the GDT, false if it is defined in the LDT
* @param index index of the segment descriptor
* @return the segment selector
*/
static __inline__ ushort_t Selector(int rpl, bool segmentIsInGDT, int index)
{
ushort_t selector = 0;
selector = (rpl & 0x3) | ((segmentIsInGDT ? 0 : 1) << 2) | ((index & 0x1FFF) << 3);
return selector;
}
随后
Load_Task_Register();
将TSS段选择子装入到TR寄存器。
但是现代操作系统一般都不用这个段,因为一个进程的信息量比TSS所能描述的信息量要大的多,这里我也不细说了。
----------------------------------------------------------------------------------------
回到主函数中的下一个初始化中断函数Init_Interrupts(),这是我们要研究的重点。
其实现位于./src/geekos/int.c
/*
* Initialize the interrupt system.
*/
void Init_Interrupts(void)
{
int i;
/* Low-level initialization. Build and initialize the IDT. */
Init_IDT();
/*
* Initialize all entries of the handler table with a dummy handler.
* This will ensure that we always have a handler function to call.
*/
for (i = 0; i < NUM_IDT_ENTRIES; ++i) {
Install_Interrupt_Handler(i, Dummy_Interrupt_Handler);
}
/* Re-enable interrupts */
Enable_Interrupts();
}
看第一个函数Init_IDT(),用于初始化中断描述符表
位于./src/geekos/idt.c
/*
* Initialize the Interrupt Descriptor Table.
* This will allow us to install C handler functions
* for interrupts, both processor-generated and
* those generated by external hardware.
*/
void Init_IDT(void)
{
int i;
ushort_t limitAndBase[3];
ulong_t idtBaseAddr = (ulong_t) s_IDT;
ulong_t tableBaseAddr = (ulong_t) &g_entryPointTableStart;
ulong_t addr;
Print("Initializing IDT...\n");
/* Make sure the layout of the entry point table is as we expect. */
KASSERT(g_handlerSizeNoErr == g_handlerSizeErr);
KASSERT((&g_entryPointTableEnd - &g_entryPointTableStart) ==
g_handlerSizeNoErr * NUM_IDT_ENTRIES);
/*
* Build the IDT.
* We're taking advantage of the fact that all of the
* entry points are laid out consecutively, and that they
* are all padded to be the same size.
*/
for (i = 0, addr = tableBaseAddr; i < NUM_IDT_ENTRIES; ++i) {
/*
* All interrupts except for the syscall interrupt
* must have kernel privilege to access.
*/
int dpl = (i == SYSCALL_INT) ? USER_PRIVILEGE : KERNEL_PRIVILEGE;
Init_Interrupt_Gate(&s_IDT[i], addr, dpl);
addr += g_handlerSizeNoErr;//加上函数的字节数,得到下一个函数的地址。(g_entryPointTableStart中的中断函数按照中断向量从小到大连续排列)
}
/*
* Cruft together a 16 bit limit and 32 bit base address
* to load into the IDTR.
*/
limitAndBase[0] = 8 * NUM_IDT_ENTRIES;
limitAndBase[1] = idtBaseAddr & 0xffff;
limitAndBase[2] = idtBaseAddr >> 16;
/* Install the new table in the IDTR. */
Load_IDTR(limitAndBase);
}
s_IDT是idt.c中静态分配的数组,它将成为中断描述符表,并被装入idtr中。
中断描述符中每一项有8B大小。
static union IDT_Descriptor s_IDT[ NUM_IDT_ENTRIES ];
看一下IDT_Descriptor的定义
位于./include/geekos/idt.h
struct Interrupt_Gate {
ushort_t offsetLow;
ushort_t segmentSelector;
unsigned reserved : 5;
unsigned signature : 8;
unsigned dpl : 2;
unsigned present : 1;
ushort_t offsetHigh;
};
union IDT_Descriptor {
struct Interrupt_Gate ig;
/*
* In theory we could have members for trap gates
* and task gates if we wanted.
*/
};
理论上,在IDT中可以放三种门描述符:trap gate、task gate、interrupt gate。这里只描述了interrupt gate。
看下一个,变量g_entryPointTableStart是中断入口地址。
位于./src/geekos/lowlevel.asm
; ----------------------------------------------------------------------
; Generate interrupt-specific entry points for all interrupts.
; We also define symbols to indicate the extend of the table
; of entry points, and the size of individual entry points.
; ----------------------------------------------------------------------
align 8
g_entryPointTableStart:
; Handlers for processor-generated exceptions, as defined by
; Intel 486 manual.
Int_No_Err 0
align 8
Before_No_Err:
Int_No_Err 1
align 8
After_No_Err:
Int_No_Err 2 ; FIXME: not described in 486 manual
Int_No_Err 3
Int_No_Err 4
Int_No_Err 5
Int_No_Err 6
Int_No_Err 7
align 8
Before_Err:
Int_With_Err 8
align 8
After_Err:
Int_No_Err 9 ; FIXME: not described in 486 manual
Int_With_Err 10
Int_With_Err 11
Int_With_Err 12
Int_With_Err 13
Int_With_Err 14
Int_No_Err 15 ; FIXME: not described in 486 manual
Int_No_Err 16
Int_With_Err 17
; The remaining interrupts (18 - 255) do not have error codes.
; We can generate them all in one go with nasm's %rep construct.
%assign intNum 18
%rep (256 - 18)
Int_No_Err intNum
%assign intNum intNum+1
%endrep
可以看到,这是一个中断入口表,前18个中断的触发源是固定的,用于处理异常和不可屏蔽中断。
Int_No_Err和Int_With_Err是两个宏
; Template for entry point code for interrupts that have
; an explicit processor-generated error code.
; The argument is the interrupt number.
%macro Int_With_Err 1
align 8
push dword %1 ; push interrupt number
jmp Handle_Interrupt ; jump to common handler
%endmacro
; Template for entry point code for interrupts that do not
; generate an explicit error code. We push a dummy error
; code on the stack, so the stack layout is the same
; for all interrupts.
%macro Int_No_Err 1
align 8
push dword 0 ; fake error code
push dword %1 ; push interrupt number
jmp Handle_Interrupt ; jump to common handler
%endmacro
两个宏的区别只是Int_No_Err先压入了一个4B的error code,都是压入了中断号并跳至中断处理函数Handle_Interrupt。
Handle_Interrupt如下
; Common interrupt handling code.
; Save registers, call C handler function,
; possibly choose a new thread to run, restore
; registers, return from the interrupt.
align 8
Handle_Interrupt:
; Save registers (general purpose and segment)
Save_Registers
; Ensure that we're using the kernel data segment
mov ax, KERNEL_DS
mov ds, ax
mov es, ax
; Get the address of the C handler function from the
; table of handler functions.
mov eax, g_interruptTable ; get address of handler table,中断函数地址表
mov esi, [esp+REG_SKIP] ; get interrupt number,得到中断向量
mov ebx, [eax+esi*4] ; get address of handler function,计算得到对应的中断函数地址
; Call the handler.
; The argument passed is a pointer to an Interrupt_State struct,
; which describes the stack layout for all interrupts.
push esp ;
call ebx ; 调用中断函数
add esp, 4 ; clear 1 argument
; If preemption is disabled, then the current thread
; keeps running.
cmp [g_preemptionDisabled], dword 0 ;0表示可以抢占,1表示禁止抢占。(初始化为0)
jne .restore
; See if we need to choose a new thread to run.
cmp [g_needReschedule], dword 0
je .restore
; Put current thread back on the run queue
push dword [g_currentThread] ; 压入作为Make_Runnable的参数
call Make_Runnable ; Make_Runnable使之前的线程进入运行队列
add esp, 4 ; clear 1 argument
; Save stack pointer in current thread context, and
; clear numTicks field.
mov eax, [g_currentThread] ; 保存当前进程上下文的esp
mov [eax+0], esp ; esp field
mov [eax+4], dword 0 ; numTicks field
; Pick a new thread to run, and switch to its stack
call Get_Next_Runnable
mov [g_currentThread], eax ; 得到将要运行的线程结构
mov esp, [eax+0] ; esp field 恢复将运行的线程栈
; Clear "need reschedule" flag ,清除标记
mov [g_needReschedule], dword 0
.restore:
; Restore registers,两种可能,1:恢复的是原来中断函数的上下文;2:恢复的是新运行线程的上下文
Restore_Registers
; Return from the interrupt.返回到原来线程,或者新的线程
iret
Save_Registers和Restore_Registers两个宏
位于./src/geekos/lowlevel.asm中,用于从栈中保存和恢复通用寄存器。
; Save registers prior to calling a handler function.
; This must be kept up to date with:
; - Interrupt_State struct in int.h
; - Setup_Initial_Thread_Context() in kthread.c
%macro Save_Registers 0
push eax
push ebx
push ecx
push edx
push esi
push edi
push ebp
push ds
push es
push fs
push gs
%endmacro
; Restore registers and clean up the stack after calling a handler function
; (i.e., just before we return from the interrupt via an iret instruction).
%macro Restore_Registers 0
pop gs
pop fs
pop es
pop ds
pop ebp
pop edi
pop esi
pop edx
pop ecx
pop ebx
pop eax
add esp, 8 ; skip int num and error code
%endmacro
可以看到,Handle_Interrupt从全局的中断处理函数指针数组中根据中断向量得到相应的中断处理函数,并跳转执行。
执行完中断函数后,它判断系统是否允许抢占,若允许则判断系统调度标志g_needReaschedule是否不为0,若不为0,则保存当前上下文。在运行队列中得到可运行线程的结构体,并切换到此线程的堆栈。最后的iret就切换到新线程运行了。
这是理解系统多任务的关键。切换线程上下文所需要保存的参数格式在
./include/geekos/int.h中
/*
* This struct reflects the contents of the stack when
* a C interrupt handler function is called.
* It must be kept up to date with the code in "lowlevel.asm".
*/
struct Interrupt_State {
/*
* The register contents at the time of the exception.
* We save these explicitly.
*/
uint_t gs;
uint_t fs;
uint_t es;
uint_t ds;
uint_t ebp;
uint_t edi;
uint_t esi;
uint_t edx;
uint_t ecx;
uint_t ebx;
uint_t eax;
/*
* We explicitly push the interrupt number.
* This makes it easy for the handler function to determine
* which interrupt occurred.
*/
uint_t intNum;
/*
* This may be pushed by the processor; if not, we push
* a dummy error code, so the stack layout is the same
* for every type of interrupt.
*/
uint_t errorCode;
/* These are always pushed on the stack by the processor. */
uint_t eip;
uint_t cs;
uint_t eflags;
};
中断函数在Init_Interrupt()中通过Install_Interrupt_Handler()插入到中断处理函数指针数组中。
两个KASSERT确定中断函数表的格式是我们预期的。
接下来代码进入for循环,dpl用于指定中断门的特权级,除了中断向量为SYSCALL_INT的中断门为用户特权级外,其余的中断门都属于内核特权级。
看下一个函数Init_Interrupt_Gate(),它在s_IDT填充相应的中断函数入口地址和特权级
/*
* Initialize an interrupt gate with given handler address
* and descriptor privilege level.
*/
void Init_Interrupt_Gate(union IDT_Descriptor* desc, ulong_t addr,
int dpl)
{
desc->ig.offsetLow = addr & 0xffff;
desc->ig.segmentSelector = KERNEL_CS;
desc->ig.reserved = 0;
desc->ig.signature = 0x70; /* == 01110000b */
desc->ig.dpl = dpl;
desc->ig.present = 1;
desc->ig.offsetHigh = addr >> 16;
}
for循环结束后,使用 Load_IDTR(limitAndBase)将s_IDT数组信息装入idtr寄存器,s_IDT将成为新的中断描述符表(之前在setup.asm中只是简单的将idtr寄存器以0填充)。
Load_IDTR()位于/src/geekos/lowlevel.asm
; Load IDTR with 6-byte pointer whose address is passed as
; the parameter.
align 8
Load_IDTR:
mov eax, [esp+4]
lidt [eax]
ret
至此Init_Interrupts()函数中的Init_IDT()完成了初始化中断描述符表。
看到Init_Interrupts()中的下一句Install_Interrupt_Handler()安装中断函数
位于.src/geekos/idt.c
/*
* Install a C handler function for given interrupt.
* This is a lower-level notion than an "IRQ", which specifically
* means an interrupt triggered by external hardware.
* This function can install a handler for ANY interrupt.
*/
void Install_Interrupt_Handler(int interrupt, Interrupt_Handler handler)
{
KASSERT(interrupt >= 0 && interrupt < NUM_IDT_ENTRIES);
g_interruptTable[interrupt] = handler;
}
很简单,为中断函数数组中相应的中断号赋函数地址,这里具体为每一个中断向量都赋了一个相同的函数地址,Dummy_Interrupt_Handler(),打印信息说明此中断无效,并死机。
看Init_Interrupts()函数最后一句,函数Enable_Interrupts()
#define Enable_Interrupts() \
do { \
KASSERT(!Interrupts_Enabled()); \
__Enable_Interrupts(); \
} while (0)
开启中断,确保中断是开着的,这里的开启中断的作用,就类似于当我们想让vi回到normal模式下时,多按1下esc。。。。。
Init_Interrupts()函数初始化了中断描述符表,并为每一个中断向量安装了默认中断函数,最后开启中断。
中断的过程如下:
CPU收到中断(外部异步中断或者内部同步中断)得到中断向量int_num-->根据中断向量在IDT中索引得到中断函数地址(idtBaseAddr+8*int_num)-->此函数会跳到一个公用的Handle_Interrupt中-->Handle_Interrupt会从堆栈中得到中断向量-->并调用g_interruptTable[int_num]执行真正的中断处理函数,响应用户。
到此Init_Interrupts()函数结束。