Contents

OS-Lab3 实验报告

思考题

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

为了在用户态只读访问页表,我们需要建立自映射。这行代码就是把页目录的UVPT页目录项指向页目录自身的地址,从而建立自映射。

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

data参数接收当前的进程控制块,并作为参数传递给回调函数map_page,用于提供进程上下文。不可以没有这个参数,因为这样页面分配将无法完成。

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

需要处理虚拟地址是否页对齐,是否有.bss段数据需要加载的情况。

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

虚拟地址。

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

位于kern/genex.S,使用汇编实现。

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

时钟中断仅在用户态开启,在内核态关闭。

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

在sched.c中,设置了一个时间片计数器count,并在每次时钟中断时跳转到schedule,在其中count–,直到count==0时切换进程,并将count重置为新进程的时间片。

难点分析

进程调度

进程调度的实现涉及到多个文件之间的跳转,分析起来较为困难,下图粗略地展示了进程调度的流程。

// 初次调用
init.c ---> schedule(0) ---> e == NULL,为其分配进程 
---> env_run(env.c) ---> env_pop_tf(env_asm.S) ---> 
ret_from_exception(genex.S)
// 切换进程
entry.S分发异常 ---> 
加载exception_handlers数组(位于traps.c)中对应函数 ---> 
跳转到该函数(handle_int,genex.S) ---> schedule(O) ---> 时间片用完则切换

值得注意的是,env_run是一个不返回函数,他最终会在ret_from_exception这个汇编函数中eret到进程,这可以解释在课下完成调度算法的扩展题目时,无论在新算法正确调度后是否return,我们都不用担心会被原算法调度污染。

异常分发与重入

异常分发

我们在kernal.lds中增加了下面两行语句,以将异常分发入口放置在正确地址,为我们的操作系统增加异常处理功能:

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

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

现在有了异常发生时我们需要进入的入口,我们就可以追踪下去,看看处理异常时究竟发生了什么。

// entry.S
tlb_miss_entry:
	j       exc_gen_entry

可以看到,tlb_miss_entry所做的事情只有一个,就是跳转到exc_gen_entry。而在exc_gen_entry中,我们所做的事情如下:

// entry.S
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

在相关寄存器状态设置完成后,我们获取了ExcCode异常码,并转到了exception_handlers(ExcCode)对应的位置。exception_handlers是一个函数指针数组,其定义如下:

//traps.c
void (*exception_handlers[32])(void) = {
    [0 ... 31] = handle_reserved,
    [0] = handle_int,
    [2 ... 3] = handle_tlb,
#if !defined(LAB) || LAB >= 4
    [1] = handle_mod,
    [8] = handle_sys,
#endif
};

上述代码使用了GNU C 的拓展语法[first … last] = value 来对数组某个区间上的 元素赋成同一个值。也就是说,exception_handlers是一个32个元素的数组,其中每一个元素都是一个参数和返回值都是void的函数。我们首先把所有元素都初始化为handle_reserved,然后将0号元素设置为handle_int,2、3号元素设置为handle_tlb,以此类推。这样,我们就能跳转到对应异常码的异常处理函数,实现了异常的分发。

异常重入

可以看到,在exec_gen_entry中,第一句话是:

//enrty.S
exc_gen_entry:
	SAVE_ALL

SAVE_ALL的定义如下;

.macro SAVE_ALL
.set noat
.set noreorder
	mfc0    k0, CP0_STATUS
	andi    k0, STATUS_UM
	beqz    k0, 1f
	move    k0, sp
	/*
	* If STATUS_UM is not set, the exception was triggered in kernel mode.
	* $sp is already a kernel stack pointer, we don't need to set it again.
	*/
	li      sp, KSTACKTOP
1:
  // Save regs
  ...
.set at
.set reorder
.endm

可以看到,我们在最开始的时候对状态位做了一个检查。如果当前处于内核态,说明我们是在处理异常的时候又发生了新的异常,由于sp指针已经指向异常栈,我们直接跳转到label 1(即代码中的1f)位置,进行当前寄存器上下文的保存,避免了之前的异常保存的上下文丢失。而如果我们当前处于用户态,则把sp移动到KSTACKTOP,从异常栈的顶部开始保存上下文。这样,我们就实现了异常重入。

实验体会

本次试验最大的体会,就是在进程调度和异常处理的过程中,涉及到大量的内存处理和寄存器操作,极其依赖Lab2的页式管理内容和CO-P7的异常处理的硬件实现,这也就要求我们必然要对这些内容极其熟悉,不然就像我一样上机被Extra肘击
Anyway,五一假期快乐!

原创说明

本片均为原创。