OS_Lab3_实验报告
一.思考题
Thinking 3.1
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V 实现了页目录自映射。
这段代码将进程页目录中的某一个表项(对应 UVPT 虚拟地址段)指向了页目录自身的物理地址。这样一来,无论是在内核态还是用户态,只需要通过访问固定的虚拟地址 UVPT,就能像访问二维数组一样便捷地查阅和修改整个虚拟地址空间的页表映射情况,避免了在内存中额外维护一套复杂的数据结构。
Thinking 3.2
data 参数是一个上下文指针,在本次实验中它实际传入的是 当前进程控制块的指针 (struct Env *e) 。
不可以没有这个参数。 elf_load_seg 作为一个通用的 ELF 加载函数,它并不知道这些段要加载给哪一个进程。通过 data 参数将进程指针传给回调函数 load_icode_mapper,回调函数内部才能知道应该调用 page_insert 将分配的物理页插入到哪个进程的页目录 (env->env_pgdir) 中。
Thinking 3.3
结合 elf_load_seg 的参数和实现,该函数主要处理了以下几种页面加载情况:
- 常规段映射 :段的大小和地址正好是页对齐的,直接按页拷贝。
- 非页对齐映射 :段的起始地址或结束地址不在页边界上,需要处理
offset偏移量。 - .bss 段处理(零初始化区) :当分配的内存大小(
sg_size)大于实际 ELF 文件中存储的数据大小(bin_size)时,多出来的部分就是.bss段。此时src指针为NULL,回调函数只需要分配物理页建立映射,不需要执行拷贝。
Thinking 3.4
env_tf.cp0_epc 存储的是 虚拟地址 。
因为当进程被调度运行,底层汇编执行 eret 指令从内核态返回用户态时,CPU 会将 PC 指针强制置为 EPC 寄存器中的值。随后 CPU 在用户态下发出的取指地址都会经过 MMU/TLB 的地址翻译,所以这里必须填入 ELF 镜像中指定的程序入口虚拟地址。
Thinking 3.5
在 kern/traps.c 中,异常处理函数地址被填入了 exception_handlers 数组:
- 0 号异常 :处理函数为
handle_int,在kern/genex.S中通过NESTED(handle_int, ...)宏定义实现。 - 1 号异常 :处理函数为
handle_mod,在kern/genex.S中实现(TLB 修改异常)。 - 2、3 号异常 :处理函数为
handle_tlb,在kern/genex.S中通过BUILD_HANDLER tlb do_tlb_refill宏展开实现。
Thinking 3.6
- 开启时机 :在用户态程序运行时是开启的。我们在
env_alloc中将新进程的cp0_status赋予了STATUS_IE和STATUS_IM7。每次进程通过env_pop_tf恢复上下文并执行eret返回用户态时,硬件中断就会被开启。 - 关闭时机 :在陷入内核处理异常/中断时是关闭的。当异常发生进入
exc_gen_entry时,汇编代码通过and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)显式清除了IE位,关闭了中断,防止在内核处理异常时发生嵌套。
Thinking 3.7
操作系统根据时钟中断切换进程的流程如下:
- 硬件触发 :硬件 Timer 计时器到达设定值,触发 7 号硬件中断。
- 异常分发 :CPU 陷入内核,PC 指向
0x80000180,执行entry.S中的SAVE_ALL保存当前进程上下文到内核栈。 - 识别与调度 :跳转到
handle_int,识别出是时钟中断,调用kern/sched.c中的schedule(0)。 - 队列轮转 :
schedule将当前进程时间片减一。若用完,则将其从就绪队列头拔下插到队尾,并选出新的队头进程。 - 恢复执行 :调用
env_run,更新当前页目录,调用env_pop_tf恢复新进程的上下文,最后执行eret,CPU 开始运行新进程。
二.难点分析
Lab 3 的难点主要集中在异常体系的构建和软硬件结合的上下文切换。
难点一:异常分发查表中的位运算巧思
难点解析: 在 entry.S 中,我们需要根据 CP0 Cause 寄存器中的 ExcCode 去查 exception_handlers 数组。ExcCode 位于寄存器的第 2~6 位。
精妙之处在于:函数指针在 32 位机器下正好占 4 字节(等于左移 2 位)。因此,我们不需要将 ExcCode 右移回最低位再乘以 4,而是直接使用掩码 0x7C (0111 1100) 按位与,截取出来的数值 天然就是目标函数在数组中的字节偏移量 。这极大地优化了异常处理底层的指令开销。
难点二:进程上下文切换的“偷梁换柱” (env_run)
难点解析: 进程切换并非简单的方法调用。其难点在于深刻理解 KSTACKTOP - 1 的含义:当异常发生时,硬件和底层汇编已将旧进程的寄存器现场压入了内核栈。env_run 必须精确地将这段内存拷贝到旧进程的 PCB->env_tf 中保存;随后,将新进程的页目录基址赋给 cur_pgdir 以切换内存视野;最后通过 env_pop_tf 将新进程的 env_tf 重新装载到硬件寄存器中。
难点三:ELF 加载与 .bss 段的陷阱 (load_icode_mapper)
难点解析: 在使用回调函数映射 ELF 段时,极易忽略物理页指针与虚拟地址的转换(必须使用 page2kva(p) 获取内核虚拟地址才能 memcpy)。此外,.bss 段在磁盘中无实际数据,传入的 src 为 NULL,此时只需映射清零的物理页,绝不能直接进行 memcpy,否则会引发 Kernel Panic。
三.实验体会
这次 Lab 3 的实验相比于 Lab 2,代码量虽然不多,但逻辑深度有了质的飞跃。一开始面对各种繁杂的汇编宏(如 SAVE_ALL、RESTORE_ALL)和进程状态的转换感到十分困惑。尤其是“伪造现场”将 ELF 入口地址强行塞入 cp0_epc,再利用 eret 巧妙启动进程的设计,让我拍案叫绝。
通过在纸上画图推演 TAILQ 双向队列在 schedule 时间片轮转中的变化,并在网上查阅 MIPS 架构 CP0 寄存器的手册,我终于打通了硬件中断触发到软件系统调度的任督二脉。看到终端里两个死循环进程在时钟中断的驱动下完美交替运行,极大地满足了我的成就感!这也为我后续理解 Lab 4 的 IPC 和 fork 写时复制机制打下了坚实的基础。