os-lab4

本文最后更新于 2024年9月6日 下午

1.思考题

4.1

  1. 内核在保存现场的时候是如何避免破坏通用寄存器的?

保存现场过程中,先通过k0寄存器暂存sp栈指针的值,之后将其余通用寄存器的值直接存到cp的协寄存器中。

  1. 系统陷入内核调用后可以直接从当时的`$a0-$a3`参数寄存器中得到用户调用`msyscall`留下的信息吗?

可以。从用户函数syscall_*()到内核函数sys_*()时,$a1-$a3未改变,$a0handle_sys()的时候被修改为内核函数的地址,但在内核函数sys_*()仅为占位符,不会被用到。

  1. 我们是怎么做到让sys开头的函数“认为”我们提供了和用户调用msyscall时同样的参数的?

用户调用时的参数:用户进程的寄存器现场(保存在了内核栈的TF_5-TF_7)的$a1-$a3;用户栈的参数$a4$a5
把上面两部分参数分别拷贝至arg1-arg5,在sys函数中作为参数传入。

  1. 内核处理系统调用的过程对Trapframe做了哪些更改?这种修改对应的用户态的变化是什么?

Trapframe结构体中的cp0_epc的值增加了4,并将sys开头函数的返回值存入v0寄存器,保证了系统调用后用户态进程可以获得正确的返回值,并且从触发系统调用的下一条指令继续执行。

4.2
判断e->env_id != envid的情况是为了确保所请求的envid与找到的环境的实际ID匹配。如果没有这个检查,可能将错误的env分配给*penv

4.3

1
2
3
4
5
6
7
8
9
10
if(!envid){
*penv=curenv;
return 0;
}else{
e=&envs[ENVX(envid)];
}
if (e->env_status == ENV_FREE || e->env_id != envid) {
return -E_BAD_ENV;
}

envid2env函数中可以看出,curenv分配的envid为0,因此,0用来作为目前运行进程的特判符。所以在给env分配envid时,使用的mkenvid返回值不能为0。

4.4

C

4.5

USTACKTOP以下的应该映射。
但是UTEMP之下的invalid memory 是为处理页写入异常时做缓冲区用的,不需要共享。
UTOP以上页面的内存与页表是所有进程共享的,且用户进程无权限访问,不需要做父子进程间的duppage

4.6

  • vpt和vpd分别代表页表项数组和页目录项数组,可以使用vpt[index]vpd[index]的方式去访问页表项和页目录项。
  • 由于用户进程下的系统调用的虚拟内存管理的函数传入的pgdir均为env结构体的kuseg的进程页目录,并且env_setup_vm()时把页目录进行自映射e->env_pgdir[PDX(UVPT)] = e->env_cr3,所以实现了用户进程的虚拟内存管理,可以通过两级页表机制访问。
    -参考vpt和vpd的定义代码:
1
2
#define vpt ((const volatile Pte *)UVPT) //它将 UVPT(用户虚拟页表基址)转换为 Pte 类型的指针。
#define vpd ((const volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))//虚拟页目录基址转换为页目录项

其中vpd的定义体现了页目录自映射。

-可以,因为在pmap.c中实现的虚拟内存机制,给页表项和页目录项的perm均为可写。

4.7

  • 注意以下代码:
1
2
3
if (tf->regs[29] < USTACKTOP || tf->regs[29] >= UXSTACKTOP) {
tf->regs[29] = UXSTACKTOP;
}

我们可以看到,这里实现了类似“异常重入”的机制,即sp指针已经位于用户异常栈,即此时已经发生tlb_mod异常时,当再次发生相同的异常,重新将sp指向栈顶,会发生这种“异常重入”。

  • tlb_mod的异常处理比较特殊,真正的核心处理函数为cow_entry(),而这个函数位于用户态空间,也就是说,我们实际是在用户态进行异常处理的核心部分,所以我们需要将异常的现场Trapframe复制到用户空间,方便处理完异常后恢复现场。

4.8

解放内核,不用内核执行大量的页面拷贝工作。同时符合MIPS4KC的微内核设计,将一些功能放到用户空间实现,提高了内核的稳定性和可移植性。

4.9

  • 第一次syscall_set_tlb_mod_entry是针对父进程设置的。子进程 RUNNABLE 后会从 syscall_exo_fork 逐级调用的 syscall 指令之后开始执行。如果在 syscall_exo_fork 之后再 syscall_set_tlb_mod_entry,那么子进程也会执行这个系统调用。

  • 父进程运行时在函数调用等情形下会修改栈。在栈空间的页面标记为写时复制之后,父进程继续运行并修改栈,就会触发 TLB Mod 异常。所以在写时复制保护机制完成之前就需要 syscall_set_tlb_mod_entry

2.难点分析

  1. 系统调用
    对于系统调用中几个函数调用顺序的理解。

    系统调用本身是为了陷入内核态,因为一些操作只能在内核态实现。
    其中从用户态到内核态的函数传参较难理解。
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
void do_syscall(struct Trapframe *tf) {
int (*func)(u_int, u_int, u_int, u_int, u_int);
int sysno = tf->regs[4];
if (sysno < 0 || sysno >= MAX_SYSNO) {
tf->regs[2] = -E_NO_SYS;
return;
}

/* Step 1: Add the EPC in 'tf' by a word (size of an instruction). */
/* Exercise 4.2: Your code here. (1/4) */
tf->cp0_epc+=4;
/* Step 2: Use 'sysno' to get 'func' from 'syscall_table'. */
/* Exercise 4.2: Your code here. (2/4) */
func=syscall_table[sysno];
/* Step 3: First 3 args are stored in $a1, $a2, $a3. */
u_int arg1 = tf->regs[5];
u_int arg2 = tf->regs[6];
u_int arg3 = tf->regs[7];

/* Step 4: Last 2 args are stored in stack at [$sp + 16 bytes], [$sp + 20 bytes]. */
u_int arg4, arg5;
/* Exercise 4.2: Your code here. (3/4) */
arg4=*(u_int *)(tf->regs[29]+16);
arg5=*(u_int *)(tf->regs[29]+20);
/* Step 5: Invoke 'func' with retrieved arguments and store its return value to $v0 in 'tf'.
*/
/* Exercise 4.2: Your code here. (4/4) */
tf->regs[2]=func(arg1,arg2,arg3,arg4,arg5);
}

我们通过Trapframe结构体保存了用户现场的上下文,以此在内核态进行传参,同时再通过系统调用处理函数表跳转到具体的调用函数,同时又在tf->regs[2]中,即v0寄存器中保存系统调用的返回值。

  1. 进程间IPC通信

这里的主要难点在于集中了对lab3和lab4前半部分的的综合理解运用,涉及了进程间的切换和系统调用。

按照上述的图示去理解ipc的通信过程,即一个进程先发出ipc_recv的信号之后,再调用schedule(1)切换到另一个进程进行ipc_send,从而实现两个进程间的通信。

  1. fork的实现

首先要理解函数duppage(),这是实现父进程将地址空间中需要与子进程共享的页面映射给子进程。

1
2
3
syscall_mem_map(0, addr, envid, addr, perm);//先将子进程映射到父进程映射的物理页实现共享
if(r)
syscall_mem_map(0,addr,0,addr,perm);//再用新的映射覆盖自己旧的映射改变权限位

同时注意先修改子进程的权限,如果先直接映射自身,那么权限位改为PTE_COW后,无法再对子进程进行映射(会陷入写时异常,还没有实现)

我们还需要注意fork中涉及处理页写入异常,这是一种特殊的异常,异常处理的核心函数cow_entry()是在用户态下调用的。
函数中memcpy((void*)UCOW,(void*)ROUNDDOWN(va,PAGE_SIZE),PAGE_SIZE);复制写时错误页的信息,需要进行向下对齐,本质上是将地址的后12位清0,得到页号。

fork函数在进行映射之前,一定要查询页目录项和页表项是否有效。

1
2
3
4
5
for (i = 0; i < VPN(USTACKTOP); i++) {
if ((vpd[i >> 10] & PTE_V) && (vpt[i] & PTE_V)) {
duppage(child, i);
}
}

fork函数是由父进程进行调用的,但是却有两个返回值,这是因为子进程的返回值:e->env_tf.regs[2]=0;,即保留在v0寄存器中。

3.实验体会

涉及不同进程之间的操作以及系统调用,这部分内容综合了整个前面的lab,难度较大,需要对OS有深刻的认识。

课下需要填写的内容较多,分散在不同的文件,实则上如果从os的涉及来看,各个文件的代码是联系紧密的。

课上上机考察十分细致,需要对整个系统调用的过程和进程之间的ipc的工作过程有清晰的认识,要理解函数之间的调用关系。

要时刻注意区分内核态用户态,这样在补充函数时不会出现一些奇奇怪怪的bug。