BUAA-OS-2023-Lab3-Report

Thinking 3.1

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

根据自映射机制可知, e->env_pgdir[PDX(UVPT)] 指的是进程块 e指向用户页表的页目录项,又根据页表映射的要求,这一项的内容应该是用户页表的物理地址,即PADDR(e->env_pgdir) ,再为其置有效位为1,初始化用户页表

Thinking 3.2

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

  • elf_load_seg() 函数定义位于 lib/elfloader.c 中;使用仅有一处,位于 kern/env.cload_icode() 函数中

首先这里的回调函数是定义在 kern/env.cload_icode_mapper() 函数,作用是把一段虚拟地址的内容加载某个进程管理块对应进程的虚拟内存中(通过申请物理页面并建立页表映射)。这两个函数中的 data 参数相同,其来源是待加载的进程管理块指针,它用来告知 load_icode_mapper() 函数加载到哪个进程。所以不可以没有这个参数,如果没有这个参数,将无法确定加载到哪个进程的虚拟内存中。

elf_load_seg() 中使用回调函数,可以令用户自定义加载段中各个待加载页面的方式

Thinking 3.3

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

考虑情况如下:

  • 段起始地址未页面对齐
u_long offset = va - ROUNDDOWN(va, BY2PG);
if (offset != 0) {
if ((r = map_page(data, va, offset, perm, bin, MIN(bin_size, BY2PG - offset))) !=
0) {
return r;
}
}
  • 复制段大小不足一个页面
map_page(data, va, offset, perm, bin, MIN(bin_size, BY2PG - offset))
// MIN(bin_size, BY2PG - offset) 选择了待复制段与页面偏移后
// 剩余空间中更小的值进行复制,避免复制不必要的内容
  • .bss 段预留空间:文件大小与程序大小不同需要补充空页面
while (i < sgsize) {
if ((r = map_page(data, va + i, 0, perm, NULL, MIN(bin_size - i, BY2PG))) != 0) {
return r;
}
i += BY2PG;
}

Thinking 3.4

这里的 env_tf.cp0_epc 字段指示了进程恢复运行时 PC 应恢复到的位置。我们要运行的进程的代码段预先被载入到了内存中,且程序入口为 e_entry,当我们运行进程时,CPU 将自动从 PC 所指的位置开始执行二进制码。

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

显然, PC 值应该在程序运行过程中保持连续,而程序运行时的物理地址应该是经过页表映射后不连续的,虚拟地址可以则保证其连续性。所以这里的 env_tf.cp0_epc 应该保存的是虚拟地址

Thinking 3.5

异常向量组中,通过把相应处理函数的地址填到对应数组项中,我们初始化了如下异常: 0 号异常的处理函数为 handle_int ,表示中断,由时钟中断、控制台中断等中断造成 1 号异常的处理函数为 handle_mod ,表示存储异常,进行存储操作时该页被标记为只读 2 号异常的处理函数为 handle_tlb ,表示 TLB load 异常 3 号异常的处理函数为 handle_tlb ,表示 TLB store 异常 8 号异常的处理函数为 handle_sys ,表示系统调用,用户进程通过执行 syscall 指令陷 入内核

  • 试找出上述 5 个异常处理函数的具体实现位置

kern/genex.S 文件中,我们可以先找到其上声明的 handle_int 函数:

NESTED(handle_int, TF_SIZE, zero)# handle_int 实现
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
END(handle_int)

其余的四个函数通过同文件的宏 BUILD_HANDLER exception handler 定义在其后:

.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# do_tlb_refill 实现于 kern/tlbex.c

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

BUILD_HANDLER reserved do_reserved# do_reserved 实现于 kern/traps.c

Thinking 3.6

  • 阅读 init.c、kclock.S、env_asm.S 和 genex.S 这几个文件,并尝试说出 enable_irqtimer_irq 中每行汇编代码的作用

enable_irq

LEAF(enable_irq)
li t0, (STATUS_CU0 STATUS_IM4 STATUS_IEc) # 允许用户态使用 CP0 寄存器 + 允许4号中断响应 +
# 开启当前状态CPU中断
mtc0 t0, CP0_STATUS # 将 CPU 状态保存在 CP0 的 STATUS 寄存器
jr ra # 返回调用者函数
END(enable_irq)

timer_irq

timer_irq:
sw zero, (KSEG1 DEV_RTC_ADDRESS DEV_RTC_INTERRUPT_ACK)
# 将 GXemul 时钟响应位置 0
li a0, 0 # 令 schedule 参数 yield = 0
j schedule # 进入 schedule 函数进行进程块队列调度

Thinking 3.7

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

首先,我们在kern/kclock.S 中的kclock_init 函数完成了时钟中断的初始化,并在 genex.S 中的 enable_irq 中设置允许响应该中断

当时钟计时归零(时间片耗尽),产生时钟中断,进入异常处理程序,并跳转到 handle_int 处理中断。

当前我们的系统只能处理 timer_irq 一种时钟中断,所以直接进入 timer_irq 函数,恢复时钟,并执行 schedule(0);

schedule() 中,我们根据当前进程块状态进行进程块队列调度,实现进程切换或进程的舍弃,至此完成时钟中断响应