BUAA-OS-Lab3 进程与异常
指导书梳理
相关文件:/include/env.h
/include/trap.h
/kern/env.c
lib/elfloader.c
pmap.h
pmap.c
kern/traps.c
kern/genex.S
kern/sched.c
进程
模板页表(MOS专有概念)
在用户空间中读取内核信息
原理
MOS 被设计成对于其中运行的每一个用户进程,都可以通过用户地址空间(kuseg)读取 pages 数组和 envs 数组的信息。为实现该功能,在创建用户进程时我们需要将 pages 数组和 envs 数组映射到用户地址空间中的
UPAGES
与UENVS
处。base_pgdir
指向模板页表页目录的内核虚拟基地址,以便创建进程时能够根据 “模板页表” 的内容创建自己的页表。每创建一个进程,都将这个模板页表页目录中用来映射 envs 与 pages 的表项复制到新创建的进程的页目录中设置目的
使用户进程共享一部分二级页表,从而节省物理页面
进程的标识
struct Env
进程控制块中
env_id
是每个进程独一无二的标识符,进程创建的时候就使用mkenvid
赋予。env_asid
记录进程的 ASID,这是进程虚拟地址空间的标识- ASID在TLB映射机制中使用——TLB 事实上构建了一个映射 < VPN, ASID >$\stackrel{TLB}{\longrightarrow}$< PFN, N, D, V, G >
- ASID 部分只占据了 0-7 共 8 个 bit,即ASID资源是有限的,需要使用一定的资源管理方法来分配、回收 ASID。MOS 实验采用了位图法管理 256 个可用的 ASID,如果 ASID 耗尽时仍要创建进程,内核会发生崩溃(panic)
设置进程控制块
在 MOS 操作系统特意将一些内核的数据暴露到用户空间,使得进程不需要切换到内核态就能访问,这是 MOS 特有的设计
在这里暴露 UTOP 往上到 UVPT 之间所有进程共享的只读空间,也就是把这部分内存对应的内核
页表 base_pgdir 拷贝到进程页表中。从 UVPT 往上到 ULIM 之间则是进程自己的页表
加载二进制镜像
要想正确加载一个 ELF 文件到内存,只需将 ELF 文件中所有需要加载的程序段加载到对应的虚拟地址上即可
elf_load_seg
只关心ELF 段的结构,由回调函数处理具体的页面加载过程
进程运行与切换
在 Lab3 中,进程切换只需要保存进程的上下文信息。而MOS中的寄存器状态保存的地方是 KSTACKTOP
以下的一个 sizeof(TrapFrame)
大小的区域中,而curenv->env_tf
是存放当前进程的上下文的区域
故保存进程上下文使用语句curenv->env_tf = *((struct Trapframe *)KSTACKTOP - 1)
中断与异常
CPU 不仅仅有常见的 32 个通用寄存器,还有功能广泛的协处理器,而中断/异常部分就用到了其中的协处理器 CP0
寄存器助记符 | CP0 寄存器编号 | 描述 |
---|---|---|
Status | 12 | 状态寄存器,包括中断引脚使能,其他 CPU 模式等位域 |
Cause | 13 | 记录导致异常的原因 |
EPC | 14 | 异常结束后程序恢复执行的位置 |
Status 寄存器
IE
位表示中断是否开启,为 1 表示开启,否则不开启当且仅当
EXL
被设置为 0 且UM
被设置为 1 时,处理器处于用户模式,其它所有情况下,处理器均处于内核模式下每当异常发生的时候,EXL 会被自动设置为 1(使处于内核态,能使用特权指令)
每个进程在每一次被调度时都会执行
1
2RESTORE_ALL //恢复处理器寄存器状态
eret // EXL被自动设置为 0
15-8 位为中断屏蔽位,每一位代表一个不同的中断活动,其中 15-10 位使能硬件中断源(该中断能否被响应),9-8位是 Cause 寄存器软件可写的中断位
Cause 寄存器
IP
(15-8 位)保存着哪一些中断发生了,其中 15-10 位来自硬件,9-8 位可以由软件写入,当 Status 寄存器中相同位允许中断(为 1)时,Cause 寄存器这一位也为1就会导致中断ExcCode
(6-2位),记录发生了什么异常。在 MOS 中,中断是 0 号异常。
CPU异常处理流程
CPU 如何处理异常?
- 设置 EPC 寄存器的值为从异常返回的地址。
- 设置 Status 寄存器(设置 EXL 位,强制 CPU 进入内核态并禁止中断)
- 设置 Cause 寄存器(记录异常原因)
- 设置 PC 为异常入口地址,随后交给软件处理
至此,我们成功地切换到内核程序,将异常处理的任务转交给操作系统。
相关指令
mfc0
Move From Coprocessor 0用于从协处理器的某个寄存器中读取值到一个通用寄存器中
mtc0
Move To Coprocessor 0用于将一个通用寄存器的值写入协处理器的某个寄存器中
1
2
3mfc0 t0, CP0_STATUS
and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
mtc0 t0, CP0_STATUS
在 SAVE_ALL 中进行判断,若 Status 寄存器的 UM 位为 0,说明此次异常在内核态触发, sp 寄存器已经在内核异常栈中。不再将 sp 设置为 KSTACKTOP ,而是使其继续增长。
这样便能够在异常中处理新的异常,而不会破坏原本的异常处理流程。这一机制被称作异常重入
异常的分发
当发生异常时,处理器会进入一个用于分发异常的程序,这个程序的作用就是检测发生了哪种异常,并调用相应的异常处理程序
一般来说,异常分发程序会被要求放在固定的某个物理地址上
在MOS中的实现为:将.text.exc_gen_entry 段和 .text.tlb_miss_entry 段用链接器放到特定的位置—— 0x80000180 和 0x80000000 处,它们是异常处理程序的入口地址。在我们的系统中,CPU 发生异常(除了用户态地址的 TLB Miss 异常)后,就会自动跳转到地址 0x80000180 处;发生用户态地址的 TLB Miss 异常时,会自动跳转到地址 0x80000000处
时钟中断
中断处理的流程
通过异常分发,判断出当前异常为中断异常,随后进入相应的中断处理程序。在 MOS 中即对应 handle_int 函数。
此前以异常的角度对时钟中断进行处理;现在以中断的角度对时钟中断进行处理
在中断处理程序中进一步判断 Cause 寄存器中是由几号中断位引发的中断,然后进入不同中断对应的中断服务函数。
中断处理完成,通过 ret_from_exception 函数恢复现场,继续执行。
在MOS 中,时间片的长度是用时钟中断衡量的。4KC 中的 CP0 内置了一个可产生中断的 Timer,MOS 即使用这个内置的 Timer 产生时钟中断
(具体细节实现见指导书)
内核初始化完毕后陷入死循环,等待第一次时钟中断来临,通过异常处理来调度已经创建好的用户进程运行
进程调度器
什么时候需要切换进程?
- 参数 yield 为真时:此时当前进程必须让出。
- count 减为 0 时:此时分给进程的时间片被用完,将执行权让给其他进程。
- 无当前进程:内核必然刚刚完成初始化,需要分配一个进程执行。
- 进程状态不是可运行:当前进程不能再继续执行,让给其他进程。
如何切换进程?
- 当前进程仍为就绪状态时,需要将其移到 env_sched_list 队列的尾部。
- 选中 env_sched_list 队列头部的进程。如果没有可用的进程,内核 panic。
- 设置 count 为当前进程的优先级(分配的时间片的数量)。
(不管是否切换,都)最后将 count 自减 1,调用 env_run 函数。
时纪
E 3.1
注意链表顺序!!初始化是倒序插入
LIST、TAIL相关宏操作的(参数)一般都是指针!!
如下定义
1
static Pde *base_pgdir;
base_pgdir
是一个Pde类型的指针,即为目录项的虚拟地址;*base_pgdir
是Pde类型的指针的解引用,即Pde,为目录项的物理地址
E 3.4
NENV
什么时候要用来作为限制条件吗?这里好像不用
E 3.5
- 分配页面后记得加
p→pp_ref
!!
E 3.12
- schedule里的count是静态变量,存放在.data区,只初始化一次,不同进程不共享!!所以虽然可能会调度多次schedule函数,但每个进程只执行一次
static int count = 0;
初始化语句!! - 注意,不管要不要插到尾部,都要先移除当前进程!!
exam前准备
有可能出问题
- scheduled函数,要不要把不是就绪状态的移除
- 获取队列的第一个进程时,记得LIST_REMOVE
extra
近年都是处理新的异常