BUAA OS LAB3 实验报告

Thinking

Thinking 3.1

Q:

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

A:

UVPT为整个页表起始虚拟地址,e->env_pgdir[PDX(UVPT)为其对应的一级页表项,即为一个二级页表的基地址,PADDR(e->env_pgdir) | PTE_V为整个页表起始的物理地址,该语句作用为将页目录自身物理地址映射为自身其中的一个一级页表项。

Thinking 3.2

Q:

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

A:

来源
include/elf.h中可以找到typedef int (*elf_mapper_t)(void *data, u_long va, size_t offset, u_int perm, const void *src,size_t len);

作用
env.cload_icode函数对elf_load_seg有如下调用:

1
elf_load_seg(ph, binary + ph->p_offset, load_icode_mapper, e)

在形参void *data的位置传入了进程控制块指针e。进一步地,我们阅读load_icode_mapper的实现:

1
2
3
4
5
6
7
8
9
10
11
static int load_icode_mapper(void *data, u_long va, size_t offset, u_int perm, const void *src,size_t len) {
struct Env *env = (struct Env *)data;
struct Page *p;
int r;
page_alloc(&p);
if (src != NULL) {
void *dst = (void *)page2kva(p) + offset;
memcpy(dst, src, len);
}
return page_insert(env->env_pgdir, env->env_asid,p, va, perm);
}

可知env这个结构控制块指针在回调函数里的作用是为page_insert函数提供了参数env->env_pgdirenv->env_asid

elf_load_seg是一个被要求满足多种调用需求的函数,而对不同调用需求的满足传入不同的mapper回调函数和data函数指针,即可实现灵活的变化。而又因为需求不同,传入data指针的类型也会存在差异,因此,采用以void类型传入,在对应的mapper回调函数中进行相应的转型可以极大程度上提升elf_load_seg函数的灵活性和可复用性。

Thinking 3.3

Q:

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

A:

  • 如果段的虚拟地址与页面边界不对齐,需要将二进制文件的前一个页面映射到虚拟地址空间中。偏移量为 offset = va - ROUNDDOWN(va, BY2PG)
  • 如果二进制文件大小小于一个页面,则需要将该页面映射到虚拟地址空间中。此时,需要使用map_page回调函数来分配和映射物理页面。
  • 如果二进制文件大小大于一个页面,则需要将文件的每一页映射到虚拟地址空间中。同样需要使用map_page回调函数来进行物理页面映射。
  • 如果二进制文件大小小于段的内存大小,则需要分配和映射页面,直到达到段的内存大小为止。

Thinking 3.4

Q:

思考上面这一段话,并根据自己在Lab2中的理解,回答:

你认为这里的env_tf.cp0_epc存储的是物理地址还是虚拟地址

A:

PC 是CPU中用于记录当前运行代码在内存中的地址的寄存器,而CPU发出的地址不可能是物理地址,而是当前运行进程地址空间中的虚拟地址。env_tf.cp0_epc是一个虚拟地址

Thinking 3.5

Q:

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

A:

在文件genex.S中有:

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
37
38
#include <asm/asm.h>
#include <stackframe.h>

.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

.text

FEXPORT(ret_from_exception)
RESTORE_ALL
eret

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)

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

那么对于这道思考题,我们不难做出回答:

对于0号异常,对应的handler的具体实现在genex.S中的汇编函数handle_int

对于1号异常,handler的具体实现为tlbex.c中的do_tlb_mod函数;

而对于2、3号异常,handler的具体实现为tlbex.c中的do_tlb_refill函数。

1
mips

Thinking 3.6

Q:

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

A:

时钟中断的开启与关闭时机分析

根据代码逻辑和描述,时钟中断的开关状态主要由 CP0 Status寄存器中断使能位(IE)Timer配置 控制。以下是具体时机:


1. 时钟中断关闭的时机

a. 进入异常处理时(全局关闭中断)
  • 代码位置entry.S.text.exc_gen_entry 段。

  • 操作
    在保存上下文(SAVE_ALL)后,通过清除 CP0_STATUSSTATUS_IE 位关闭全局中断:

    1
    2
    3
    mfc0    t0, CP0_STATUS
    and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE) # 清除 IE 位(关闭中断)
    mtc0 t0, CP0_STATUS
  • 原因
    防止处理异常时被新的中断打断,确保异常处理的原子性。

b. 处理时钟中断过程中(隐式关闭中断)
  • 代码位置genex.Shandle_int 函数。
  • 机制
    在进入异常处理流程后,硬件自动设置 CP0_STATUS.EXL=1,这会隐式关闭中断(即使 IE=1EXL=1 时中断仍被屏蔽)。
  • 表现
    执行 handle_intret_from_exception 期间,中断保持关闭。

2. 时钟中断开启的时机

a. 异常处理返回时(恢复中断使能)
  • 代码位置genex.Sret_from_exception 段。

  • 操作
    通过 RESTORE_ALL 宏恢复进程的 CP0_STATUS 寄存器,可能重新开启中断(若原状态 IE=1):

    1
    2
    3
    FEXPORT(ret_from_exception)
    RESTORE_ALL # 恢复 Status 寄存器(包括 IE 位)
    eret
  • 触发场景

    • 从时钟中断处理(handle_int)返回用户态时。
    • 进程切换后通过 env_pop_tf 跳转到 ret_from_exception 恢复新进程的上下文。
b. 进程调度前初始化 Timer(显式开启中断)
  • 代码位置env_asm.Senv_pop_tf 函数。

  • 操作

    • RESET_KCLOCK 宏配置 Timer(重置 CountCompare 寄存器),启动时钟中断计时。

    • 跳转到 ret_from_exception,通过 RESTORE_ALL 恢复新进程的 CP0_STATUS(通常包含 IE=1)以开启中断:

      1
      2
      3
      4
      LEAF(env_pop_tf)
      RESET_KCLOCK # 配置 Timer
      j ret_from_exception # 恢复 Status 寄存器(开启中断)
      END(env_pop_tf)
  • 关键作用
    确保新进程开始执行时,时钟中断已启用,从而驱动时间片轮转调度。


总结表格

场景 时钟中断状态 关键代码/行为
进入异常处理流程 关闭 exc_gen_entry 清除 STATUS_IE
处理时钟中断中 关闭 硬件隐式关闭(EXL=1
异常处理返回(用户态) 开启 ret_from_exception 恢复 STATUS_IE
进程切换后初始化 Timer 开启 env_pop_tfret_from_exception

Thinking 3.7

Q:

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

A:

其实指导书已经把流程梳理的比较清楚了,我在这里再归纳一下。

step1:异常分发

当硬件产生时钟中断时,处理器进入异常分发程序entry.S中的exc_gen_entry:

1
2
3
4
5
6
exc_gen_entry:
SAVE_ALL // 保存上下文到异常栈
mfc0 t0, CP0_CAUSE
andi t0, 0x7c // 0b'0111_1100取6-2位异常码
lw t0, exception_handlers(t0)
jr t0 // 跳转到相应的异常处理程序

step2:进一步细分中断类型

在上面的异常分发程序中,我们获取到的异常码为0(也就是中断异常),在获取异常码后,便跳转到genex.S中相应的异常处理程序handle_int来进一步细分中断异常的类型:

1
2
3
4
5
6
7
8
9
10
11
12
NESTED(handle_int, TF_SIZE, zero)
mfc0 t0, CP0_CAUSE
mfc0 t2, CP0_STATUS
and t0, t2
andi t1, t0, STATUS_IM4
bnez t1, timer_irq // 检查到是时钟中断
// TODO: handle other irqs
timer_irq:
sw zero, (KSEG1 | DEV_RTC_ADDRESS | DEV_RTC_INTERRUPT_ACK) // 写该地址响应中断
li a0, 0
j schedule // 跳转到schedule函数进行进程的调度
END(handle_int)

step3:进程调度

调用sched.c中的schedule函数,通过时间片轮转算法来实现进程的调度。

lab2难点

T1

注意需要以倒叙插入,因为LIST_INSERT_HEAD为插入头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void env_init(void) {
int i;
/* 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. */

/* Exercise 3.1: Your code here. (2/2) */

for (i = NENV - 1; i >= 0; --i) {
envs[i].env_status = ENV_FREE;
LIST_INSERT_HEAD(&env_free_list, &envs[i], env_link);
}
......
}

T2

pa2page()物理地址转换为页表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void map_segment(Pde *pgdir, u_int asid, u_long pa, u_long va, u_int size, u_int perm) {

assert(pa % PAGE_SIZE == 0);
assert(va % PAGE_SIZE == 0);
assert(size % PAGE_SIZE == 0);

/* Step 1: Map virtual address space to physical address space. */
for (int i = 0; i < size; i += PAGE_SIZE) {
/*
* Hint:
* Map the virtual page 'va + i' to the physical page 'pa + i' using 'page_insert'.
* Use 'pa2page' to get the 'struct Page *' of the physical address.
*/
/* Exercise 3.2: Your code here. */
panic_on(page_insert(pgdir, asid, pa2page(pa + i), va + i, perm));
}
}

T3

e->env_pgdir = (Pde *)page2kva(p);接受的为虚拟地址

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
static int env_setup_vm(struct Env *e) {
/* Step 1:
* Allocate a page for the page directory with 'page_alloc'.
* Increase its 'pp_ref' and assign its kernel address to 'e->env_pgdir'.
*
* Hint:
* You can get the kernel address of a specified physical page using 'page2kva'.
*/
struct Page *p;
try(page_alloc(&p));
/* Exercise 3.3: Your code here. */

p->pp_ref++;
e->env_pgdir = (Pde *)page2kva(p);

/* Step 2: Copy the template page directory 'base_pgdir' to 'e->env_pgdir'. */
/* Hint:
* As a result, the address space of all envs is identical in [UTOP, UVPT).
* See include/mmu.h for layout.
*/
memcpy(e->env_pgdir + PDX(UTOP), base_pgdir + PDX(UTOP),
sizeof(Pde) * (PDX(UVPT) - PDX(UTOP)));

/* Step 3: Map its own page table at 'UVPT' with readonly permission.
* As a result, user programs can read its page table through 'UVPT' */
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V;
return 0;
}

T4

注意异常返回值

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
int env_alloc(struct Env **new, u_int parent_id) {
int r;
struct Env *e;

/* Step 1: Get a free Env from 'env_free_list' */
/* Exercise 3.4: Your code here. (1/4) */

e = LIST_FIRST(&env_free_list);
if (e == NULL) {
return -E_NO_FREE_ENV;
}

/* Step 2: Call a 'env_setup_vm' to initialize the user address space for this new Env. */
/* Exercise 3.4: Your code here. (2/4) */

if ((r = env_setup_vm(e)) != 0) {
return r;
}

/* Step 3: Initialize these fields for the new Env with appropriate values:
* 'env_user_tlb_mod_entry' (lab4), 'env_runs' (lab6), 'env_id' (lab3), 'env_asid' (lab3),
* 'env_parent_id' (lab3)
*
* Hint:
* Use 'asid_alloc' to allocate a free asid.
* Use 'mkenvid' to allocate a free envid.
*/
e->env_user_tlb_mod_entry = 0; // for lab4
e->env_runs = 0; // for lab6
/* Exercise 3.4: Your code here. (3/4) */
if ((r = asid_alloc(&e->env_asid)) != 0) {
return r;
}
e->env_id = mkenvid(e);
e->env_parent_id = parent_id;

/* Step 4: Initialize the sp and 'cp0_status' in 'e->env_tf'.
* Set the EXL bit to ensure that the processor remains in kernel mode during context
* recovery. Additionally, set UM to 1 so that when ERET unsets EXL, the processor
* transitions to user mode.
*/
e->env_tf.cp0_status = STATUS_IM7 | STATUS_IE | STATUS_EXL | STATUS_UM;
// Reserve space for 'argc' and 'argv'.
e->env_tf.regs[29] = USTACKTOP - sizeof(int) - sizeof(char **);

/* Step 5: Remove the new Env from env_free_list. */
/* Exercise 3.4: Your code here. (4/4) */

LIST_REMOVE(e, env_link);

*new = e;
return 0;
}

T5

memcpy();接受虚拟地址

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
static int load_icode_mapper(void *data, u_long va, size_t offset, u_int perm, const void *src,
size_t len) {
struct Env *env = (struct Env *)data;
struct Page *p;
int r;

/* 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);
}

T6

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
static void load_icode(struct Env *e, const void *binary, size_t size) {
315 /* Step 1: Use 'elf_from' to parse an ELF header from 'binary'. */
316 const Elf32_Ehdr *ehdr = elf_from(binary, size);
317 if (!ehdr) {
318 panic("bad elf at %x", binary);
319 }
320
321 /* Step 2: Load the segments using 'ELF_FOREACH_PHDR_OFF' and 'elf_load_seg'.
322 * As a loader, we just care about loadable segments, so parse only program headers here.
323 */
324 size_t ph_off;
325 ELF_FOREACH_PHDR_OFF (ph_off, ehdr) {
326 Elf32_Phdr *ph = (Elf32_Phdr *)(binary + ph_off);
327 if (ph->p_type == PT_LOAD) {
328 // 'elf_load_seg' is defined in lib/elfloader.c
329 // 'load_icode_mapper' defines the way in which a page in this segment
330 // should be mapped.
331 panic_on(elf_load_seg(ph, binary + ph->p_offset, load_icode_mapper, e));
332 }
333 }
334
335 /* Step 3: Set 'e->env_tf.cp0_epc' to 'ehdr->e_entry'. */
336 /* Exercise 3.6: Your code here. */
337 e->env_tf.cp0_epc=ehdr->e_entry;
338 }

T7

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) {
349 struct Env *e;
350 /* Step 1: Use 'env_alloc' to alloc a new env, with 0 as 'parent_id'. */
351 /* Exercise 3.7: Your code here. (1/3) */
352 panic_on(env_alloc(&e,0));
353 /* Step 2: Assign the 'priority' to 'e' and mark its 'env_status' as runnable. */
354 /* Exercise 3.7: Your code here. (2/3) */
355 e->env_pri=priority;
356 e->envstatus=ENV_RUNNABLE;
357 /* Step 3: Use 'load_icode' to load the image from 'binary', and insert 'e' into
358 * 'env_sched_list' using 'TAILQ_INSERT_HEAD'. */
359 /* Exercise 3.7: Your code here. (3/3) */
360 load_icode(e,binary,size);
361 TAILQ_INSERT_HEAD(&env_sched_list,e,env_sched_link);
362 return e;
363 }

T8

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
37
38
void env_run(struct Env *e) {
assert(e->env_status == ENV_RUNNABLE);
// WARNING BEGIN: DO NOT MODIFY FOLLOWING LINES!
#ifdef MOS_PRE_ENV_RUN
MOS_PRE_ENV_RUN_STMT
#endif
// WARNING END

/* Step 1:
* If 'curenv' is NULL, this is the first time through.
* If not, we may be switching from a previous env, so save its context into
* 'curenv->env_tf' first.
*/
if (curenv) {
curenv->env_tf = *((struct Trapframe *)KSTACKTOP - 1);
}

/* Step 2: Change 'curenv' to 'e'. */
curenv = e;
curenv->env_runs++; // lab6

/* Step 3: Change 'cur_pgdir' to 'curenv->env_pgdir', switching to its address space. */
/* Exercise 3.8: Your code here. (1/2) */

cur_pgdir = curenv->env_pgdir;

/* Step 4: Use 'env_pop_tf' to restore the curenv's saved context (registers) and return/go
* to user mode.
*
* Hint:
* - You should use 'curenv->env_asid' here.
* - 'env_pop_tf' is a 'noreturn' function: it restores PC from 'cp0_epc' thus not
* returning to the kernel caller, making 'env_run' a 'noreturn' function as well.
*/
/* Exercise 3.8: Your code here. (2/2) */
env_pop_tf(&curenv->env_tf, curenv->env_asid);
}

T9

注意插入位置为下方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.section .text.exc_gen_entry
exc_gen_entry:
SAVE_ALL
/*
* Note: When EXL is set or UM is unset, the processor is in kernel mode.
* When EXL is set, the value of EPC is not updated when a new exception occurs.
* To keep the processor in kernel mode and enable exception reentrancy,
* we unset UM and EXL, and unset IE to globally disable interrupts.
*/
mfc0 t0, CP0_STATUS
and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
mtc0 t0, CP0_STATUS
/* Exercise 3.9: Your code here. */
mfc0 t0, CP0_CAUSE
andi t0, 0x7c
lw t0, exception_handlers(t0)
jr t0

T10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SECTIONS {
/* Exercise 3.10: Your code here. */

. = 0x80000000;
.tlb_miss_entry : {
*(.text.tlb_miss_entry)
}

. = 0x80000180;
.exc_gen_entry : {
*(.text.exc_gen_entry)
}

......
}

T11

重置计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.macro RESET_KCLOCK
li t0, TIMER_INTERVAL
/*
* Hint:
* Use 'mtc0' to write an appropriate value into the CP0_COUNT and CP0_COMPARE registers.
* Writing to the CP0_COMPARE register will clear the timer interrupt.
* The CP0_COUNT register increments at a fixed frequency. When the values of CP0_COUNT and
* CP0_COMPARE registers are equal, the timer interrupt will be triggered.
*
*/
/* Exercise 3.11: Your code here. */
mtc0 zero, CP0_COUNT
mtc0 t0, CP0_COMPARE
.endm

T12

注意内外层逻辑顺序

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
void schedule(int yield) {
static int count = 0; // remaining time slices of current env
struct Env *e = curenv;

/* We always decrease the 'count' by 1.
*
* If 'yield' is set, or 'count' has been decreased to 0, or 'e' (previous 'curenv') is
* 'NULL', or 'e' is not runnable, then we pick up a new env from 'env_sched_list' (list of
* all runnable envs), set 'count' to its priority, and schedule it with 'env_run'. **Panic
* if that list is empty**.
*
* (Note that if 'e' is still a runnable env, we should move it to the tail of
* 'env_sched_list' before picking up another env from its head, or we will schedule the
* head env repeatedly.)
*
* Otherwise, we simply schedule 'e' again.
*
* You may want to use macros below:
* 'TAILQ_FIRST', 'TAILQ_REMOVE', 'TAILQ_INSERT_TAIL'
*/
/* Exercise 3.12: Your code here. */
if (yield || count == 0 || e == NULL || e->env_status != ENV_RUNNABLE) {
if (e != NULL && e->env_status == ENV_RUNNABLE) {
TAILQ_REMOVE(&env_sched_list, e, env_sched_link);
TAILQ_INSERT_TAIL(&env_sched_list, e, env_sched_link);
}
e = TAILQ_FIRST(&env_sched_list);
if (e == NULL) {
panic("schedule: no runnable envs\n");
}
count = e->env_pri;
}
count--;
env_run(e);
}

实验体会总结

lab3主要为进程的创建和切换等操作,涉及到了内存的分配和映射,大体和lab2的思路相同,主要理解好内存管理相关的内容后就可以较为轻松地完成,但是,其中涉及到一些复杂的进程管理问题,需要熟读指导书和hint找到相关属性和宏定义。