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 的参数和实现,考虑该函数需要处理哪些页面加载的情况。

  1. 段起始地址不是页对齐。
    比如 va 落在某一页的中间,那第一页不是从页首开始写,而是从页内 offset 处开始装数据。指导书专门说了它会正确处理这些页面偏移。
  2. 文件数据只覆盖部分页面。
    也就是 .text / .data 那部分真实在 ELF 文件里存在的数据,可能:只占一页的一部分,横跨多页,最后一页只写一部分。
    这时前几页可能是“整页拷贝”,最后一页可能是“半页拷贝”。
  3. sg_size > bin_size,也就是带 .bss。
    这时段在内存里比在文件里更大,后面多出来的部分就是 .bss,应当补 0。指导书明确写了:如果开了新页面但没填满,比如 .bss 区域,余下部分用 0 填充。
  4. 纯 .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.SSTATUS_UM | STATUS_EXL | STATUS_IE 清掉,其中清 IE全局关中断,清 UM/EXL 是为了让处理器稳定留在内核态并允许异常处理流程继续

用户进程运行前
env_tf.cp0_status预先写好 IE=1, IM7=1

调度切换时
env_pop_tfRESET_KCLOCKRESTORE_ALL 恢复 Status

执行 eret
硬件把 EXL0,真正开启时钟中断响应

用户进程运行中
时钟中断可被响应

一旦中断发生
硬件自动把 EXL1,进入异常态,相当于关闭中断响应

异常处理 / 调度 / 切换完成后
再次 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 前半部分最核心的主线是:

id
1
2
3
4
5
6
env_create
→ env_alloc
→ env_setup_vm
→ load_icode
→ elf_load_seg
→ load_icode_mapper

  • env_alloc:分配并初始化进程控制块,填写 env_idenv_asidenv_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. 进程的运行

进程运行过程:

id
1
env_run→ env_pop_tf→ ret_from_exception→ eret

指导书在 env_run 的说明中总结得很清楚:

  1. 保存当前进程的上下文信息
  2. 切换 curenv 为即将运行的进程
  3. 设置 cur_pgdir
  4. 调用 env_pop_tf 恢复现场并异常返回。

这里的难点在于要区分:

  • load_icode:只是把程序内容装进去,解决“进程里面有什么”
  • env_pop_tf:才是把 CPU 的寄存器现场切到这个进程上,解决“CPU 现在为谁服务”

4. 异常处理

Lab3 的后半部分进入 中断与异常
指导书在概况图中指出:

  • 内核初始化完毕后会陷入死循环
  • 等待第一次时钟中断来临
  • 通过异常处理来调度已经创建好的用户进程运行
  • 用户进程运行过程中,又会遇到 TLB Load MissTLB Store MissTimer Interrupt
  • 在时钟中断发生后,操作系统实现了进程切换。

时钟中断

作用是:定期把 CPU 从用户进程手里抢回来

调度(schedule

作用是:决定接下来该谁运行
指导书在 schedule 的提示中强调:

  • count 记录当前进程剩余执行次数
  • 调度队列中包含所有就绪进程
  • scheduleenv_runenv_pop_tfret_from_exception 都是不返回函数。

上下文切换(env_run / env_pop_tf

作用是:真正把 CPU 的执行现场切过去

三者的关系

id
1
2
3
4
5
6
7
Timer Interrupt
→ handle_int
→ schedule
→ env_run
→ env_pop_tf
→ ret_from_exception
→ 新进程运行

5. 整体理解

单独看每一块都能理解,但连起来就容易懵,不理解为什么要这么做。
实际上,这是因为lab3第一次形成了操作系统里最经典的一条闭环:

id
1
2
3
4
5
6
7
8
创建进程
→ 装载程序
→ 运行用户进程
→ 时钟中断发生
→ 进入异常处理
→ 调度器选下一个进程
→ 恢复现场
→ 新进程继续运行

实验体会

  1. lab3的代码补全感觉很简单,提示实在是很清晰,所以没费什么功夫就完成了。但是分开来看都能理解,一合起来看整体流程还是蒙的,感觉在脑子里建立一个完整的流程链蛮困难的。
  2. lab3-pre-exam也是很好理解,但是有一点,不能太依赖hint了,可能有的一些维护的小点hint里没有给,但是自己也要写,比如维护runtime_left
  3. 这次周二就把lab3弄完了,希望能多一点时间理解巩固,lab3-exam加油!lab3-extra加油!
  4. lab3-exam课上45min通过,卡了一下一些细节的地方(很多因为代码段是重复的所以直接从原函数或者lab3-pre-exam复制过来了,导致一些变量名啊函数名啊什么的没有改过来,总体来说还是顺利的)。
  5. 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