Lab3_实验报告

一、思考题

Thinking 3.1

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

e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V

UVPT是用户新建进程页表虚拟地址的起始地址

其表示将页目录中第PDX(UVPT)个页表项映射到页目录本身的物理地址,并设置有效位。

目的旨在在用户内存空间中划分出一部分,使得用户可以通过访问这部分空间得到二级页表以及页目录中的数据

因为在UVPT-ULIM之间的4MB空间的va 它们的PDX(va) = PDX(UVPT) = 0x1ff

如图我访问一级页表的时候 查一级页表得到的二级页表基地址就是页目录的物理地址,进入到的二级页表即页目录本身

通过 PTX(va) 计算二级页表索引,访问了页目录项所对应的二级页表的内容

如果PTX(va) = PDX(va) 那么就会访问页目录本身

图片

Thinking 3.2

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

load_icode函数中使用elf_load_seg(ph, binary + ph->p_offset, load_icode_mapper, e) 其中dataenv_create新创建的进程控制块e

dataelf_load_seg 函数中被传入了 map_page 函数、也就是load_icode_mapper 函数,该函数中使用env = (struct Env*)data

没有这个参数不行,因为data原本在kern/env.c文件,它无法在外部文件lib/elfloader.c被使用,必须利用参数传入,使用 void* 类型传递参数,使用时再进行类型强制转换为struct Env*,提高程序灵活性,方便对其它类型的结构体进行操作

Thinking 3.3

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

  • 加载的虚拟地址不与页对齐

    将不对齐的部分先映射到内存中

    1
    2
    3
    4
    5
    6
    7
    u_long offset = va - ROUNDDOWN(va, BY2PG);
    if (offset != 0) {
    if ((r = map_page(data, va, offset, perm, bin, MIN(bin_size, PAGE_SIZE - offset))) !=0) {
    //bin_size 大小可能小于最小对齐的大小
    return r;
    }
    }
  • 数据完整的部分循环加载

    1
    2
    3
    4
    5
    6
    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) {
    //剩余bin_size 大小可能小于一页的大小
    return r;
    }
    }
  • bin_size < sgsize,循环创建新页,但不向其中加载内容

    1
    2
    3
    4
    5
    6
    while (i < sgsize) {
    if ((r = map_page(data, va + i, 0, perm, NULL, MIN(sgsize - i, PAGE_SIZE))) != 0) {
    return r;
    }
    i += PAGE_SIZE;
    }

Thinking 3.4

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

虚拟地址

load_icode()最后将进程控制块中trap framecp0的epc 寄存器的值设置为 ELF 文件中设定的程序入口地址,代表进程恢复运行时 PC 应恢复到的位置,是虚拟地址

Thinking 3.5

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

位于kern/genex.S 文件中

  • 0号handle_int在20-29行
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)
  • 其余通过宏函数 BUILD_HANDLER 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.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

BUILD_HANDLER tlb do_tlb_refill #kern/tlbex.c

#if !defined(LAB) || LAB >= 4
BUILD_HANDLER mod do_tlb_mod #kern/tlbex.c
BUILD_HANDLER sys do_syscall #kern/syscall_all.c
#endif

Thinking 3.6

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

关闭:

  • 异常分发

entry.S文件中,执行.text.exc_gen_entry代码段时,and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)会关闭时钟中断

开启:

  • 由异常分发程序调用 handle_int 函数(kern/genex.S),经过 timer_irq(kern/genex.S)、schedule(kern/sched.c)、env_run(kern/env.c) 、env_pop_tf(kern/env_asm.S )来完成进程调度

    调用 env_pop_tf ,设置新进程的上下文并运行新进程,会执行 j ret_from_exception(kern/genex.S)

1
2
3
FEXPORT(ret_from_exception)
RESTORE_ALL
eret

eretEXL自动设置为0,此时 Status 中的 UMIE 均已被设置为 1,表示在用户模式下且开启中断。之后操作系统可以正常响应中断了。

Thinking 3.7

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

env_pri 是进程的优先级,表示进程会执行几个时钟周期,存入count,当count 减为 0 时,此时分给进程的时间片被用完,执行时钟中断,将当前进程从env_sched_list 移除,执行队首进程

二、实验难点

1.异常重入

若 Status 寄存器的 UM 位为 0,说明此次异常 在内核态触发, sp 寄存器已经在内核异常栈中。

不再将 sp 设置为 KSTACKTOP ,而是使其继续增长。这样我 们便能够在异常中处理新的异常,而不会破坏原本的异常处理流程。

  • 清除 Status 寄存器中的 UM、EXL、IE 位,以保持处理器处于内核态(UM==0)、关闭 中断且允许嵌套异常
  • 取得 Cause 寄存器中的 2~6 位,也就是对应的异常码
  • exception_handlers 数组用于定义异常对应的处理函数,实现异常的分发
  • 跳转到对应的中断处理函数中,从而响应了异常
  • j ret_from_exception
1
2
3
FEXPORT(ret_from_exception)
RESTORE_ALL
eret

2.进程调度

切换进程条件:

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

切换:

  • 如果不是无当前进程,删除env_sched_list里当前进程(?)
  • 当前进程仍为就绪状态时,需要将其移到 env_sched_list 队列的尾部(?)
  • 选中 env_sched_list 队列头部的进程。如果没有可用的进程,内核 panic
  • 设置 count 为当前进程的优先级(分配的时间片的数量)

最后将 count 自减 1,调用 env_run 函数

关于(?)我和同学展开了真理标准问题大讨论,咨询助教之后

认为写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(yield || count <= 0 || e == NULL || e->env_status != ENV_RUNNABLE){
if(e != NULL){
//not here TAILQ_REMOVE(&env_sched_list, e, env_sched_link);
if(e->env_status == ENV_RUNNABLE){
TAILQ_REMOVE(&env_sched_list, e, env_sched_link);
TAILQ_INSERT_TAIL(&env_sched_list, e, env_sched_link);
}
}
if(TAILQ_EMPTY(&env_sched_list)){
panic("schedule: no runnable envs");
}
e = TAILQ_FIRST(&env_sched_list);

count = e->env_pri;
}

count--;
env_run(e);

因为看了lab4之后,如果一个进程的状态设置成ENV_NOT_RUNNABLE,它立马被移除TAILQ队列,如果按照原来的写法,它会被重复移除两次,造成错误

3.新增异常处理

注册 handle 函数

  • kern/genex.S 中,增加处理 xx 的 handle_xx ,可以命名其异常处理函数名为 do_xx
1
BUILD_HANDLER xx do_xx

更新异常向量组

  • kern/traps.c 中,更新异常向量:
1
2
3
4
5
6
7
8
9
10
void (*exception_handlers[32])(void) = {
[0 ... 31] = handle_reserved,
[0] = handle_int,
[2 ... 3] = handle_tlb,
[xx] = handle_xx, // 支持 handle_ov 以处理 ov
#if !defined(LAB) LAB >= 4
[1] = handle_mod,
[8] = handle_sys,
#endif
};
  • kern/traps.c 中,新增处理方法void do_xx()

三、实验体会

夜难寐,exame狼狈通过,extra爆0,作于学M食堂

我自认为反复学习Lab3两遍,已经对进程有了足够的了解,但周三晚上的上机狠狠击碎了我的幻想,我反思之后,认为自己还缺乏以下三点:

  • 限时训练:在高度紧张的限时测试中,如何精准提取题干的关键信息,这是我缺乏的方法。因为平时总是碎片化学习os,缺乏完整思考的能力。
  • 缺乏实践:对于实验代码的思考与理解,只停留在脑海里。如何新增异常处理的过程复习到了,但只是在脑海里过了一遍,没有想到实际上机写起来会有这么多bug
  • 理解测评:没有认真研究测评机的运作逻辑,pre_env_run是什么,为什么不能修改,测试点在哪里找等问题从前完全没有思考过

秉持着菜就多练的原则,lab4的学习与上机一定不会这么狼狈了!!!

今当写lab4,临表涕零,不知所言