操作系统lab4

lab4实验报告

思考题

Thingking1

  • 内核在保存现场时,通过SAVE_ALL宏实现保存现场,SAVE_ALL`宏具体实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mfc0    k0, CP0_STATUS
    andi k0, STATUS_UM
    beqz k0, 1f
    move k0, sp
    li sp, KSTACKTOP
    1:
    subu sp, sp, TF_SIZE
    sw k0, TF_REG29(sp)
    mfc0 k0, CP0_STATUS
    sw k0, TF_STATUS(sp)
    // TODO...

    阅读上述代码可知,内核将栈指针指向KSTACKTOP然后将各个寄存器的值全部压入栈中,从而将现场的各个寄存器的值存入内核的栈空间中保护起来。

  • 陷入内核后不可以直接从$a0~$a3中获得用户调用msyscall留下的信息,因为调用该函数后寄存器的值可以被修改,但用户空间原本的寄存器值已经被压入栈中保存起来,因此应当从栈中获取。

  • 由于内核在保存现场时将现场的寄存器中全部压入栈中保存起来,同时将栈所指向的结构体指针作为参数传入do_syscall中,从而将前四个参数直接从该结构体中读出,同时读出该结构体中的栈指针$sp,栈指针指向的栈中的第5,第6个字即为后两个参数,实现如下。

    1
    2
    3
    4
    5
    6
    7
    8
    u_int arg1 = tf->regs[5];
    u_int arg2 = tf->regs[6];
    u_int arg3 = tf->regs[7];
    u_int arg4, arg5;
    u_int *sp;
    sp = (u_int *)tf->regs[29];
    arg4 = *(sp+4);
    arg5 = *(sp+5);
  • 设置cp0中epc寄存器的值,使异常处理结束后能够返回到正确的位置继续执行用户程序。

Thingking2

阅读env.c中的envid2env函数,可发现其获取进程控制块的方法为e = &envs[ENVX(envid)],其中ENVX(envid)宏定义为#define ENVX(envid) ((envid) & (NENV - 1)),可知其简单获取envid的前十位作为数组下标进行访问,阅读env.c中的mkenvid函数,其中关键代码为return ((++i) << (1 + LOG2NENV)) | (e - envs);,由此可知,如果新老进程使用同一个进程控制块,其envid的前10位相同,因此envid2env函数中访问到的进程控制块相同,但是此时进程控制块中保存的进程id已经改变,如果不判断的话可能导致得到错误的进程。

Thingking3

envid2env()函数有如下代码:

1
2
3
4
if (envid == 0) {
*penv = curenv;
return 0;
}

可知,从id=0表示当前进程,这一操作使得获取当前进程的方法十分便捷,而为了防止id=0指向两个进程(即一个为当前进程,应该为真实id=0的进程),需要保证mkenvid()函数返回值不为0,使所有进程的真实id均不为0。

Thingking4

fork函数只在父进程中被调用一次,但是在父子进程中有两个不同的返回值。

Thingking5

env_init()有如下代码:

1
2
map_segment(base_pgdir, 0, PADDR(pages), UPAGES,ROUND(npage * sizeof(struct Page), PAGE_SIZE), PTE_G);
map_segment(base_pgdir, 0, PADDR(envs), UENVS, ROUND(NENV * sizeof(struct Env),PAGE_SIZE),PTE_G);

可知,虚拟内存中\(UTOP - UPAGES\)之间的进程控制块部分和\(UPAGES - UVPT\)之间的页数组的部分已经在env_init()中完成了映射,而在往上的用户页表,内核地址区等不需要映射,UTOP以下的用户异常栈等部分不需要映射,因此只需要映射USTACKTOP以下的部分。

Thingking6

实现如下:

1
2
#define vpt ((const volatile Pte *)UVPT) 
#define vpd ((const volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))
  • 由实现可知,vpt为页表起始地址,vpd为页目录起始地址,通过给定的页表项虚拟地址,使用vpt[VPN(va)]即可访问虚拟地址为va的页表项内容,使用vpd[PDX(va)]即可访问虚拟地址为va的页目录项内容。

  • 虚拟地址空间是连续的,因此只需要给定页表起始地址和虚拟地址就可以直接找到对应的页表项,从而存取页表。

  • 观察vpd的定义,可知\(vpd = UVPT + UVPT>>10\),由此体现了页目录自映射。

  • 进程不应当使用此方法修改页表,页表应该由内核修改。

Thingking7

  • 在处理tlb_mod异常时,可能出现缺页的情况,此时需要处理缺页异常,出现异常重入。

  • 因为处理tlb_mod异常是在用户态进行的,将异常现场的各个寄存器交给用户空间使其在用户态可以进行异常处理。

Thingking8

在写入异常时,在用户态如果产生看缺页异常等其他异常,则可以陷入内核进行处理,而在内核态则需要其他方法进行处理。

Thingking9

  • 设置子进程的写入异常处理入口,防止子进程处理写时复制机制时出错。

  • 由于未设置,写入异常处理入口,可能导致写入异常处理失败。

难点分析

本次实验主要难点为判断那些内存使用duppage复制到子进程。

部分内存结构图如下:

1
2
3
4
5
o  UTOP,UENVS   -----> +----------------------------+------------0x7f40 0000    |
o UXSTACKTOP -/ | user exception stack | PTMAP |
o +----------------------------+------------0x7f3f f000 |
o | | PTMAP |
o USTACKTOP ----> +----------------------------+------------0x7f3f e000

由前面的分析我们知道需要复制的部分为USTACKTOP以下的所有虚拟地址空间,但是我们注意到,\(USATCTOP = 0x7f37e000\)\(PDX(USTACKTOP) = 0x1fc\),因此外层循环如果终止条件不取\(USATCKTOP\)(即使用i<PDX(USATCKTOP)而不是i<=PDX(USATCKTOP))最大虚拟地址为\(0x7f000000\),则两层循环最大遍历到的虚拟地址为\(0x7efff000\),因此无法将USTACKTOP以下的所有虚拟地址空间均完全复制,因此采用如下的循环结构进行复制操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (i = 0;i<=PDX(USTACKTOP);i++){
if (vpd[i]&PTE_V) {
for (u_int j = 0;j<PAGE_SIZE/sizeof(Pte);j++) {
u_long va = (i * (PAGE_SIZE / sizeof(Pte)) + j) << PGSHIFT;
if (va>=USTACKTOP) {
break;
}
if (vpt[VPN(va)]&PTE_V) {
duppage(child,VPN(va));
}
}
}
}

实验心得

本次实验主要难点在于理解fork()的实现,其余部分指导书的表述均较为清晰,但是fork部分由于代码注释中的“below”表述,以及初学阶段对于哪些虚拟地址空间需要复制子进程的理解较为模糊,导致上述代码中第一层for循环的条件有误,找了很久的bug。