09保护模式下的进程调度 - 图文

更新时间:2024-03-31 14:21:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

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>3], vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[i].ldt), LDT_SIZE * sizeof(DESCRIPTOR) - 1, DA_LDT); p_proc++; selector_ldt += 8; }

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格式的)应用程序(需要分别加载),再进

行保护模式下的进程调度。

本文来源:https://www.bwwdw.com/article/821r.html

Top