北航操作系统Lab3实验报告

2025-04-19

本次实验主要实现了MOS操作系统的进程管理

Thinking 3.1

请结合MOS中的页目录自映射应用解释代码中 e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) PTE_V 的含义。
  • UVPT 代表的是虚拟空间中页表空间的起始地址,PDX(UVPT) 代表的是页目录在页表空间的第几页,所以 env_pgdir[PDX(UVPT)] 代表所谓的页目录自映射项,也就是说这个页目录项中存放的地址,应当是页目录项的物理地址。
  • 所以这行代码的含义是:将进程e的页目录项物理页号与有效位拼接,存到进程e页表空间的页目录自映射项中。

Thinking 3.2

elf_load_seg 以函数指针的形式,接受外部自定义的回调函数 map_page。 请你找到与之相关的 data 这一参数在此处的来源,并思考它的作用。没有这个参数可不可以?为什么?

  • 来源:data 是传入的进程控制块指针,在 load_icode_mapper 和 load_icode 函数中被调用
  • 作用:在增加虚拟地址到物理地址映射的时候提供 env_pgdir 和 env_asid,将进程指针一直传递下去
  • 如果没有data,load_icode_mapper就不能知道当前进程空间的页目录基地址和asid,必须要有这个参数

Thinking 3.3

结合 elf_load_seg 的参数和实现,考虑该函数需要处理哪些页面加载的情况。

elf_load_seg 函数需要处理页面起始未对齐部分、完整页面部分、程序段剩余内存这三种页面加载情况。

  • 起始未对齐部分:当程序段的虚拟地址 va 不是页面大小的整数倍时,将程序段开头部分的数据加载到第一个页面中,这个页面只加载部分数据。

  • 完整页面部分:在处理完页面起始地址未对齐的情况后,将程序段剩余的数据完整地加载到页面中。

  • 程序段剩余内存:把多余的空间用0填充满。

Thinking 3.4

这里的env_tf.cp0_epc字段指示了进程恢复运行时PC应恢复到的位置。我们要运行的 进程的代码段预先被载入到了内存中,且程序入口为e_entry,当我们运行进程时,CPU将自动从PC所指的位置开始执行二进制码。思考上面这一段话,并根据自己在Lab2中的理解,回答:你认为这里的env_tf.cp0_epc存储的是物理地址还是虚拟地址?

  • 存储的是虚拟地址,epc 存储的是发生错误时 CPU 所处的指令地址,对于 CPU 来说,所见的都是虚拟地址,因此 env_tf.cp0_epc 存储的是虚拟地址。

Thinking 3.5

试找出0、1、2、3号异常处理函数的具体实现位置。8号异常(系统调用) 涉及的do_syscall()函数将在Lab4中实现。

  • handle_int 在 genex.S 文件中。

  • handle_mod 和 handle_tlb,handle_sys 三者都是通过 genex.S 文件中的宏函数 BUILD_HANDLER 实现,这个宏接收两个参数:exception 和 handler,拼接生成一个异常处理函数。

.macro BUILD_HANDLER exception handler
NESTED(handle_\exception, TF_SIZE + 8, zero)
        move    a0, sp
        addiu   sp, sp, -8
        jal     \handler
        addiu   sp, sp, 8
        j       ret_from_exception
END(handle_\exception)
.endm
BUILD_HANDLER tlb do_tlb_refill
#if !defined(LAB) || LAB >= 4
BUILD_HANDLER mod do_tlb_mod
BUILD_HANDLER sys do_syscall
#endif

Thinking 3.6

阅读entry.S、genex.S和env_asm.S这几个文件,并尝试说出时钟中断在哪些时候开启,在哪些时候关闭。

  • 异常处理进入时,时钟中断关闭,在 entry.S 的 exc_gen_entry 函数中,当处理异常时,CP0 的状态寄存器 STATUS 的 STATUS_UM(用户模式标志)、STATUS_EXL(异常级别标志)和 STATUS_IE(中断使能标志)清零。STATUS_IE 清零意味着全局中断被禁用,此时时钟中断也被关闭了,避免处理异常的过程中被其他中断干扰。

  • 从异常返回时,时钟中断开启,在 genex.S 中的 ret_from_exception 函数中,调用了 RESTORE_ALL 恢复之前保存的寄存器状态, 然后执行 eret 指令,恢复异常前的状态,包括中断使能标志 STATUS_IE,在执行 eret 之后,时钟中断会重新开启。

  • 在 env_asm.S 中的 env_pop_tf 函数中,跳转到 ret_from_exception,从异常返回,时钟中断重新开启。

# entry.S
exc_gen_entry:
        SAVE_ALL
        mfc0    t0, CP0_STATUS
        and     t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
        mtc0    t0, CP0_STATUS 
        
# genex.S
FEXPORT(ret_from_exception)
        RESTORE_ALL
        eret

# env_asm.S
LEAF(env_pop_tf)
.set reorder
.set at
        mtc0    a1, CP0_ENTRYHI
        move    sp, a0
        RESET_KCLOCK
        j       ret_from_exception
END(env_pop_tf)

Thinking 3.7

阅读相关代码,思考操作系统是怎么根据时钟中断切换进程的。

  • 模拟器通过kclock_init函数完成时钟中断的初始化,设置了时钟中断发生的频率,调用enable_irq函数开启中断。

  • 在进程运行过程中,若时钟发生中断,则会触发MIPS中断,系统将PC指向 0x800000080,跳转到 .text.exc_gen_entry 代码段,进行异常分发。

  • 由于是中断,判断为0号异常,则跳转到中断处理函数handle_init,进而判断属于中断中的 IM4(时钟中断),进而跳转到 timer_irq函数处理,timer_irq 函数调用 schedule 函数开始进行进程调度。

  • 什么时候需要切换进程?

    • 参数yield为真时,此时当前进程必须让出。
    • count减为0时,此时分给进程的时间片被用完,被执行权让给其他进程。

    • 当前无进程,内核必然刚刚完成初始化,需要分配一个进程执行。

    • 进程状态不是可运行,当前进程不能再继续执行,让给其他进程。
  • 如何切换进程?

    • 当前进程仍为就绪状态时,需要将其移到env_sched_list队列的尾部。
    • 选中env_sched_list队列头部的进程,如果没有可用的进程,内核panic。
    • 设置count为当前进程的优先级(分配的时间片的数量)。
    • 最后将count自减1,调用env_run函数。

实验难点

本次实验的难点主要集中在操作系统底层机制的理解与复杂逻辑的实现上,尤其是进程地址空间管理、上下文切换和调度策略的设计,需要结合理论知识,逐层拆解代码逻辑。

  • 进程调度:调度函数的核心是从就绪队列中选择下一个运行进程,实验中需要考虑多种需要调度的情况,此处的难点在于理清进程状态与调度链表操作的对应关系,避免出现链表操作错误或状态遗漏。
  • 虚实地址空间映射:需要理解用户虚拟地址空间的宏定义,这些宏代表进程地址空间中不同段的边界,在更新页目录时,需要区分可动态分配的用户空间和固定映射的内核相关区域,这要求对MMU地址转换机制、自映射原理有清晰认识。
  • 中断处理:这一部分需要结合异常处理流程,理解多段汇编代码,exc_gen_entry 中保存现场并通过操作 CP0 寄存器关闭中断,ret_from_exception 恢复现场并重新开启中断,中断的开启与关闭时机直接影响进程切换的原子性,否则可能导致中断嵌套或竞态条件。
  • 多进程调试:在跳板机环境中调试多进程程序时,进程切换的随机性导致断点难以精准捕获目标状态。设置断点后可能因时钟中断触发调度,难以跟踪单个进程的执行路径。此时使用 printk 打印关键数据是更好的选择。

实验感想

本次实验虽未涉及复杂算法,但对操作系统底层机制的“落地”实现有了更深刻的认识,尤其是理论与代码的映射关系。过去对进程管理的理解停留在PCB、调度队列等抽象概念,实验中则需亲手处理页目录项的物理地址映射。我意识到“用户空间”并非虚无缥缈的概念,而是通过页目录项逐字节定义的地址空间。这种从抽象到具体的转换,让我对操作系统的内存管理、进程管理等底层机制的理解更进一步。