1.进程
我们来盘点一下,完成进程切换需要哪些数据结构和程序模块:1)首先,一个进程必须有代码、数据(和堆栈):相关数据有LDT、段描述符、TSS等
2)对于正在休息的进程,我们需要让它重新醒来的时候记得挂起之前的状态,从而让原来的任务能够继续执行——所以我们需要保存程序状态,这就是PCB(程序控制块)
3)进程的切换者,是操作系统的进程调度模块
4)时钟中断处理程序,帮助我们完成进程切换
2.与进程切换相关的汇编指令
pushad:因为进程切换需要保存进程上下文,使用push来保存每个寄存器比较麻烦,于是,intel提供了一条指令pushad来保存所有通用寄存器的值IRET 与 IRETD 是相同操作码的助记符。IRETD 助记符(中断返回双字)用于从使用 32 位操作数大小的中断返回;不过大多数汇编器对这两种操作数大小都互换使用 IRET 助记符。
3.进程调度大致过程
PCB:PCB是用来描述进程的,它独立在进程之外,当我们将上下文压入PCB之时,已经处在进程管理模块中了。ESP指向:在进程调度模块中,将会用到堆栈,而寄存器被压栈到进程表之后,esp指向PCB的某个位置——接下来的堆栈操作将破坏PCB。为了解决上述问题,需要将esp指向专门的内核栈。所以,在进程切换的过程中ESP的指向有三次:进程堆栈——PCB——内核栈。
特权级变换:从外层到内层次,从TSS中取得SS:ESP;初始化的时候,从ring0>>>ring1,这个和恢复进程执行有点像,我们需要完成上下文的初始化,然后使用iretd指令来完成转移。
恢复:首先,我们需要从PCB中恢复寄存器的值,然后指令iretd,设置cs:ip和eflags,这样程序就回到了进程B。4.初始化——ring0>>>ring1
为了对从内核到进程的转化有一个感性的认识,我们来看一下转化时刻的代码:
chapter6/i/kernel/kernel.asm ; ==================================================================================== ; restart ; ==================================================================================== restart: mov esp, [p_proc_ready] ;esp 指向LPCB,在进程运行的时候就已经准备好了 lldt [esp + P_LDT_SEL] lea eax, [esp + P_STACKTOP];eax=esp+P_stacktop mov dword [tss + TSS3_S_SP0], eax ;这样以后,地址tss + TSS3_S_SP 处,存放的是ss0的地址 restart_reenter: dec dword [k_reenter] pop gs pop fs pop es pop ds popad add esp, 4 iretd
那么这一部分从哪里跳转过来的呢? /kernel/mai.c/kernel_main()>>restart()
分析一下代码执行流程:
1)p_proc_ready,是一个PCB指针,指向下一个将要执行的PCB;
2)好了,这里,我们去查看一下PCB的定义和P_LDT_SEL的定义:
PCB的定义:
31 typedef struct s_proc { 32 STACK_FRAME regs; /* process registers saved in stack frame */ 33 34 u16 ldt_sel; /* gdt selector giving ldt base and limit */ 35 DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */ 36 37 int ticks; /* remained ticks */ 38 int priority; 39 40 u32 pid; /* process id passed in from MM */ 41 char p_name[16]; /* name of the process */ 42 }PROCESS;P_LDT_SEL表示的是ldt_sel的索引,来看看PCB总相关变量的一些列定义:
8 P_STACKBASE equ 0 9 GSREG equ P_STACKBASE 10 FSREG equ GSREG + 4 11 ESREG equ FSREG + 4 12 DSREG equ ESREG + 4 13 EDIREG equ DSREG + 4 14 ESIREG equ EDIREG + 4 15 EBPREG equ ESIREG + 4 16 KERNELESPREG equ EBPREG + 4 17 EBXREG equ KERNELESPREG + 4 18 EDXREG equ EBXREG + 4 19 ECXREG equ EDXREG + 4 20 EAXREG equ ECXREG + 4 21 RETADR equ EAXREG + 4 22 EIPREG equ RETADR + 4 23 CSREG equ EIPREG + 4 24 EFLAGSREG equ CSREG + 4 25 ESPREG equ EFLAGSREG + 4 26 SSREG equ ESPREG + 4 27 P_STACKTOP equ SSREG + 4 28 P_LDT_SEL equ P_STACKTOP 29 P_LDT equ P_LDT_SEL + 4 30 31 TSS3_S_SP0 equ 4
好了,看到这里,或许你已经明白了PCB结构,如下:
35 typedef struct s_tss { 36 u32 backlink; 37 u32 esp0; /* stack pointer to use during interrupt */ 38 u32 ss0; /* " segment " " " " */ .... .... }TSS
3)总结一下,前两句是设置LDT;接着两句是设置tss的sp0
我们来看看调用代码的上下文:
73 /* 初始化 8253 PIT */ 74 out_byte(TIMER_MODE, RATE_GENERATOR); 75 out_byte(TIMER0, (u8) (TIMER_FREQ/HZ) ); 76 out_byte(TIMER0, (u8) ((TIMER_FREQ/HZ) >> 8)); 77 78 put_irq_handler(CLOCK_IRQ, clock_handler); /* 设定时钟中断处理程序 */ 79 enable_irq(CLOCK_IRQ); /* 让8259A可以接收时钟中断 */ 80 81 restart(); 82 83 while(1){}
结合一下进程表的开始结构图,我们得出如下结论:
下一次中断发生时候,先pop sregs,然后在pop regs,接着跳过retaddr,然后执行iretd。下次中断发生的时候,需要完成的工作就是:恢复各个寄存器的值、TSS中ss0和设置ldtr。
4.1时钟中断处理程序
时钟中断只是为了完成进程切换,我们这里不使用复杂的调度,仅仅完成ring0>>ring1,所以使用iret即可。150 ALIGN 16 151 hwint00: ; Interrupt routine for irq 0 (the clock). 152 iretd
4.2PCB、进程体、GDT和TSS
对于PCB的初始化,我们仅仅需要设置sregs、eip、esp和eflags。另外,cs和ds此时对应的是LDT,所以需要初始化LDT。另外,我们还需要初始化TSS中ss0和esp0。好了,我们来看一下进程表、PCB、GDT、TSS他们之间的数据关系:(见上图)
接下来,我们来做这四个部分的初始化工作:
是一个函数,不停打印字母A:chapter6/a/kernel/main.c
51 void TestA() 52 { 53 int i = 0; 54 while(1){ 55 disp_str("A"); 56 disp_int(i++); 57 disp_str("."); 58 delay(1); 59 } 60 }思考一下,TestA仅仅是一个进程,而且是被中断调度的对象,显然不是内核的一部分。怎么将控制权转移到进程呢?在前面的章节中,kernel_main是内核函数,跳转过程:
kernel.asm中有一条jmp kernel_main指令;kernel_main是main.c中的一个函数,kernel.main最后一句是while(1){},所以内核将进入等待模式,将会相应中断处理模块和进程调度模块的请求。
2)进程表
根据上图进程表示意图,我们不难定义PCB的相关结构:
9 typedef struct s_stackframe { 10 u32 gs; /* \ */ 11 u32 fs; /* | */ 12 u32 es; /* | */ 13 u32 ds; /* | */ 14 u32 edi; /* | */ 15 u32 esi; /* | pushed by save() */ 16 u32 ebp; /* | */ 17 u32 kernel_esp; /* <- 'popad' will ignore it */ 18 u32 ebx; /* | */ 19 u32 edx; /* | */ 20 u32 ecx; /* | */ 21 u32 eax; /* / */ 22 u32 retaddr; /* return addr for kernel.asm::save() */ 23 u32 eip; /* \ */ 24 u32 cs; /* | */ 25 u32 eflags; /* | pushed by CPU during interrupt */ 26 u32 esp; /* | */ 27 u32 ss; /* / */ 28 }STACK_FRAME; 29 30 31 typedef struct s_proc { 32 STACK_FRAME regs; /* process registers saved in stack frame */ 33 34 u16 ldt_sel; /* gdt selector giving ldt base and limit */ 35 DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */ 36 u32 pid; /* process id passed in from MM */ 37 char p_name[16]; /* name of the process */ 38 }PROCESS;知道了数据结构,再来看看它的初始化a/kernel/main.c
26 p_proc->ldt_sel = SELECTOR_LDT_FIRST; 27 memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS>>3], sizeof(DESCRIPTOR)); 28 p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5; // change the DPL 29 memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS>>3], sizeof(DESCRIPTOR)); 30 p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5; // change the DPL 31 32 p_proc->regs.cs = (0 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; 33 p_proc->regs.ds = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; 34 p_proc->regs.es = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; 35 p_proc->regs.fs = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; 36 p_proc->regs.ss = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; 37 p_proc->regs.gs = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK; 38 p_proc->regs.eip= (u32)TestA; 39 p_proc->regs.esp= (u32) task_stack + STACK_SIZE_TOTAL; 40 p_proc->regs.eflags = 0x1202; // IF=1, IOPL=1, bit 2 is always 1. 41 42 p_proc_ready = proc_table; 43 restart();其中,上面用到的宏定义在protect.h之中,参考:a/include/protect.h
67 #define INDEX_DUMMY 0 /* \ */ 68 #define INDEX_FLAT_C 1 /* | LOADER 里面已经确定了的 */ 69 #define INDEX_FLAT_RW 2 /* | */ 70 #define INDEX_VIDEO 3 /* / */ 71 #define INDEX_TSS 4 72 #define INDEX_LDT_FIRST 5 73 /* 选择子 */ 74 #define SELECTOR_DUMMY 0 /* \ */ 75 #define SELECTOR_FLAT_C 0x08 /* | LOADER 里面已经确定了的 */ 76 #define SELECTOR_FLAT_RW 0x10 /* | */ 77 #define SELECTOR_VIDEO (0x18+3)/* /<-- RPL=3 */ 78 #define SELECTOR_TSS 0x20 /* TSS */ 79 #define SELECTOR_LDT_FIRST 0x28 80 81 #define SELECTOR_KERNEL_CS SELECTOR_FLAT_C 82 #define SELECTOR_KERNEL_DS SELECTOR_FLAT_RW 83 #define SELECTOR_KERNEL_GS SELECTOR_VIDEO 84 85 /* 每个任务有一个单独的 LDT, 每个 LDT 中的描述符个数: */ 86 #define LDT_SIZE 2 87 88 /* 选择子类型值说明 */ 89 /* 其中, SA_ : Selector Attribute */ 90 #define SA_RPL_MASK 0xFFFC 91 #define SA_RPL0 0 92 #define SA_RPL1 1 93 #define SA_RPL2 2 94 #define SA_RPL3 3 95 96 #define SA_TI_MASK 0xFFFB 97 #define SA_TIG 0 98 #define SA_TIL 4填充GDT中进程LDT的描述符:a/kernel/protect.c
109 init_descriptor(&gdt[INDEX_LDT_FIRST], 110 vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[0].ldts), 111 LDT_SIZE * sizeof(DESCRIPTOR) - 1, 112 DA_LDT);这个函数的实现:
149 PRIVATE void init_descriptor(DESCRIPTOR *p_desc,u32 base,u32 limit,u16 attribute) 150 { 151 p_desc->limit_low = limit & 0x0FFFF; 152 p_desc->base_low = base & 0x0FFFF; 153 p_desc->base_mid = (base >> 16) & 0x0FF; 154 p_desc->attr1 = attribute & 0xFF; 155 p_desc->limit_high_attr2= ((limit>>16) & 0x0F) | (attribute>>8) & 0xF0; 156 p_desc->base_high = (base >> 24) & 0x0FF; 157 }3)准备GDT和TSS
现在,剩下的就是TSS的初始化和对应描述符在GDT中的填充了:
初始化TSS:
99 /* 填充 GDT 中 TSS 这个描述符 */ 100 memset(&tss, 0, sizeof(tss)); 101 tss.ss0 = SELECTOR_KERNEL_DS; 102 init_descriptor(&gdt[INDEX_TSS], 103 vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss), 104 sizeof(tss) - 1, 105 DA_386TSS); 106 tss.iobase = sizeof(tss); /* 没有I/O许可位图 */ 下面,填写tr: 130 xor eax, eax 131 mov ax, SELECTOR_TSS 132 ltr ax4.3iretd
这里,我们先使用一个简单的restart函数:
294 restart: 295 mov esp, [p_proc_ready] 296 lldt [esp + P_LDT_SEL] 297 lea eax, [esp + P_STACKTOP] 298 mov dword [tss + TSS3_S_SP0], eax 299 300 pop gs 301 pop fs 302 pop es 303 pop ds 304 popad 305 306 add esp, 4 307 308 iretd
好了,使用iretd将加载CS:IP,想一想,CS和IP的值是多少?注意,编译以后的main.c中Test函数是位于32b代码段中,这个我们需要用反汇编研究一下。
4.4进程启动与回顾
让我们来回想一下第一个进程的启动过程:初始化进程:testA;初始化GDT中的TSS和LDT的两个字符,初始化TSS(在init_prot()之中);准备进程表(在kernel.main());完成跳转(kernel.asm)
不过,我们现在仅仅完成了从内核到用户进程;但是如何完成进程切换,显然,我们需要打开时钟中断和设置8259A的EOI位。
总结一下:
kernel的工作流程:
kernel.asm:
_start:内核入口,顺序往下执行
cstart():将loader中的GDT复制到内核、设置gdt和ldt,初始化中断向量表
init_prot():初始化8259A,初始化各个中断门
设置TR
main.c/tinix_main():
设置PCB信息
restart():lldt、ss0、恢复段寄存器和通用寄存器、进入ring1(iretd),执行TestA——无限循环
while(1)
这里,我们来介绍一个技巧:如何调试系统内核?
我们原来的调试,都是在汇编程序的状态,如何按照C语言的行级别来调试内核呢?这里,我们挖一个坑,以后再回填这个地方。