BUAA_OS_lab3

BUAA_OS_lab3学习日记

Thinking思考题

Thinking 3.1

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

答:

User VPT中存放了1024个页表,由于自映射的关系,这个页表里又包括了页目录的页表,每一个页表中又有1024个页表项,UVPT:用户页表的起始处的内核虚拟地址

PDX(UVPT):UVPT所处的页目录号(即 UVPT 处于第 PDX(UVPT) 个页目录项所映射的4MB空间。PDX获得的是这个虚拟地址的页目录号。

e->env_pgdir:进程 e 的页目录的内核虚拟地址

PADDR(e->env_pgdir):进程 e 的页目录的物理地址

PADDR(e->env_pgdir) | PTE_V:页目录的物理基地址,加上权限位(只读权限)

自映射的意义即是只需要知道二级页表的初始地址,就能构造出页目录的初始地址以及指向页目录自身的PDE,这样,方便我们访问管理页表与页目录。

Thinking 3.2

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

1
int elf_load_seg(Elf32_Phdr *ph, const void *bin, elf_mapper_t map_page, void *data);

data 是传入的进程控制块指针,在 load_icode_mapper 和 load_icode 函数中被调用,在 load_icode 函数中 data 被赋予进程控制块的指针 e。

作用:在增加虚拟地址到物理地址映射的时候提供env_pgdir和env_asid;如果没有data,load_icode_mapper就不能知道当前进程空间的页目录基地址和asid,所以必须要有这个参数。

Thinking 3.3

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int elf_load_seg(Elf32_Phdr *ph, const void *bin, elf_mapper_t map_page, void *data) {
u_long va = ph->p_vaddr;
size_t bin_size = ph->p_filesz;
size_t sgsize = ph->p_memsz;
u_int perm = PTE_V;
if (ph->p_flags & PF_W) {
perm |= PTE_D;
}

int r;
size_t i;
u_long offset = va - ROUNDDOWN(va, PAGE_SIZE);
if (offset != 0) {
if ((r = map_page(data, va, offset, perm, bin,
MIN(bin_size, PAGE_SIZE - offset))) != 0) {
return r;
}
}

/* Step 1: load all content of bin into memory. */
for (i = offset ? MIN(bin_size, PAGE_SIZE - offset) : 0; i < bin_size; i += PAGE_SIZE) {
if ((r = map_page(data, va + i, 0, perm, bin + i, MIN(bin_size - i, PAGE_SIZE))) !=
0) {
return r;
}
}

/* Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`. */
while (i < sgsize) {
if ((r = map_page(data, va + i, 0, perm, NULL, MIN(sgsize - i, PAGE_SIZE))) != 0) {
return r;
}
i += PAGE_SIZE;
}
return 0;
}
  • 函数判断va是否页对齐,如果不对齐,需要将多余的地址记为offset,我们将最开头不对齐的部分 “剪切” 下来,先映射到内存的页中。
  • 将段内所有页映射到物理空间
  • 最后我们处理段大小大于数据大小的情况。在这一部分,我们不断创建新的页,但是并不向其中加载任何内容。

Thinking 3.4

问:这里的 env_tf.cp0_epc 存储的是物理地址还是虚拟地址?

根据上学期计组P7的知识,因为epc存储的是发生错误时CPU所处的指令地址,对于CPU来说,在程序中引用访存的地址都是虚拟地址,因此env_tf.cp0_epc毫无疑问存储的是虚拟地址.

Thinking 3.5

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

备注: C 的拓展语法 [first … last] = value 来对数组某个区间上的 元素赋成同一个值

查找结果如下:

kern/genex.S:20:NESTED(handle_int, TF_SIZE, zero)
kern/genex.S:29:END(handle_int)

handle_int在genex.S文件中

handle_mod和handle_tlb,handle_sys藏得比较深,三者都是通过genex.S文件中的宏函数BUILD_HANDLER实现的,代码如下:

1
2
3
4
5
6
7
8
BUILD_HANDLER tlb do_tlb_refill

#if !defined(LAB) || LAB >= 4
BUILD_HANDLER mod do_tlb_mod
BUILD_HANDLER sys do_syscall
#endif

BUILD_HANDLER reserved do_reserved

Thinking 3.6

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

entry.S

1
2
3
4
5
6
7
8
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)

genex.S

1
2
3
4
5
6
7
8
9
10
NESTED(handle_int, TF_SIZE, zero)
mfc0 t0, CP0_CAUSE
mfc0 t2, CP0_STATUS
and t0, t2
andi t1, t0, STATUS_IM7
bnez t1, timer_irq
timer_irq:
li a0, 0
j schedule
END(handle_int)

当中断屏蔽位位为1且,CP0_CAUSE为时钟中断,且CP0_STATUS正常相应,体现为这几个寄存器储存的值做与运算的结果不为0,时钟中断开始,调度完成后结束。

Thinking 3.7

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

再调度函数中,如果满足切换条件(yield || count <= 0 || e == NULL || e->env_status != ENV_RUNNABLE),如果当前进程没有就绪,将其移到调度链表尾部,反之则不用,接着选中调度链表头部进程达到调度目的。

Exercise

Exercise 3.1

实现进程控制快的初始化功能,具体实现参考注释即可。

1
2
3
4
5
6
7
8
9
10
11
12
/* Step 1: Initialize 'env_free_list' with 'LIST_INIT' and 'env_sched_list' with
* 'TAILQ_INIT'. */
/* Exercise 3.1: Your code here. (1/2) */
LIST_INIT(&env_free_list);
TAILQ_INIT(&env_sched_list);
/* Step 2: Traverse the elements of 'envs' array, set their status to 'ENV_FREE' and insert
* them into the 'env_free_list'. Make sure, after the insertion, the order of envs in the
* list should be the same as they are in the 'envs' array. */
for (i = NENV - 1; i >= 0; i--) {
envs->env_status = ENV_FREE;
LIST_INSERT_HEAD(&env_free_list,&envs[i], env_link);
}

Exercise 3.2

段地址的初始化使用 map_segment 函数在该页表中将内核数组 pages 和 envs 映射到了用户空间的 UPAGES 和 UENVS 处

map_segment功能是在一级页表基地址 pgdir 对应的两级页表结构中做段地址映射,将虚拟地 址段 [va,va+size) 映射到物理地址段 [pa,pa+size)

1
page_insert(pgdir, asid, pa2page(pa + i), va + i, perm);

Exercise 3.3

env_setup_vm函数:初始化新进程的地址空间

步骤如下:

  • 申请一块页表,并将页表转化为kernel地址赋值给e->env_pgdir
  • 制了 UTOPUVPT 的虚拟地址空间对应的页表项
  • 我们将 UVPT 虚拟地址映射到页目录本身的物理地址,并设置只读权限。这样的话,页目录中的项所对应的,就不只是二级页表,还包含有一个一级页表,也就是页目录自身。这就是自映射。

Exercise 3.4

步骤如下:

  • 获取从空进程块表中获取到一个进程块
  • 初始化新进程的地址空间
  • 将进程块的各属性赋值(env_user_tlb_mod_entry、env_runs、env_tf.cp0_status……),手工初始化进程控制块。
  • 将该进程块从空进程块表移除

Exercise 3.5

load_icode_mapper函数

需要分配所需的物理页面,并在页表中建立映射。若 src 非空,你还需要将该处的 ELF 数据拷贝到物理页面中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Step 1: Allocate a page with 'page_alloc'. */
/* Exercise 3.5: Your code here. (1/2) */
if ((r = page_alloc(p)) != 0) {
return r;
}
/* Step 2: If 'src' is not NULL, copy the 'len' bytes started at 'src' into 'offset' at this
* page. */
// Hint: You may want to use 'memcpy'.
if (src != NULL) {
/* Exercise 3.5: Your code here. (2/2) */
memcpy((void *)(page2kva(p) + offset), src, len);
}

/* Step 3: Insert 'p' into 'env->env_pgdir' at 'va' with 'perm'. */
return page_insert(env->env_pgdir, env->env_asid, p, va, perm);

Exercise 3.6

步骤:

  • elf_from做类型转化并检查是否为正确的ELF文件头
  • 接下来使用ELF_FOREACH_PHDR_OFF遍历所有程序表头,在循环中,取出对应的程序头,如果其中的 p_type 类型为 PT_LOAD,说明其对应的程序需要被加载到内存中。我们调用 elf_load_seg 函数来进行加载
  • 将进程控制块中 trap frame 的 epc cp0 寄存器的值设置为 ELF 文件中设定的程序入口地址

Exercise 3.7

直接创建进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Env *env_create(const void *binary, size_t size, int priority) {
struct Env *e;
/* Step 1: Use 'env_alloc' to alloc a new env, with 0 as 'parent_id'. */
/* Exercise 3.7: Your code here. (1/3) */
env_alloc(&e, 0);
/* Step 2: Assign the 'priority' to 'e' and mark its 'env_status' as runnable. */
/* Exercise 3.7: Your code here. (2/3) */
e->env_pri = priority;
e->env_status = ENV_RUNNABLE;
/* Step 3: Use 'load_icode' to load the image from 'binary', and insert 'e' into
* 'env_sched_list' using 'TAILQ_INSERT_HEAD'. */
/* Exercise 3.7: Your code here. (3/3) */
load_icode(e, binary, size);
TAILQ_INSERT_HEAD(&env_sched_list, e, env_sched_link);
return e;
}

Exercise 3.8

  • 保存当前进程的上下文信息。

寄存器保存的地方是KSTACKTOP以下的一个sizeof(TrapFrame)大小的区域中,达到保存上下文的目的

  • 切换 curenv 为即将运行的进程。
  • 设置全局变量 cur_pgdir 为当前进程页目录地址,在 TLB 重填时将用到该全局变量。
  • 调用 env_pop_tf 函数,恢复现场、异常返回。

Exercise 3.9

完成异常分发代码

1
2
3
4
5
6
7
8
9
10
SAVE_ALL
#使用 SAVE_ALL 宏将当前上下文保存到内核的异常栈中。
mfc0 t0, CP0_STATUS
and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
mtc0 t0, CP0_STATUS
mfc0 t0, CP0_CAUSE
andi t0, 0x7c
#获得 Cause 寄存器中的 2~6 位,也就是对应的异常码,这是区别不同异常的重要标志
lw t0, exception_handlers(t0)
jr t0

Exercise 3.10

.text.exc_gen_entry 段和 .text.tlb_miss_entry 段需要被链接器放到特定的位置。

CPU 发生异常(除了用户态地址的 TLB Miss 异常)后,就会自动跳转 到地址 0x80000180 处;发生用户态地址的 TLB Miss 异常时,会自动跳转到地址 0x80000000 处。开始执行。

Exercise 3.11

时钟中断

将CP0_COUNT和CP0_COMPARE赋予适当的值

RESET_KCLOCK 宏将 Count 寄存器清零并将 Compare 寄存器配置为我们所期望的计时器周 期数,这就对 Timer 完成了配置。

1
2
3
mtc0	t0,CP0_COMPARE
li t0,0
mtc0 t0,CP0_COUNT

Exercise 3.12

进程调度——时间片轮转的算法

无需进行切换时,我们只需要将剩余时间片长度 count 减去 1,然后调用 env_run 函数,继 续运行当前进程 curenv。在发生切换的情况下,我们还需要判断当前进程是否仍然就绪,如果 是则将其移动到调度链表的尾部。之后,我们选中调度链表首部的进程来调度运行,将剩余时间 片长度设置为其优先级。

难点分析

进程初始化与分配

初始化:

map_segment 函数 $\rightarrow$ env_init 函数

分配

env_setup_vm函数:初始化新进程的地址空间 $\rightarrow$ env_alloc函数

加载二进制镜像

1
2
3
4
5
static void load_icode(struct Env *e, const void *binary, size_t size); 
//负责加载可执行文件 binary 到进程 e 的内存中。它调用的 elf_from 函数完成了解析 ELF 文件头的部分,elf_load_seg 负责将 ELF 文件的一个 segment 加载到内存。

static int load_icode_mapper(void *data, u_long va, size_t offset, u_int perm, const void *src, size_t len)
//成作为回调函数的 load_icode_mapper 函数,需要分配所需的物理页面,并在页表中建立映射。若 src 非空,需要将该处的 ELF 数据拷贝到物理页面中

进程创建与切换运行

创建:分配一个新的 Env 结构体,设置进程控制块,并将程序 载入到该进程的地址空间

进程切换的时候,为了保证下一次进入这个进程 的时候我们不会再“从头来过”,而是有记忆地从离开的地方继续往后走,我们要保存一些信息, 那么,需要保存什么信息呢?事实上,我们只需要保存进程的上下文信息,包括通用寄存器、HI、 LO 和 CP0 中的 Status,EPC,Cause 和 BadVAddr 寄存器。进程控制块除了 env_tf 其他的字 段在进程切换后还保留在原本的进程控制块中,并不会改变,因此不需要保存。(发生进程调度,或当陷入内 核时,会将当时的进程上下文环境保存在 env_tf 变量中(Trapframe结构体))

中断与异常

时间片:比如设定某个进程的时间片的长度为 200 倍的 TIMER_INTERVAL(时钟中断间隔),那么当 MOS 记录到该进程的执行中发生了 200 个时钟中断 时,MOS 就知晓该进程的时间片结束了。

最后留下一段有意思的空间分配

1
2
3
4
5
6
7
8
9
10
11
12
13
o      KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------end
o | Kernel Stack | | KSTKSIZE /|\
o +----------------------------+----|------ |
o | Kernel Text | | PDMAP
o KERNBASE -----> +----------------------------+----|-------0x8002 0000 |
o | Exception Entry | \|/ \|/
o ULIM -----> +----------------------------+------------0x8000 0000-------
o | User VPT | PDMAP /|\
o UVPT -----> +----------------------------+------------0x7fc0 0000 |
o | pages | PDMAP |
o UPAGES -----> +----------------------------+------------0x7f80 0000 |
o | envs | PDMAP |
o UTOP,UENVS -----> +----------------------------+------------0x7f40 0000 |

感想

本次作业围绕进程的创建、初始化、分配、映射、运行、切换等展开,难度有所提示,但大部分的操作和页表部分类似。


BUAA_OS_lab3
https://fantasylee21.github.io/2024/04/05/BUAA-OS-lab3/
作者
Fantasylee
发布于
2024年4月5日
更新于
2024年5月5日
许可协议