指导书梳理

相关文件:/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专有概念)

在用户空间中读取内核信息

Untitled

  • 原理

    MOS 被设计成对于其中运行的每一个用户进程,都可以通过用户地址空间(kuseg)读取 pages 数组和 envs 数组的信息。为实现该功能,在创建用户进程时我们需要将 pages 数组和 envs 数组映射到用户地址空间中的 UPAGESUENVS 处。

    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 之间则是进程自己的页表

    Untitled

加载二进制镜像

要想正确加载一个 ELF 文件到内存,只需将 ELF 文件中所有需要加载的程序段加载到对应的虚拟地址即可

Untitled

  • 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 寄存器

Untitled

  • IE 位表示中断是否开启,为 1 表示开启,否则不开启

  • 当且仅当 EXL 被设置为 0 且 UM 被设置为 1 时,处理器处于用户模式,其它所有情况下,处理器均处于内核模式下

    • 每当异常发生的时候,EXL 会被自动设置为 1(使处于内核态,能使用特权指令)

    • 每个进程在每一次被调度时都会执行

      1
      2
      RESTORE_ALL //恢复处理器寄存器状态
      eret // EXL被自动设置为 0
  • 15-8 位为中断屏蔽位,每一位代表一个不同的中断活动,其中 15-10 位使能硬件中断源(该中断能否被响应),9-8位是 Cause 寄存器软件可写的中断位

Cause 寄存器

Untitled

  • IP (15-8 位)保存着哪一些中断发生了,其中 15-10 位来自硬件,9-8 位可以由软件写入,当 Status 寄存器中相同位允许中断(为 1)时,Cause 寄存器这一位也为1就会导致中断
  • ExcCode (6-2位),记录发生了什么异常。在 MOS 中,中断是 0 号异常。

CPU异常处理流程

CPU 如何处理异常?

  1. 设置 EPC 寄存器的值为从异常返回的地址。
  2. 设置 Status 寄存器(设置 EXL 位,强制 CPU 进入内核态并禁止中断
  3. 设置 Cause 寄存器(记录异常原因)
  4. 设置 PC 为异常入口地址,随后交给软件处理

至此,我们成功地切换到内核程序,将异常处理的任务转交给操作系统。

  • 相关指令

    • mfc0 Move From Coprocessor 0

      用于从协处理器的某个寄存器中读取值到一个通用寄存器中

    • mtc0 Move To Coprocessor 0

      用于将一个通用寄存器的值写入协处理器的某个寄存器中

    1
    2
    3
    mfc0    t0, CP0_STATUS
    and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
    mtc0 t0, CP0_STATUS
### SAVE_ALL 与异常重入

在 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处

时钟中断

  • 中断处理的流程

    1. 通过异常分发,判断出当前异常为中断异常,随后进入相应的中断处理程序。在 MOS 中即对应 handle_int 函数。

      此前以异常的角度对时钟中断进行处理;现在以中断的角度对时钟中断进行处理

    2. 在中断处理程序中进一步判断 Cause 寄存器中是由几号中断位引发的中断,然后进入不同中断对应的中断服务函数

    3. 中断处理完成,通过 ret_from_exception 函数恢复现场,继续执行。

  • 在MOS 中,时间片的长度是用时钟中断衡量的。4KC 中的 CP0 内置了一个可产生中断的 Timer,MOS 即使用这个内置的 Timer 产生时钟中断

    (具体细节实现见指导书)

  • 内核初始化完毕后陷入死循环,等待第一次时钟中断来临,通过异常处理来调度已经创建好的用户进程运行

进程调度器

什么时候需要切换进程?

  1. 参数 yield 为真时:此时当前进程必须让出。
  2. count 减为 0 时:此时分给进程的时间片被用完,将执行权让给其他进程。
  3. 无当前进程:内核必然刚刚完成初始化,需要分配一个进程执行。
  4. 进程状态不是可运行:当前进程不能再继续执行,让给其他进程。

如何切换进程?

  1. 当前进程仍为就绪状态时,需要将其移到 env_sched_list 队列的尾部。
  2. 选中 env_sched_list 队列头部的进程。如果没有可用的进程,内核 panic。
  3. 设置 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

近年都是处理新的异常