OS-Lab3
Lab3实验报告
思考题
Thinking 3.1 请结合 MOS 中的页目录自映射应用解释代码中 e->env_pgdir[PDX(UVPT)]
= PADDR(e->env_pgdir) | PTE_V 的含义。
把页目录这4MB映射到[UVPT,ULIM)这一块用户区虚拟地址空间,使页表映射用户空间可见。
- VPT = Virtual Page Table
- User VPT = 给用户进程看的页表映射
Thinking 3.2 elf_load_seg 以函数指针的形式,接受外部自定义的回调函数 map_page。请你找到与之相关的 data 这一参数在此处的来源,并思考它的作用。没有这个参数可不可以?为什么?
data 是回调函数的外部环境。
没有这个参数可以,但是会使通用性变差,耦合更强,可扩展性变差。data 参数的本质,就是为了让 elf_load_seg 和具体 OS 的页映射逻辑解耦。指导书也明确强调了这种设计:elf_load_seg 只关心 ELF 段结构,不处理与具体操作系统相关的页面加载过程。
Thinking 3.3 结合 elf_load_seg 的参数和实现,考虑该函数需要处理哪些页面加载的情况。
- 段起始地址不是页对齐。
比如 va 落在某一页的中间,那第一页不是从页首开始写,而是从页内 offset 处开始装数据。指导书专门说了它会正确处理这些页面偏移。 - 文件数据只覆盖部分页面。
也就是 .text / .data 那部分真实在 ELF 文件里存在的数据,可能:只占一页的一部分,横跨多页,最后一页只写一部分。
这时前几页可能是“整页拷贝”,最后一页可能是“半页拷贝”。 - sg_size > bin_size,也就是带 .bss。
这时段在内存里比在文件里更大,后面多出来的部分就是 .bss,应当补 0。指导书明确写了:如果开了新页面但没填满,比如 .bss 区域,余下部分用 0 填充。 - 纯 .bss 页。
有些页根本没有任何文件数据,只是因为 sg_size 更大才需要在内存里存在。这种页也要分配出来,并初始化为 0。
Thinking 3.4 思考上面这一段话,并根据自己在 Lab2 中的理解,回答: 你认为这里的 env_tf.cp0_epc 存储的是物理地址还是虚拟地址?
虚拟地址。
- load_icode 把用户程序装入用户地址空间
- 程序入口是 ehdr->e_entry
- 这个入口要写进 env_tf.cp0_epc
- 进程恢复时,CPU 用这个值作为 PC 开始执行
- 而用户程序运行时 PC 持有的是虚拟地址
这题可以按“向量组里挂的是谁 → 这个符号真正定义在哪 → 最后又调到哪”来找。
指导书先给了异常向量组 exception_handlers:
0 号是 handle_int,1 号是 handle_mod,2/3 号都是 handle_tlb。
结合 Lab3 的代码组织,通常可以这样定位:
Thinking 3.5 试找出 0、1、2、3 号异常处理函数的具体实现位置。8 号异常(系统调用)涉及的 do_syscall() 函数将在 Lab4 中实现。
- 0 号异常:
handle_int,在kern/genex.S中实现,之后进入中断处理/调度流程。 - 1 号异常:
handle_mod,在kern/genex.S中实现。 - 2 号异常:
handle_tlb,在kern/genex.S中实现,后续调用kern/traps.c中的do_tlb_refill。 - 3 号异常:
handle_tlb,和 2 号一样,在kern/genex.S中实现,后续也进入kern/traps.c中的do_tlb_refill。
Thinking 3.6 阅读 entry.S、genex.S 和 env_asm.S 这几个文件,并尝试说出时钟中断在哪些时候开启,在哪些时候关闭。
- 开启时机:
- 在
env_pop_tf恢复一个进程运行之前,先重设计时器,再恢复Status,最后eret返回;这时用户进程开始在“可被时钟中断打断”的状态下运行。 env_tf.cp0_status里预先设置了STATUS_IM7 | STATUS_IE | STATUS_EXL | STATUS_UM,而RESTORE_ALL会把它写回CP0_STATUS。
- 在
- 关闭时机:
- 一旦异常/中断发生,先进入
entry.S的异常入口,SAVE_ALL保存现场; - 随后
entry.S把STATUS_UM | STATUS_EXL | STATUS_IE清掉,其中清IE是全局关中断,清UM/EXL是为了让处理器稳定留在内核态并允许异常处理流程继续。
- 一旦异常/中断发生,先进入
用户进程运行前env_tf.cp0_status 里预先写好 IE=1, IM7=1
↓
调度切换时env_pop_tf 里 RESET_KCLOCK,RESTORE_ALL 恢复 Status
↓
执行 eret
硬件把 EXL 清 0,真正开启时钟中断响应
↓
用户进程运行中
时钟中断可被响应
↓
一旦中断发生
硬件自动把 EXL 置 1,进入异常态,相当于关闭中断响应
↓
异常处理 / 调度 / 切换完成后
再次 eret,重新开启
Thinking 3.7 阅读相关代码,思考操作系统是怎么根据时钟中断切换进程的。
用户进程运行
↓
时钟中断发生
↓
进入异常入口
↓
异常分发程序根据 ExcCode 查到 0 号异常
↓
调用 handle_int
↓
判断是否真的是 7 号中断(时钟中断)
↓
调用 schedule
↓
选出下一个可运行进程
↓
调用 env_run
↓
保存当前进程现场,切换到新进程
↓env_pop_tf
↓ret_from_exception
↓eret 返回用户态
↓
新进程开始运行
难点分析
Lab3 的主题是 进程与异常。这一实验主要完成两件事:
一是创建一个进程并成功运行,二是实现时钟中断,使内核能够重新获得 CPU 控制权并完成进程切换。
相比 Lab2,我认为Lab3 的难点不在某一个单独函数特别复杂,而在于它第一次把 进程创建、地址空间初始化、ELF 装载、异常处理、时钟中断、调度切换 这些模块连成了一条完整执行链。指导书在概况图中也明确给出了这条主线:env_init -> env_create -> env_alloc -> env_setup_vm -> load_icode -> elf_load_seg -> load_icode_mapper -> user process -> handle_int -> schedule -> env_run。
1. 进程创建与管理
Lab2 的重点是页表、TLB 和地址映射;Lab3 则要求在此基础上进一步理解:
- 一个进程不仅仅是一段代码
- 它还需要有自己的 PCB(
Env) - 有自己的地址空间
- 有自己的寄存器现场
- 能被内核保存、恢复和调度
Lab3 前半部分最核心的主线是:1
2
3
4
5
6env_create
→ env_alloc
→ env_setup_vm
→ load_icode
→ elf_load_seg
→ load_icode_mapper
env_alloc:分配并初始化进程控制块,填写env_id、env_asid、env_parent_id,并设置初始cp0_status和用户栈位置。env_setup_vm:初始化进程地址空间,为新进程分配页目录、复制高地址模板映射、建立UVPT自映射。是在搭地址空间的骨架。load_icode:把 ELF 用户程序真正加载到该进程的地址空间中elf_load_seg:按段、按页处理程序映像load_icode_mapper:作为回调函数,负责把某一页真正分配、映射并写入数据。是在往骨架里面逐页装程序内容。
2. ELF 装载流程
load_icode -> elf_load_seg -> load_icode_mapper 这一段,也是 Lab3 很典型的难点。
指导书在概况图里明确表明:load_icode 负责程序加载,elf_load_seg 负责段级处理,load_icode_mapper 则进一步调用 page_alloc & page_insert 完成真正的内存映射。
这一部分容易混乱的原因在于它使用了“回调函数”设计:
elf_load_seg并不直接操作页表- 它只负责按 ELF 规则遍历段、处理页对齐、处理
.bss - 真正“对某一页做分配和映射”的,是
load_icode_mapper
并且值得注意的是:
- 一定要理解为何
elf_load_seg要设计成通用加载器,而不是直接耦合操作系统页表逻辑,这一点在思考题中也有体现。
3. 进程的运行
进程运行过程:1
env_run→ env_pop_tf→ ret_from_exception→ eret
指导书在 env_run 的说明中总结得很清楚:
- 保存当前进程的上下文信息
- 切换
curenv为即将运行的进程 - 设置
cur_pgdir - 调用
env_pop_tf恢复现场并异常返回。
这里的难点在于要区分:
load_icode:只是把程序内容装进去,解决“进程里面有什么”。env_pop_tf:才是把 CPU 的寄存器现场切到这个进程上,解决“CPU 现在为谁服务”。
4. 异常处理
Lab3 的后半部分进入 中断与异常。
指导书在概况图中指出:
- 内核初始化完毕后会陷入死循环
- 等待第一次时钟中断来临
- 通过异常处理来调度已经创建好的用户进程运行
- 用户进程运行过程中,又会遇到
TLB Load Miss、TLB Store Miss和Timer Interrupt - 在时钟中断发生后,操作系统实现了进程切换。
时钟中断
作用是:定期把 CPU 从用户进程手里抢回来。
调度(schedule)
作用是:决定接下来该谁运行。
指导书在 schedule 的提示中强调:
count记录当前进程剩余执行次数- 调度队列中包含所有就绪进程
schedule、env_run、env_pop_tf、ret_from_exception都是不返回函数。
上下文切换(env_run / env_pop_tf)
作用是:真正把 CPU 的执行现场切过去。
三者的关系
1 | Timer Interrupt |
5. 整体理解
单独看每一块都能理解,但连起来就容易懵,不理解为什么要这么做。
实际上,这是因为lab3第一次形成了操作系统里最经典的一条闭环:
id 1
2
3
4
5
6
7
8
创建进程
→ 装载程序
→ 运行用户进程
→ 时钟中断发生
→ 进入异常处理
→ 调度器选下一个进程
→ 恢复现场
→ 新进程继续运行
1 | 创建进程 |
实验体会
- lab3的代码补全感觉很简单,提示实在是很清晰,所以没费什么功夫就完成了。但是分开来看都能理解,一合起来看整体流程还是蒙的,感觉在脑子里建立一个完整的流程链蛮困难的。
lab3-pre-exam也是很好理解,但是有一点,不能太依赖hint了,可能有的一些维护的小点hint里没有给,但是自己也要写,比如维护runtime_left。- 这次周二就把
lab3弄完了,希望能多一点时间理解巩固,lab3-exam加油!lab3-extra加油! lab3-exam课上45min通过,卡了一下一些细节的地方(很多因为代码段是重复的所以直接从原函数或者lab3-pre-exam复制过来了,导致一些变量名啊函数名啊什么的没有改过来,总体来说还是顺利的)。lab3-extra极限21:59:14通过,果然得战斗到最后一刻。先是程序运行超时(卡在pmap.c init了),这个我以为和课下一样是变量类型设置的不规范的问题,但是检查了好几遍都没发现。然后结合提示,调整了一下cp0-epc自加和printk的顺序,把地址自增挑出来放在case外面,先printk再自增就解决超时问题,但是本地测试仍未通过。读题发现score取错了,修正,通过本地,交上去40分,此时距离考试结束还有12min。又仔细读了一边题,发现index要判断是否大于等于数组大小,是才更改imm,修正交上去,仍是40分,现在已经21:56了。又逐行看了一下code=0x06部分的代码,发现新的instr里应该|的是new_imm而不是length,修正,等待冷却,21:59:14冷却结束,提交100分通过。惊心动魄。那么,五一快乐!
原创说明
参考了往届三位学长学姐的博客。感谢他们的精心整理和付出。
https://hyggge.github.io
https://yanna-zy.github.io
https://demiurge-zby.github.io

