OS-Lab3

Foolish-Han Lv2

Lab3 实验报告

思考题

Thinking 3.1

请结合 MOS 中的页目录自映射应用解释代码中 e-> env_pgdir [PDX(UVPT)] = PADDR(e-> env_pgdir) | PTE_V 的含义。

UVPT(user virtual page table):用户页表起始处的内核虚拟地址

PDX(UVPT):UVPT 的页目录号

PADDR(e-> env_pgdir):用户页目录的内核虚拟地址

PTE_V:页表项的有效位

将页目录中第 PDX(UVPT) 项映射到页目录,使得用户可以访问到页目录的内容(只读)。

Thinking 3.2

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

  • dataload_icode 函数调用 elf_load_seg 时,以参数 e 的形式传入,即进程页控制块的指针。
  • 没有这个参数不可以。这个参数在 elf_load_seg 回调 load_icode_mapper 时,需要通过 e->env_pgdire->env_asid 为其指明页目录和命名空间,使得虚拟地址可以正确映射到复制到的物理空间中

Thinking 3.3

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

  • 判断虚拟地址是否页对齐,不对齐,则首先将虚拟地址所在页面的剩余二进制数据填入对应位置中
  • 接着以页对齐的方式,将剩余的二进制数据填入对应位置中
  • 倘若其在内存中还有冗余的位置,则为其分配相应的空间

Thinking 3.4

思考上面这一段话,并根据自己在 Lab2 中的理解,回答:你认为这里的 env_tf.cp0_epc 存储的是物理地址还是虚拟地址?

虚拟地址。不论是 C 语言,还是汇编语言,其所使用的地址都是虚拟地址。

Thinking 3.5

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

  • 0 号异常处理函数 handle_intgenex.S 中实现,lab3 只能处理时钟中断,最终跳转到 sched.c 中的 schedule 实现
  • 1 号异常处理函数 handle_modgenex.S 中进行分发,跳转到 tlbex.c 中的 do_tlb_mod 函数实现
  • 2、3 号异常处理函数 handle_tlbgenex.S 中进行分发,跳转到 tlbex.c 中的 tlb do_tlb_refill 函数实现

Thinking 3.6

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

当时钟中断发生后,进入 entry.S 中的 exc_gen_entry 函数进行处理,该函数首先调用 stackframe.h 中的 SAVE_ALL 来保存现场,然后关闭时钟中断,进行异常分发处理。在 genex.S 中通过 handle_int 处理时钟中断,跳转到 sched.c 中的 schedule 实现,在 schedule 最后调用 env_run 时,又调用了 env_run 函数。该函数最后调用了实现在 env_asm.S 中的 env_pop_tf 函数。env_pop_tf 又跳转到 genex.S 中的 ret_from_exception 函数,这个函数调用 stackframe.h 中的 RESTORE_ALL 来恢复现场,重新开启时钟中断。

Thinking 3.7

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

类似 3.6,当发生时钟中断后,系统在 sched.c 中通过 schedule 调度进程,当满足一定条件时(详见 [Exercise 3.12](#Exercise 3.12)),则换下一个进程运行(可能仍然是当前进程)。

难点分析

Exercise 3.1

TAILQ 宏定义分析

1
_TAILQ_HEAD(name, type, qual)

定义一个双向尾队列,这个结构体的名称为 name,元素类型为 type,里面含有一个指向队列头元素的指针 tqh_first 和指向队列尾元素的下一个元素的指针的指针 tqh_lastqual 为限定符( quality : const, volatile…)。

1
TAILQ_HEAD(name, type)

定义一个双向尾队列,这个结构体的名称为 name,元素类型为 type,里面含有一个指向队列头元素的指针 tqh_first 和指向队列尾元素的下一个元素的指针的指针 tqh_last

1
TAILQ_HEAD_INITIALIZER(head) 

初始化双向尾队列 head(不是指针),将其 tqh_first 设置为 NULL,将其 tqh_last 设置为 tqh_first 的地址。给元素赋值时作为常量使用,注意与 TAILQ_INIT(head) 区分。

1
_TAILQ_ENTRY(type, qual)  

定义双向尾队列元素的指针域,元素类型为 type,里面含有一个指向下一个元素地址的指针 tqe_next,一个指向前一个元素的下一个元素指针的指针 tqe_prevqual 为限定符( quality : const, volatile…)。

1
TAILQ_ENTRY(type)

定义双向尾队列元素的指针域,元素类型为 type,里面含有一个指向下一个元素地址的指针 tqe_next,一个指向前一个元素的下一个元素指针的指针 tqe_prev

1
TAILQ_INIT(head)

初始化指针 head 指向的双向尾队列,将其 tqh_first 设置为 NULL,将其 tqh_last 设置为 tqh_first 的地址。初始化队列时作为宏函数使用,注意与 TAILQ_HEAD_INITIALIZER(head) 区分。

1
TAILQ_INSERT_HEAD(head, elm, field)

在指针 head 指向的双向尾队列中,将指针 elm 指向的元素插在队列头,field 为元素的指针域结构体名称。

1
TAILQ_INSERT_TAIL(head, elm, field)  

在指针 head 指向的双向尾队列中,将指针 elm 指向的元素插在队列尾,field 为元素的指针域结构体名称。

1
TAILQ_INSERT_AFTER(head, listelm, elm, field) 

在指针 head 指向的双向尾队列中,将指针 elm 指向的元素插在指针 listElm 指向的元素后面,field 为元素的指针域结构体名称。

1
TAILQ_INSERT_BEFORE(listelm, elm, field)

将指针 elm 指向的元素插在指针 listElm 指向的元素前面,field 为元素的指针域结构体名称。

1
TAILQ_REMOVE(head, elm, field)

在指针 head 指向的双向尾队列中,将指针 elm 指向的元素从中移除,field 为元素的指针域结构体名称。

1
TAILQ_FOREACH(var, head, field) 

遍历指针 head 指向的双向尾队列,var 为已声明好的元素指针,供遍历时使用,field 为元素的指针域结构体名称。

1
TAILQ_CONCAT(head1, head2, field)

将指针 head2 指向的双向尾队列插入到指针 head1 指向的双向尾队列后面,并将指针 head2 指向的队列清空,field 为元素的指针域结构体名称。

1
TAILQ_EMPTY(head) ((head)->tqh_first == NULL)

判断指针 head 指向的双向尾队列是否为空。

1
TAILQ_FIRST(head) ((head)->tqh_first)

获得指针 head 指向的双向尾队列的第一个元素的指针。

1
TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)

获得双向尾队列中,elm 指向的元素的下一个元素的指针,field 为元素的指针域结构体名称。

注意事项

  • 初始化空闲进程列表、调度进程列表
  • envs[NENV-1] 开始倒序将进程控制块插入空闲进程列表中。

Exercise 3.2

利用 page_insert 函数,将从 va va + i 的虚拟地址映射到 pa pa + i,逐页将物理地址转化为页控制块指针,插入即可。

Exercise 3.3

尝试分配一个页面作为进程的页目录,将页面的引用次数加一,同时设置到进程控制块中。

Exercise 3.4

  • 空闲进程列表为空,则返回错误值;不为空,则取出其中第一个进程控制块
  • 尝试初始化新进程的地址空间
  • 为新进程设置父进程的 id,并为自己获取 id,尝试分配命名空间
  • 从空闲进程列表中移除,赋值到传入指针指定的空间中

Exercise 3.5

  • 尝试分配页
  • 利用 memcpy 将 src 处开始长度为 len 字节的内容复制到页的起始处(page2kva(p))加 offset 偏移量的位置上

Exercise 3.6

将进程控制块中 env_tf 的 cp0_epc 设置为 elf 文件的入口地址,使得进程调度时可以从正确的位置开始执行

Exercise 3.7

  • 分配一个进程用于初始化,父进程 id 设置为 0,代表该进程为根节点的进程
  • 为该进程设置优先级,并将状态设置为可运行
  • 载入该进程执行所需的二进制数据,并将该进程插入到进程调度列表中

Exercise 3.8

  • 设置进程运行所需的页目录
  • 调用汇编函数恢复现场,指明该进程的 trapframe 地址和命名空间

Exercise 3.9

  • CP0_CAUSE 中获取异常信息
  • 取其 6:2 位获得中断码
  • 根据中断码获取相应中断的处理地址
  • 跳转到相应位置处理中断

Exercise 3.10

按照要求将 .text.exc_gen_entry 段和 .text.tlb_miss_entry 段 放到相应的地址空间即可

Exercise 3.11

  • 将计数器清零
  • CP0_COMPARE 设置为预先确定的值

Exercise 3.12

  • 当前进程为空 或 时间片用完 或 当前进程不可运行 或 当前进程主动让出时间片时
    • 如果当前进程不为空
      • 把它从调度进程列表中移除
      • 如果当前进程可运行
        • 重新添加到调度进程列表的尾部
    • 判断调度进程列表是否为空并在为空时发出警告
    • 从调度进程列表中取出一个新的进程控制块
    • 设置时间片长度为该进程的优先级
  • 该进程用掉一个时间片,时间片长度减一
  • 运行该进程

实验体会

这次实验和上次实验一样,难点都不在于代码的补全,而在于整个进程调度的流程、中断处理的方式以及各个函数的调用关系的把控。实验代码中有大量令人困惑的缩写、难以理解的操作,尽管不影响补全,但往往既不知其然,也不知其所以然,学了也跟没学一样。指导书也只能提供极为有限的帮助。

对此,只有顺着 mips_init 的调用逐层去尽可能的理解代码,在各个 C 文件、汇编文件、链接文件之间反复横跳,尽可能地去顿悟(问 AI、看博客、做梦)。毕竟,这次上机也告诉我们,不理解整个体系的代价就是不给提示就做不下去。