OS-Lab3

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 这一参数在此处的来源,并思考它的作用。没有这个参数可不可以?为什么?
data
在load_icode
函数调用elf_load_seg
时,以参数e
的形式传入,即进程页控制块的指针。- 没有这个参数不可以。这个参数在
elf_load_seg
回调load_icode_mapper
时,需要通过e->env_pgdir
和e->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_int
在genex.S
中实现,lab3 只能处理时钟中断,最终跳转到sched.c
中的schedule
实现 - 1 号异常处理函数
handle_mod
在genex.S
中进行分发,跳转到tlbex.c
中的do_tlb_mod
函数实现 - 2、3 号异常处理函数
handle_tlb
在genex.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_last,qual 为限定符( 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_prev,qual 为限定符( 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 pa
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、看博客、做梦)。毕竟,这次上机也告诉我们,不理解整个体系的代价就是不给提示就做不下去。