09保护模式下的进程调度 - 图文
更新时间:2024-03-31 14:21:01 阅读量: 综合文库 文档下载
- 当前IE进程处于保护模式推荐度:
- 相关推荐
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000保护模式下的进程
调度
本实验采用C与汇编语言混合编程,利用计时中断实现保护模式下多个进程间的简单切换。
1 程序结构
1.1 运行过程
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
系统加电或重启 ------------------------------------------- 系统初始化 系统BIOS例程 加载引导扇区
控制权交给引导扇区 -------------------------------------- 加载loader 引导扇区(BOOT5) 跳转到loader ------------------------------------------------ 获取并显示内存信息 定义并初始化GDT 进入保护模式 装载(LOADER) 启动分页机制 加载kernel
跳转到kernel ------------------------------------------------ 切换到内核GDT 内核(KERNEL) 初始化8259A ************************ 初始化IDT protect.c:init_prot() * 在GDT中添加TSS和3个LDT描述符 * 初始化TSS ************************** 初始化进程表 ************************ 设置时钟中断处理程序 * 打开时钟中断 main.c:kerel_main() * 调用restart ***************************
进程切换 -----------------------------------------------------
1.2 程序目录
下面是我们程序目录ps中的子目录结构及文件的功用。 tree .
├── a.img (FAT12软盘映像:将复制进文件LOADER.BIN和KERNEL.BIN) ├── boot (引导&装入)
│ ├── boot.asm (引导扇区:FAT12、装入LOADER.BIN) │ ├── include (包含汇编程序)
│ │ ├── fat12hdr.inc (FAT12的常量定义) │ │ ├── load.inc (装载地址的常量定义) │ │ └── pm.inc (保护模式的常量与宏定义)
│ └── loader. asm (装载程序:获取并显示内存信息、定义并初始化GDT、 ├── include (包含头文件) 进入保护模式、启动分页机制、加载kernel) │ ├── const.h (定义类型和常量)
│ ├── global.h (定义全局数据结构[数组]) │ ├── proc.h (定义进程表和任务结构)
│ ├── protect.h (定义保护模式下的数据结构类型和符号常量) │ ├── proto.h (定义函数的原型)
│ ├── sconst.inc (定义汇编程序用的PCB、时钟控制器常量和选择符常量) │ ├── string.h (定义memcpy和memset的函数原型) │ └── type.h (定义无符号整数和函数指针类型) ├── kernel (内核程序)
│ ├── clock.c (定义时钟中断处理程序clock_handler) │ ├── global.c (定义全局数据结构[数组]) │ ├── i8259.c (初始化时钟控制器)
│ ├── kernel.asm (切换GDT、装入IDT和TTS、调用main.c中的kerel_main()、 │ │ 定义异常和硬件中断处理、定义save和restart函数) │ ├── main.c (初始化进程表、定义用户进程函数)
│ ├── protect.c (定义异常与硬件中断处理、初始化TSS和LDT描述符) │ └── start.c (复制GDT、初始化IDT) ├── lib (库函数)
│ ├── kliba.asm (定义显示串、端口I/O和开闭硬件中断的汇编函数) │ ├── klib.c (定义整数转字符串、显示整数和延时函数) │ └── string. asm (定义内存复制、内存设置和串复制函数)
└── Makefile (定义用于自动编译和软盘挂载/文件复制的Make文件)
1.3 逻辑框图
与实模式下进程调度的类似。
系统启动 引导扇区 装载程序 加载内核(含三个应用程序函数) 计时器 中断处理程序 进程调度 内核程序 进程A 进程B 进程C 全局逻辑图
(红色为内核模块,绿色为进程调度)
进程运行 中断发生 利用PCB保存进程状态 挂起进程 进程调度 启动下一进程 进程切换
1.4 运行结果
2 程序模块
boot.asm与我们的boot5.asm相同,loader.asm及其3个包含文件fat12hdr.inc、load.inc和pm.inc与上次实验中的相同,没有必要重复介绍。下面只是关于内核程序诸模块的功能
介绍与代码分析。
2.1 应用程序函数
为了简单,我们在内核程序组中增加了C程序main.c,在其中初始化进程表,并定义3个函数TestA、TestB、TestC来充当3个用户进程。
2.1.1 进程函数TestX
这些进程函数很简单,只是显示字符串“A”/“B”/“C”+计数+句点的死循环。 下面它们的C语言源代码(main.c):
/*===========================================================* TestA
*===========================================================*/ void TestA() { int i = 0; while(1){ disp_str(\ disp_int(i++); disp_str(\ delay(1); } }
/*===========================================================* TestB
*===========================================================*/ void TestB() { int i = 0x1000; while(1){ disp_str(\ disp_int(i++); disp_str(\ delay(1); } }
/*===========================================================* TestC
*===========================================================*/ void TestC()
{ }
int i = 0x2000; while(1){ disp_str(\ disp_int(i++); disp_str(\ delay(1); }
2.1.2 串显示函数disp_str
进程函数中的字符串显示函数disp_str被定义在汇编库程序kliba.asm(也与上次实验的相同)中:
; ============================================================= ; void disp_str(char * info);
; ============================================================= disp_str: push ebp mov ebp, esp mov esi, [ebp + 8] ; pszInfo mov edi, [disp_pos] mov ah, 0Fh .1: lodsb ; ds:esi -> al test al, al jz .2 cmp al, 0Ah ; 是回车吗? jnz .3 push eax mov eax, edi mov bl, 160 div bl and eax, 0FFh inc eax mov bl, 160 mul bl mov edi, eax pop eax jmp .1 .3: mov [gs:edi], ax add edi, 2
jmp .1 .2: mov [disp_pos], edi pop ebp ret
该函数通过改写显存内容来显示字符(绿色代码)。在此函数中字符串被显示在由全局变量disp_pos所指定的位置,显示完后再更新该变量的值(红色代码)。如果显示的字符是回车符,则让EAX = EDI(=disp_pos),EAX/160 = AL,EDI =(AL++)* 160为下一行的首地址(蓝色代码)。因每行80列,每个字符位占两个字节(AL=字符、AH=颜色),所以一行为160B。
2.1.3 延时函数delay
延时函数delay则被定义在C库程序klib.c中,它是一个三层循环,最外层的循环是函数的输入参数,在我们的程序中取为1:
/*===========================================================* delay
*===========================================================*/ PUBLIC void delay(int times) { int i, j, k; for(k=0;k 2.2 入口函数 内核的入口在kernel.asm的_start函数(与上次实验的5.1中的代码相似,红色代码为不同的部分)中: _start: ; 把 esp 从 LOADER 挪到 KERNEL mov esp, StackTop ; 堆栈在 bss 段中 mov dword [disp_pos], 0 sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT lgdt [gdt_ptr] ; 使用新的GDT lidt [idt_ptr] jmp SELECTOR_KERNEL_CS:csinit csinit: ; “这个跳转指令强制使用刚刚初始化的结构” xor eax, eax mov ax, SELECTOR_TSS ltr ax jmp kernel_main 这段代码所完成的工作有: ? 更新堆栈指针ESP= StackTop(跳转到kernel时已经切换了SS) ? 初始化disp_pos=0 ? 切换GDT:保存老GDTR、调用cstar(复制GDT、初始化IDT)、装入新GDT ? 装入IDT和TSS ? 跳转到main.c中的kerel_main()函数来初始化进程表、设置时钟中断处理程序、打 开时钟中断、调用restart函数 在汇编程序kernel.asm中还 ? 定义了异常和硬件中断处理 ? 定义了save(用于在中断处理中保存CPU上下文寄存器)和restart(被save调用, 用于从内核切换到用户进程)函数 2.3 PCB与进程表和任务表 2.3.1 定义PCB 我们在头文件proc.h中,定义了自己的PCB(Process Control Block,进程控制块)结构类型PROCESS: typedef struct s_proc { STACK_FRAME regs; /* 保存在栈帧中的处理器寄存器值 */ u16 ldt_sel; /* GDT中的LDT选择符,含LDT的基址和界限 */ DESCRIPTOR ldt[LDT_SIZE]; /* 含代码与数据段描述符的LDT */ u32 pid; /* 进程ID */ char p_name[16]; /* 进程名串(16个字符)*/ }PROCESS; 其中,符号常量LDT_SIZE =2在头文件protect.h中定义: /* 每个任务有一个单独的 LDT, 每个 LDT 中的描述符个数: */ #define LDT_SIZE 2 其中,用于保存处理器寄存器值的栈帧结构类型STACK_FRAME的定义为: typedef struct s_stackframe { /* proc_ptr指向这里 ↑ Low */ u32 gs; /* ┓ │ */ u32 fs; /* ┃ │ */ u32 es; /* ┃ │ */ u32 ds; /* ┃ │ */ u32 edi; /* ┃ │ */ u32 esi; /* ┣ 由save()压栈 │ */ u32 ebp; /* ┃ │ */ u32 kernel_esp;/*┃<- 'popad'会忽略它 │ */ u32 ebx; /* ┃ ↑栈增长方向*/ u32 edx; /* ┃ │ */ u32 ecx; /* ┃ │ */ u32 eax; /* ┛ │ */ u32 retaddr; /* <- save()函数的返回地址 │ */ u32 eip; /* ┓ │ */ u32 cs; /* ┃ │ */ u32 eflags; /* ┣ 由CPU在中断时压栈 │ */ u32 esp; /* ┃ │ */ u32 ss; /* ┛ ┷ High */ }STACK_FRAME; 所以我们的PCB(共有110B)为: GS:辅助段寄存器 -------------4个段寄存器------------- 4*4 B = 16B(原4*2B = 8B) FS:辅助段寄存器 在save函数中 ES:辅助段寄存器 手工压栈 DS:数据段寄存器 -------------------------------------------- EDI:目的变址寄存器 ----------8个通用寄存器---------- 8*4B = 32B ESI:源变址寄存器 EBP:基址指针寄存器 ESP:堆栈指针寄存器 在save函数中 EBX:基址寄存器 用PUSHAD压栈 EDX:数据寄存器 ECX:计数寄存器 EAX:累加寄存器 -------------------------------------------- save函数返回地址 由CALL指令压栈 4B EIP:指令指针寄存器 ----------------------- 5*4B =20B(原3*4B + 2*2B = 16B) CS:代码段寄存器 在中断时 ESP:栈顶指针 EFLAGS:标志寄存器 由CPU压栈 SS:堆栈段寄存器 --------------------------- LDT选择符 2B 代码段描述符 ------- 进程的LDT -------- 2*8B = 16B 数据段描述符 --------------------------------- 进程ID 4B 进程名串 16B 注意:32位系统堆栈的压栈的基本单位为32位=4B,所以本来只有16位的段寄存器在栈中也需要占32位。 这里的PCB与我们前面讲过的实模式进程调度中的PCB相比,主要差别是多数16位寄存器被改成了32位的,另外还增加了save函数返回地址和LDT项(红色部分)。 2.3.2 定义进程表 为了简单,我们用数组(而不是链表)来实现进程表,进程表含若干PCB项。进程表数组proc_table在global.c中定义: PUBLIC PROCESS proc_table[NR_TASKS]; 其中的符号常量NR_TASKS = 3,在proc.h中定义: /* Number of tasks */ #define NR_TASKS 3 2.3.3 任务表 我们定义自己的(包含用户进程的任务信息的)任务结构类型,并创建了一个含3个用 户任务的任务表数组,它们主要用于用户PCB的初始化。 ? 定义任务结构类型(proc.h): typedef struct s_task { task_f initial_eip; int stacksize; char name[16]; }TASK; 任务结构中包含初始的EIP(用户任务函数的指针)、栈的大小和任务的名串。其中的任务函数指针类型task_f被定义在type.h中: typedef void (*task_f) (); ? 定义并初始化任务表(global.c): PUBLIC TASK task_table[NR_TASKS] = {{TestA, STACK_SIZE_TESTA, \ {TestB, STACK_SIZE_TESTB, \ {TestC, STACK_SIZE_TESTC, \其中,函数指针TestX用到的函数原型被定义在proto.h中: /* main.c */ void TestA(); void TestB(); void TestC(); 符号常量STACK_SIZE_TESTx被定义在proc.h中: /* stacks of tasks */ #define STACK_SIZE_TESTA 0x800 #define STACK_SIZE_TESTB 0x800 #define STACK_SIZE_TESTC 0x800 #define STACK_SIZE_TOTAL (STACK_SIZE_TESTA + \\ STACK_SIZE_TESTB + \\ STACK_SIZE_TESTC) 我们在global.c中专门为这3个进程定义了单独的栈: PUBLIC char task_stack[STACK_SIZE_TOTAL]; 2.3.4 初始化进程表 我们在main.c的kernel_main函数中初始化进程表: /*===========================================================* kernel_main *===========================================================*/ PUBLIC int kernel_main() { disp_str(\ TASK* p_task = task_table; PROCESS* p_proc = proc_table; char* p_task_stack = task_stack + STACK_SIZE_TOTAL; u16 selector_ldt = SELECTOR_LDT_FIRST; int i; for (i = 0; i < NR_TASKS; i++) { strcpy(p_proc->p_name, p_task->name); // name of the process p_proc->pid = i; // pid p_proc->ldt_sel = selector_ldt; memcpy(&p_proc->ldt[0], &gdt[SELECTOR_KERNEL_CS >> 3], sizeof(DESCRIPTOR)); p_proc->ldt[0].attr1 = DA_C | PRIVILEGE_TASK << 5; memcpy(&p_proc->ldt[1], &gdt[SELECTOR_KERNEL_DS >> 3], sizeof(DESCRIPTOR)); p_proc->ldt[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5; p_proc->regs.cs = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.ds = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.es = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.fs = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.ss = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; p_proc->regs.gs = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK; p_proc->regs.eip = (u32)p_task->initial_eip; p_proc->regs.esp = (u32)p_task_stack; p_proc->regs.eflags = 0x1202; /* IF=1, IOPL=1 */ p_task_stack -= p_task->stacksize; p_proc++; p_task++; selector_ldt += 8; } k_reenter = 0; p_proc_ready = proc_table; put_irq_handler(CLOCK_IRQ, clock_handler); /* 设置时钟中断处理程序 */ enable_irq(CLOCK_IRQ); /* 允许时钟中断 */ restart(); while(1){} } 从代码不难看出: ? PCB中的进程名串是从任务表的对应项复制过来的 ? 进程ID=进程在进程表中的序号0/1/2 ? LDT选择符定义为本进程的LDT描述符在GDT中的偏移量(0x28+i*8) ? LDT中进程的代码和数据段描述符是从内核对应描述符复制过来的,只是它们属性中 的特权级改成了1 ? 段寄存器CS为代码段的选择符,指向LDT中的第一个描述符(8*0,内核代码段) ? 段寄存器GS为显存段的选择符,指向GDT中的对应段描述符,只是特权级从3改为 了1 ? 其余4个段寄存器都指向LDT中的第二个描述符(8*1,内核数据段) ? EIP=指向任务表中对应的函数指针TestX ? ESP=独立堆栈的不同位置(利用任务表中的栈大小值):TestA在栈的顶部、Test ? EFLAGS=0x1202,相当于IF=1和IOPL=1(其中IOPL是80286增加的标志位,占2 个二进制位[第12~13位],为当前程序或任务的I/O特权级标志。任务或程序的CPL必须小于或等于IOPL值时,才能够访问I/O端口) 在上面的代码里面,用到了一些GDT和存储段描述符类型值的符号常量,它们在protect.h中定义: /* GDT */ /* 描述符索引 */ #define INDEX_DUMMY 0 // ┓ #define INDEX_FLAT_C 1 // ┣ LOADER 里面已经确定了的. #define INDEX_FLAT_RW 2 // ┃ #define INDEX_VIDEO 3 // ┛ #define INDEX_TSS 4 #define INDEX_LDT_FIRST 5 /* 选择子 */ #define SELECTOR_DUMMY 0 // ┓ #define SELECTOR_FLAT_C 0x08 // ┣ LOADER 里面已经确定了的. #define SELECTOR_FLAT_RW 0x10 // ┃ #define SELECTOR_VIDEO (0x18+3) // ┛<-- RPL=3 #define SELECTOR_TSS 0x20 // TSS从外层跳到内层时SS和ESP #define SELECTOR_LDT_FIRST 0x28 // 的值从里面获得 #define SELECTOR_KERNEL_CS SELECTOR_FLAT_C #define SELECTOR_KERNEL_DS SELECTOR_FLAT_RW #define SELECTOR_KERNEL_GS SELECTOR_VIDEO ?? /* 存储段描述符类型值说明 */ #define DA_DR 0x90 /* 存在的只读数据段类型值 */ #define DA_DRW 0x92 /* 存在的可读写数据段属性值 */ #define DA_DRWA 0x93 /* 存在的已访问可读写数据段类型值*/ #define DA_C 0x98 /* 存在的只执行代码段属性值 */ ?? 选择符的符号常量在protect.h中定义: /* 选择子类型值说明 */ /* 其中, SA_ : Selector Attribute */ #define SA_RPL_MASK 0xFFFC /* 最低2位为0 */ #define SA_RPL0 0 #define SA_RPL1 1 #define SA_RPL2 2 #define SA_RPL3 3 #define SA_TI_MASK 0xFFFB /* 第3位为0 */ #define SA_TIG 0 #define SA_TIL 4 特权级的符号常量在const.h中定义: /* 权限 */ #define PRIVILEGE_KRNL 0 #define PRIVILEGE_TASK 1 #define PRIVILEGE_USER 3 /* RPL */ #define RPL_KRNL SA_RPL0 #define RPL_TASK SA_RPL1 #define RPL_USER SA_RPL3 2.3.5 填充TSS和LDT描述符 在从loader.asm复制过来的GDT中,只定义了4个描述符,并没有我们现在要用的TSS描述符和3个进程的LDT描述符,它们都是后来在protect.c中的init_prot函数的末尾处新增加的: /* 填充GDT中TSS描述符 */ memset(&tss, 0, sizeof(tss)); tss.ss0 = SELECTOR_KERNEL_DS; tss.iobase = sizeof(tss); /* 没有I/O许可位图 */ init_descriptor(&gdt[INDEX_TSS], vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss), sizeof(tss) - 1, DA_386TSS); /* 填充 GDT 中3个用户进程的LDT描述符 */ int i; PROCESS* p_proc = proc_table; u16 selector_ldt = INDEX_LDT_FIRST << 3; for(i=0;i 2.3.6 关系图 PCB GS FS ES DS EDI ESI EBP ESP EBX EDX ECX EAX retaddr EIP CS EFLAGS ESP SS LDT选择符 代码段描述符 数据段描述符 进程ID 进程名串 GDT 空 内核代码段描述符 内核数据段描述符 显存段描述符 TSS描述符 LDTA描述符 LDTB描述符 LDTC描述符 TSS LDT 代码 数据 堆栈 进程A 2.4 时钟中断处理 2.4.1 IDT 在保护模式下,每当中断发生时,CPU会由IDTR寄存器中的IDT起始地址找到当前的IDT,并根据中断号在IDT中定位对应的中断门描述符,利用该描述符中的段选择符和偏移量,就可以得到该中断所对应处理程序的入口地址了。 ? 硬件中断宏与处理函数 内核程序在kernel.asm中定义了硬件中断的处理函数宏hwint_master和具体的处理函数hwint00~16: ; 中断和异常 -- 硬件中断 ; --------------------------------- %macro hwint_master 1 call save in al, INT_M_CTLMASK ; `. or al, (1 << %1) ; | 屏蔽当前中断 out INT_M_CTLMASK, al ; / mov al, EOI ; `. 置EOI位 out INT_M_CTL, al ; / sti ; CPU在响应中断的过程中会自动关中断,这句之后就允许响应新的中断 push %1 ; `. call [irq_table + 4 * %1] ; | 中断处理程序 pop ecx ; / cli in al, INT_M_CTLMASK ; `. and al, ~(1 << %1) ; | 恢复接受当前中断 out INT_M_CTLMASK, al ; / ret %endmacro ALIGN 16 hwint00: ; Interrupt routine for irq 0 (the clock). hwint_master 0 ?? 可见,内核程序的硬件中断处理,是先调用save函数,再调用我们自己定义的硬件中断向量表irq_table中的对应表项。 ? 初始化IDT 内核程序在protect.c中的init_prot里,对(8259A中断控制器和)IDT进行了初始化: /*=========================================================* init_prot *----------------------------------------------------------------------* 初始化 IDT *=========================================================*/ PUBLIC void init_prot() { init_8259A(); // 全部初始化成中断门(没有陷阱门) init_idt_desc(INT_VECTOR_DIVIDE, DA_386IGate, divide_error, PRIVILEGE_KRNL); ?? init_idt_desc(INT_VECTOR_IRQ0 + 0, DA_386IGate, hwint00, PRIVILEGE_KRNL); ?? } /*===========================================================* init_idt_desc *----------------------------------------------------------------------* 初始化 386 中断门 *===========================================================*/ PUBLIC void init_idt_desc(unsigned char vector, u8 desc_type, int_handler handler, unsigned char privilege) { GATE * p_gate = &idt[vector]; u32 base = (u32)handler; p_gate->offset_low = base & 0xFFFF; p_gate->selector = SELECTOR_KERNEL_CS; p_gate->dcount = 0; p_gate->attr = desc_type | (privilege << 5); p_gate->offset_high = (base >> 16) & 0xFFFF; } ? 装入IDT 在global.h中定义,在global.c中实现IDT指针变量: u8 gdt_ptr[6]; // 0~15:Limit 16~47:Base 在start.c的cstart函数中设置gdt_ptr的值: // idt_ptr[6]共6个字节:0~15:Limit、16~47:Base。用作sidt以及lidt的参数。 u16* p_idt_limit = (u16*)(&idt_ptr[0]); u32* p_idt_base = (u32*)(&idt_ptr[2]); *p_idt_limit = IDT_SIZE * sizeof(GATE) - 1; *p_idt_base = (u32)&idt; 在kernel.asm的_start函数中装入IDT: lidt [idt_ptr] 2.4.2 硬件中断处理 ? 初始化中断控制器8259A和硬件中断向量表 内核程序在i8259.c中定义了初始化中断控制器8259A的函数init_8259A(在上面介绍 的init_prot函数的开始处被调用),将主8259A的起始中断号设置成20h,关闭所有硬件中断,并用(只是显示中断号信息的)伪中断处理函数spurious_irq来初始化我们自己的硬件中断向量表中的所有16个(入口函数)表项: /*==========================================================* init_8259A *==========================================================*/ PUBLIC void init_8259A() { out_byte(INT_M_CTL, 0x11); // Master 8259, ICW1. out_byte(INT_S_CTL, 0x11); // Slave 8259, ICW1. // Master 8259, ICW2. 设置 '主8259' 的中断入口地址为 0x20: out_byte(INT_M_CTLMASK, INT_VECTOR_IRQ0); // Slave 8259, ICW2. 设置 '从8259' 的中断入口地址为 0x28: out_byte(INT_S_CTLMASK, INT_VECTOR_IRQ8); out_byte(INT_M_CTLMASK, 0x4);// Master 8259, ICW3. IR2 对应 '从8259'. out_byte(INT_S_CTLMASK, 0x2);// Slave 8259, ICW3. 对应 '主8259' 的 IR2. out_byte(INT_M_CTLMASK, 0x1); // Master 8259, ICW4. out_byte(INT_S_CTLMASK, 0x1); // Slave 8259, ICW4. out_byte(INT_M_CTLMASK, 0xFF); // Master 8259, OCW1. out_byte(INT_S_CTLMASK, 0xFF); // Slave 8259, OCW1. int i; for (i = 0; i < NR_IRQ; i++) irq_table[i] = spurious_irq; } /*===========================================================* spurious_irq *===========================================================*/ PUBLIC void spurious_irq(int irq) { disp_str(\ disp_int(irq); disp_str(\} 其中的符号常量INT_VECTOR_IRQ0 = 32、INT_VECTOR_IRQ8 = 40,在protect.h中定义: /* 中断向量 */ #define INT_VECTOR_IRQ0 0x20 #define INT_VECTOR_IRQ8 0x28 硬件中断向量表在global.c中定义: PUBLIC irq_handler irq_table[NR_IRQ]; 其中的irq_handler是在type.h中定义的一种函数指针类型: typedef void (*irq_handler) (int irq); 而硬件中断用的符号常量NR_IRQ(=16)等,则被定义在const.h中: /* Hardware interrupts */ #define NR_IRQ 16 /* Number of IRQs */ #define CLOCK_IRQ 0 #define KEYBOARD_IRQ 1 ?? ? 设置时钟中断处理 在main.c中的kernel_main函数的末尾处: put_irq_handler(CLOCK_IRQ, clock_handler); /* 设置时钟中断处理程序 */ enable_irq(CLOCK_IRQ); /* 允许时钟中断 */ 其中,put_irq_handler函数被定义在i8259.c的末尾: /*=======================================================* put_irq_handler *=======================================================*/ PUBLIC void put_irq_handler(int irq, irq_handler handler) { disable_irq(irq); irq_table[irq] = handler; } enable_irq函数则被定义在kliba.asm的末尾: ; ========================================================= ; void enable_irq(int irq); ; ========================================================= ; Enable an interrupt request line by clearing an 8259 bit. ; Equivalent code: ; if(irq < 8) ; out_byte(INT_M_CTLMASK, in_byte(INT_M_CTLMASK) & ~(1 << irq)); ; else ; out_byte(INT_S_CTLMASK, in_byte(INT_S_CTLMASK) & ~(1 << irq)); ; enable_irq: mov ecx, [esp + 4] ; irq pushf cli mov ah, ~1 rol ah, cl ; ah = ~(1 << (irq % 8)) cmp cl, 8 jae enable_8 ; enable irq >= 8 at the slave 8259 enable_0: in al, INT_M_CTLMASK and al, ah out INT_M_CTLMASK, al ; clear bit at master 8259 popf ret enable_8: in al, INT_S_CTLMASK and al, ah out INT_S_CTLMASK, al ; clear bit at slave 8259 popf ret 2.4.3 时钟中断处理函数 时钟中断处理函数clock_handler被定义在clock.c中: /*==========================================================* clock_handler *==========================================================*/ PUBLIC void clock_handler(int irq) { disp_str(\ if (k_reenter != 0) { /* 为中断重入 */ disp_str(\ return; /* 显示“!”后直接返回 */ } /* 无中断重入时切换到下一个PCB */ p_proc_ready++; if (p_proc_ready >= proc_table + NR_TASKS) { p_proc_ready = proc_table; } } 在此函数中,首先显示单字符的字符串“#”,然后判断是否为中断重入,若是则显示字符串“!”后直接返回,若不是则让当前进程指针指向下一个PCB。 2.4.4 中断重入处理 为了避免中断重入可能造成的进程调度失败,我们在global.h中定义(在global.c中实现)了一个全局变量k_reenter,并在main.c中的kernel_main函数的末尾处将其初始化为0。 在kernel.asm中定义的save函数中,先k_reenter++,再判断k_reenter=0?若=0则不是重入,调用restart进行进程切换,否则不切换,最后都让k_reenter--。 2.5 进程调度 2.5.1 从内核跳转到用户进程 定义在kernel.asm后部的restart函数用于从内核跳转到下一个进程(参见“08特权级与代码转移.doc”中的2.5.2): ; ============================================================= ; restart ; ============================================================ restart: mov esp, [p_proc_ready] ; ESP -> 当前PCB lldt [esp + P_LDT_SEL] ; 装入当前LDT lea eax, [esp + P_STACKTOP] ; LEA:获取有效偏移地址 mov dword [tss + TSS3_S_SP0], eax ; 设置TSS中ESP0 --> PCB中的栈帧顶 restart_reenter: dec dword [k_reenter] pop gs ; 弹出PCB中EIP前的内容 pop fs pop es pop ds popad add esp, 4 ; 跳过返回地址 iretd ; 利用PCB栈中剩下的EIP、CS、EFLAGS、ESP和SS跳转到用户进程 其中的符号常量在sconst.inc中定义: P_STACKTOP equ SSREG + 4 P_LDT_SEL equ P_STACKTOP P_LDT equ P_LDT_SEL + 4 TSS3_S_SP0 equ 4 前三个分别代表栈帧顶、LDT选择符、LDT在PCB中的偏移量,TSS3_S_SP0则为ESP0在TSS中的偏移量。 2.5.2 三种栈 进程调度需要使用大量栈操作,而且涉及到三种不同的栈:应用程序栈、进程表栈、内核栈。其中的进程表栈(TSS),只是我们为了保存和恢复进程的上下文寄存器值,而临时设置的一个伪局部栈,不是正常的程序栈,由CPU在调用内核中的中断处理程序(从特权级1 --> 特权级0)时自动切换。 代码 数据 ESP 堆栈 进程X 中断发生时,ESP指向进程堆栈的某个位置 中断发生,从ring1跳到ring0 堆栈切换到TSS中预设的SS0和ESP0 PCB X PCB Y ?? 寄存器的值被保存到进程表中 内核中的进程控制表 ESP ESP 内核栈 为防止堆栈操作破坏进程表内容 需要通过设置ESP来切换到内核栈 在时钟中断发生时,CPU会先检查中断处理程序的特权级是否与当前的一致,若不一致,则会从当前的TSS中选择切换至哪个特权级别的堆栈SS和ESP,并将EFLAGS、CS、EIP先后压入该堆栈中,接着跳转到(位于内核中)时钟中断处理程序(clock_handler函数)执行。 为了及时保护中断现场,必须在中断处理函数的最开始处,立即保存被中断程序的所有上下文寄存器中的当前值。这正是我们在硬件中断处理函数hwint00的开始处,调用save函数的理由: %macro hwint_master 1 call save ?? 而save函数也是开始处,压栈保护上下文寄存器: save: pushad ; `. push ds ; | push es ; | 保存原寄存器值 push fs ; | push gs ; / ?? 2.5.3 TSS 在protect.h中定义了TSS结构类型: typedef struct s_tss { u32 backlink; u32 esp0; /* stack pointer to use during interrupt */ u32 ss0; /* \ segment \ \ \ \ */ u32 esp1; u32 ss1; u32 esp2; u32 ss2; u32 cr3; u32 eip; u32 flags; u32 eax; u32 ecx; u32 edx; u32 ebx; u32 esp; u32 ebp; u32 esi; u32 edi; u32 es; u32 cs; u32 ss; u32 ds; u32 fs; u32 gs; u32 ldt; u16 trap; u16 iobase; /* I/O位图基址大于或等于TSS段界限,就表示没有I/O许可位图 */ /*u8 iomap[2];*/ }TSS; 在global.h定义(在global.c中实现)了TSS结构对象: TSS tss; 在protect.c的init_prot函数中:设置tss,并填充GDT中的TSS描述符: /* 填充 GDT 中 TSS 这个描述符 */ memset(&tss, 0, sizeof(tss)); /* 全部置为0 */ tss.ss0 = SELECTOR_KERNEL_DS; /* 设SS0=内核的数据段 */ tss.iobase = sizeof(tss); /* 无I/O许可位图 */ init_descriptor(&gdt[INDEX_TSS], vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss), sizeof(tss) - 1, DA_386TSS); 在kernel.asm的_start函数中装入: mov ax, SELECTOR_TSS ltr ax 其中的符号常量SELECTOR_TSS(=32)在sconst.inc中定义: SELECTOR_TSS equ 0x20 ; TSS从外层跳到内存时SS和ESP的值从里面获得 为TSS描述符在GDT中的偏移量(GDT中特权级0的选择符)。与在protect.h定义的一致: #define SELECTOR_TSS 0x20 // TSS从外层跳到内存时SS和ESP的值从里面获得 2.5.4 时钟中断处理 在kernel.asm中,我们定义了主8259A的中断处理宏hwint_master, 时钟中断的处理程序hwint00就是利用该宏来创建的: ; 中断和异常 -- 硬件中断 ; --------------------------------- %macro hwint_master 1 call save in al, INT_M_CTLMASK ; `. or al, (1 << %1) ; | 屏蔽当前中断 out INT_M_CTLMASK, al ; / mov al, EOI ; `. 置EOI位 out INT_M_CTL, al ; / sti ; CPU在响应中断的过程中会自动关中断,这句之后就允许响应新的中断 push %1 ; `. call [irq_table + 4 * %1] ; | 中断处理程序 pop ecx ; / cli in al, INT_M_CTLMASK ; `. and al, ~(1 << %1) ; | 恢复接受当前中断 out INT_M_CTLMASK, al ; / ret %endmacro ALIGN 16 hwint00: ; Interrupt routine for irq 0 (the clock). hwint_master 0 在这里先调用save函数,再屏蔽当前中断,接着发送中断处理完成的消息EOI,并用CPU指令STI打开中断响应,然后调用中断处理程序,再用CPU指令CLI关闭中断响应,最后允许当前中断。 中断处理程序的调用,是通过其入口地址在中断向量表irq_table中的偏移量来实现的。irq_table在global.c中定义: PUBLIC irq_handler irq_table[NR_IRQ]; 其中的符号常量NR_IRQ=16,在const.h中定义: #define NR_IRQ 16 /* Number of IRQs */ irq_table在i8259.c的init_8259A函数的末尾处被初始化成显示中断号信息串: ?? int i; for (i = 0; i < NR_IRQ; i++) irq_table[i] = spurious_irq; } /*==========================================================* spurious_irq *==========================================================*/ PUBLIC void spurious_irq(int irq) { disp_str(\ disp_int(irq); disp_str(\} 在我们目前的程序中,只有时钟中断的处理程序被重新设置,见上面的2.4.3。 2.5.5 进程切换 定义在kernel.asm后部的save和restart函数用于进程切换。其中的save函数保存处理器的上下文寄存器(PCB的一部分)到当前PCB中,并切换到内核堆栈,最后调用PCB中的返回函数(restart)完成进程切换(从内核 --> 下一进程): ; ============================================================= ; save ; ============================================================= save: pushad ; `. push ds ; | push es ; | 保存原寄存器值 push fs ; | push gs ; / mov dx, ss mov ds, dx mov es, dx mov eax, esp ;eax = PCB起始地址 inc dword [k_reenter] ;k_reenter++; cmp dword [k_reenter], 0 ;if(k_reenter ==0) jne .1 ;{ mov esp, StackTop ; mov esp, StackTop <--切换到内核栈 push restart ; push restart jmp [eax + RETADR - P_STACKBASE]; return; .1: ;} else { 已经在内核栈,不需要再切换 push restart_reenter ; push restart_reenter jmp [eax + RETADR - P_STACKBASE]; return; ;} 其中的符号常量RETADR等是PCB中对应表项“save函数的返回地址”相对栈顶P_STACKBASE的偏移量,在汇编包含文件sconst.inc中定义: P_STACKBASE equ 0 GSREG equ P_STACKBASE FSREG equ GSREG + 4 ESREG equ FSREG + 4 DSREG equ ESREG + 4 EDIREG equ DSREG + 4 ESIREG equ EDIREG + 4 EBPREG equ ESIREG + 4 KERNELESPREG equ EBPREG + 4 EBXREG equ KERNELESPREG+ 4 EDXREG equ EBXREG + 4 ECXREG equ EDXREG + 4 EAXREG equ ECXREG + 4 RETADR equ EAXREG + 4 EIPREG equ RETADR + 4 CSREG equ EIPREG + 4 EFLAGSREG equ CSREG + 4 ESPREG equ EFLAGSREG + 4 SSREG equ ESPREG + 4 P_STACKTOP equ SSREG + 4 P_LDT_SEL equ P_STACKTOP P_LDT equ P_LDT_SEL + 4 TSS3_S_SP0 equ 4 可见这里的RETADR - P_STACKBASE = save函数的返回地址在PCB中的偏移量,而现在的EAX = ESP = 当前PCB的起始地址,所以jmp [eax + RETADR - P_STACKBASE]语句,相当于调用save的返回语句ret。 为什么不直接调用简单的ret,而改用复杂的JMP语句呢?因为ret需要当前栈顶ESP所指向的(由call语句压栈的)EIP值才能返回。但是现在为了使用内核中的调度程序restart,我们已经通过ESP= StackTop,将堆栈从当前进程的PCB切换到了内核,所以原来的返回值已经不在现在的堆栈中了。 在JMP之前的PUSH语句,将restart(函数的起始地址)压栈,是充当内核栈中的新的返回用EIP,供时钟中断处理函数hwint00最后的返回语句来调用。 3 内核程序的过程框图 3.1 初始过程 kernel.asm中的_start函数 ESP --> 内核栈顶 start.c中的cstart函数 显示串\disp_pos = 0 复制loader的GDT到内核 保存GDTR的值 到gdt_ptr 让gdt_ptr指向新GDT 让idt_ptr指向IDT 调用cstart函数 调用init_prot函数 从gdt_ptr装入新GDT 显示串\从idt_ptr装入IDT protect.c中的init_prot函数 装入TSS 调用init_8259A函数 跳转到kernel_main main.c中的kernel_main函数 初始化IDT 设置TSS、填充GDT中的TSS 显示串\填充GDT中的3个LDT 利用任务表初始化进程表 i8259.c中的init_8259A函数 p_proc_ready指向进程表首 初始化8259A 设置时钟中断处理程序 初始化硬件中断向量表 允许时钟中断 kernel.asm中的restart函数 装入当前用户进程的LDT 调用restart函数 进入死循环 置TSS的ESP0-->当前PCB 利用IRETD指令 从内核跳转到当前用户进程 3.2 调度过程 用户进程运行 显示串“X0xn.” 时钟中断发生 CPU通过IDTR寄存器找到当前IDT 再通过中断号定位其中的时钟中断门描述符 从中获得中断处理程序的段选择符和偏移量 因DPL和RPL(=0) < CPL(=1) 从用户进程栈切换到内核中的当前PCB栈 压原用户进程栈的SS和ESP、 EFLAGS、CS和EIP入新栈 kernel.asm中的save函数 将上下文寄存器压入当前PCB 切换到内核栈 调用IDT中的时钟中断处理程序hwint00 kernel.asm中的hwint00函数 应用硬件中断处理宏hwint_master 将restart入口地址压栈 返回到hwint00 调用save函数 clock.c中的clock_handler函数 屏蔽时钟中断 显示字符串\发送中断处理结束消息EOI p_proc_ready指向下一PCB 用STI指令打开中断响应 调用硬件中断向量表irq_table中的 时钟中断处理程序clock_handler kernel.asm中的restart函数 装入当前用户进程的LDT 用CLI指令关闭中断响应 置TSS的ESP0-->当前PCB 允许时钟中断 利用IRETD指令 从内核跳转到当前用户进程 利用RET指令调用restart函数 4 完整源代码 略。 参见我的个人网页中的ps.zip压缩包。 5 编译运行 下面是具体步骤: ? 下载我的个人网页中的ps.zip压缩包,内含FAT12的软盘映像文件a.img。 ? 启动VMware虚拟机及其中的Ubuntu。 ? 在Ubuntu中启动主文件夹。 ? 将ps.zip复制到主文件夹中。 将ps.zip复制到主文件夹 ? 双击主文件夹中的ps.zip图标。 ? 在打开的归档管理器窗口中,双击PS目录。 归档管理器窗口 PS目录 ? 单击归档管理器窗口中的“提取”按钮,在弹出的提取窗口中按右下角的“提取” 按钮。 ? 在弹出的显示“提取成功完成”的信息框中,按“退出”或“关闭”钮关闭它。 ? 压缩包被解压到了主文件夹的ps子目录中。 主文件夹增加了ps子目录 ? 双击ps子目录。 ? ? ? ? 主文件夹的ps子目录 关闭主文件夹,启动终端。 用命令cd ps进入ps子目录。 用命令ls显示ps目录中的文件列表。 用命令cat Makefile显示Make文件(注意文件名是区分大小写的)。 显示Make文件 ? 用命令sudo mkdir /mnt/floppy创建软盘挂载目录。 创建软盘挂载目录 ? 用命令make image编译链接程序、挂载软盘映像、复制装载和内核程序到软盘。 ? ? ? ? make image 关闭终端,启动主文件夹及其ps子目录。 将ps中的a.img文件和ps/boot子目录中的boot.bin文件复制到Windows的C:\\Program Files\\Bochs-2.6\\MyOS目录中。 利用DiskWriter将boot.bin复制到a.img文件。 在Bochs虚拟机的配置文件中,将a.img设置为软盘a,并用a启动Bochs虚拟机。运行结果如下图: 保护模式下的进程调度程序的运行结果 实验14 ? 掌握保护模式下进程调度的方法和步骤。 ? 实现(进程调度)内核程序kernel.bin。 ? 将loader.bin和kernel.bin文件放入FAT12虚拟软盘映像的根目录中,并用此软盘映像 启动虚拟机。 ? (选做)修改本实验中的相关程序,实现从10MB硬盘映像启动和4个进程间的切换。 ? (选做)修改本实验中的程序,实现带优先级的进程调度(如对各个进程使用不同的延 时函数输入值)。 ? (选做)修改内核中的相关程序,控制各个用户进程运行时间片的长短,从而达到实现 优先级的目的。 ? (选做)将各个用户进程写成独立的(ELF格式的)应用程序(需要分别加载),再进 行保护模式下的进程调度。
正在阅读:
09保护模式下的进程调度 - 图文03-31
电子商务1007教案06-21
工地秩序维护组管理方案01-21
高级技术类职称英语考试试题集09-04
人教版化学选修四教材分析08-28
新同学作文450字06-23
2、铁运 69号 关于发布《接触网安全工作规程》和《接触网06-05
花雕醉鸡08-19
- 多层物业服务方案
- (审判实务)习惯法与少数民族地区民间纠纷解决问题(孙 潋)
- 人教版新课标六年级下册语文全册教案
- 词语打卡
- photoshop实习报告
- 钢结构设计原理综合测试2
- 2014年期末练习题
- 高中数学中的逆向思维解题方法探讨
- 名师原创 全国通用2014-2015学年高二寒假作业 政治(一)Word版
- 北航《建筑结构检测鉴定与加固》在线作业三
- XX县卫生监督所工程建设项目可行性研究报告
- 小学四年级观察作文经典评语
- 浅谈110KV变电站电气一次设计-程泉焱(1)
- 安全员考试题库
- 国家电网公司变电运维管理规定(试行)
- 义务教育课程标准稿征求意见提纲
- 教学秘书面试技巧
- 钢结构工程施工组织设计
- 水利工程概论论文
- 09届九年级数学第四次模拟试卷
- 调度
- 进程
- 模式
- 保护
- 图文
- 转作风敢担当善作为抓落实心得体会
- 微波测量系统调试与频率
- 矿用调度绞车设计II
- 职工公寓规划设计说明docx
- 2015资助政策网络竞赛学习题库(部分)
- 质量管理知识题库(313)
- 一道竞赛题的解题心路历程
- 高中地理《地球的运动》教案4 湘教版必修1
- 第一批不需要申请《医疗器械经营企业许可证》的第II类医疗器械7
- 化工原理第四版第一章课后习题答案
- 功能高分子材料重点整理
- 九年级历史下册第二单元第8课正义的胜利教案北师大版
- 渔业经济学习题集
- 公路路产损失赔偿费 云交财(1994)352号
- 水文地质与工程地质考试复习要点及练习题和答案
- 《苹果里的五角星》教学反思
- 《普通物理2》期末复习课(11数本,6.17)(1)
- 课堂生成让教学变得如此美丽
- GreenPlum安装笔记 - 图文
- 教师相关论述