BUAA_OS_Lab4实验报告

思考题

Thinking 4.1

image-20220514204748966

  • 调用宏函数SAVE_ALL来保存现场,在此过程中,仅仅操作了k0、k1两个寄存器。这两个寄存器专为OS/异常处理保留,程序中不会使用到。由此避免破坏通用寄存器。
  • 可以,因为调用msyscall()时前4个函数参数都存到了$a0 ~ $a3中,使用syscall陷入内核时没有破坏这4个寄存器的值
  • 陷入内核时$a0 ~ $a3的值没有被破坏,而其它的函数参数会被复制到内核栈中(KERNEL_SP),此时可以认为获取了用户调用msyscall时传入的参数
    • 在具体处理函数(sys_*)之前,计算了正确的epc值并存入栈中,使得用户程序可以回到正确的位置接着执行。
    • 在系统调用后,将v0寄存器中的值存入Trapframe中,使得用户程序退出内核态时可以得到正确的系统调用返回值。

Thinking 4.2

image-20220515210239646

mkenvid()中,envid的第10位一定是1,保证了envid != 0始终成立。

u_int mkenvid(struct Env *e) { // 函数实现了为进程控制块e生成env_id的功能
    u_int idx = e - envs;
    u_int asid = asid_alloc();
    return (asid << (1 + LOG2NENV)) | (1 << LOG2NENV) | idx;
}

envid2env()中,当传入的envid == 0时,会返回当前进程的控制块。

借助这个特性,可以很方便的通过envid2env()获取指向当前进程控制块的指针,然后借助这个指针实现对进程控制块的访问。

对于一些需要对当前进程控制块进行访问的程序来说,这个特性大大简化了程序,十分方便。

Thinking 4.3

image-20220515211430222

  • 子进程完全共享了父进程的代码段。
  • 子进程的初始pc被设置成了父进程的epc。
    • 准确的说是在sys_env_alloc()中设置了子进程的tf.pc = tf.cp0_epc,这样当子进程被唤醒后,就会从epc开始执行。
    • 此时的epc也就是fork()中的syscall_env_alloc()(准确的说是msyscall(),因为syscall_env_alloc()是内联函数):在fork()中调用了syscall_env_alloc(),随后陷入内核,调用sys_env_alloc(),所以sys_env_alloc()中的epc为syscall_env_alloc()处。

Thinking 4.4

image-20220515211441155

image-20220515211454017

答案为C。子进程在fork()中通过函数syscall_env_alloc()被创建,随后子进程会从syscall_env_alloc()(实际是msyscall返回的位置)后的下一句开始执行,因此子进程仅仅只在父进程中被调用了一次。通过判断sys_env_alloc()的返回值,明确当前进程的父子身份从而产生不同的返回值。

Thinking 4.5

image-20220515211523319

\(UTOP\)及以上的空间在env_setup_vm()中就已经完成了映射。

\(USTACKTOP \sim UTOP\)之间为Invalid memory和user exception stack,无需映射。

因此,只需要映射\(USTACKTOP\)以下的内存空间。

Thinking 4.6

image-20220515211532208

  • vpt和vpd分别指向了用户页表和用户页目录,可以借助这两个指针实现对用户页表和用户页目录的访问。在使用时,以vpt为例,想要获取当前虚拟地址va所对应的页表项,可以((Pte *)(*vpt))[VPN(va)] 。首先(*vpt)获取页表的地址,然后(Pte *)进行类型转换,接着通过宏VPN(va)获取虚拟地址va对应的页面的编号,即相对(*vpt)的偏移量,通过这个偏移量就可以得到对应的页表项了。
  • 实现角度:
    • 在MOS中,存储页表的空间属于用户空间,因此用户进程所以可以通过一个指针取得页表的地址来进行访问。
    • 系统是线性地进行页面的映射地,因此可以很方便的实现“虚拟地址->虚拟页号”的转变。
    • 虚拟页号也就是 虚拟地址对应的虚拟页面 相对页表项的偏移量,获得了页表首地址和偏移量后,就可以获取对应的页表项了。
  • vpd的值为(UVPT+(UVPT>>12)*4),意味以UVPT为基地址,向偏移(UVPT>>12)项,得到的也就是UVPT对应的页表项。将其作为vpd看待,即认为这一页映射了整个页表,可作为页目录。说明页表中存在某一页映射了整个页表,由此体现了自映射。
  • 不可以,进程只可读页表项,不能进行修改。页表的维护由OS负责。

Thinking 4.7

image-20220516113857538

  • 当发生其它中断时发生缺页中断的话,就会进行中断重入
  • 因为“真正”处理异常的函数是在用户态下运行的,此时只能访问用户空间,所以要将异常的现场复制到用户空间。同时,这样子也能够保存现场,防止其被破坏。

Thinking 4.8

image-20220516113914924

  • 减少了内核出错的可能,即使程序崩溃,也不会影响系统的稳定。
  • 通用寄存器使用的CPU处理的。只要在异常处理前将其全部保存,在异常处理完后将其全部恢复,对于正常处理程序的CPU而言,就没有破坏现场中的通用寄存器。

Thinking 4.9

image-20220516113928316

  • 在syscall_env_alloc中,会复制父进程的进程栈空间,在这个过程中,set_pgfault_handler()所设置的env_pgfault_handler和env_xstacktop都会一并被复制。
  • set_pgfault_handler()本就是用于处理写时复制保护机制带来的错误的,如果放在其后,写时保护机制将不能正确运行。
  • 不需要,子进程只需要与父进程保持一致就可以了。父进程调用set_pgfault_handler()后,__pgfault_handler就已经被设置好,当建立写时复制保护机制时,__pgfault_handler会被共享,因此无需赋值。

实验难点展示

上机部分

  • lab4-1-exam:相对不难,其中那个锁的位置值得思考,在本次上机实验中我选择使用全局变量保存锁,其值为持有进程的id。

  • lab4-1-extra:实现起来复杂度远高于exam,但只要理解了ipc和系统调用,实现起来难度不大,我的大体实现思路如下:

    • 选择开一个结构体数组记录每次信息发送的相关值和一个记录是否完成的标记。
    • 接收进程:首先查表,有无自己可以接受的信息,有的话就接收,设置发送进程状态为RUNNABLE 并正常退出,否则阻塞。
    • 发送进程:检查接收进程的状态,若阻塞,直接进程信息发送同时设置接收进程状态为RUNNABLE。若接收进程没有阻塞,将待发送的信息添加到信息表中,阻塞。
  • lab4-2-exam:总体简单。

  • lab4-2-extra:珍爱头发,选择放弃

课下部分

lab4前半部分难度并不大,在充分理解了lab3中异常的处理流程后会发现其并不难,主要是要熟练的使用前几个实验中实现的多个函数。

本次lab的难点只要集中在fork的实现上,尤其是页写入异常处理机制的建立以及页写入异常的处理流程,由于涉及到了多个函数,且有不少汇编代码,所以个人认为理解起来难度不小。
另外,指导书所说写的很清晰,但是由于是平直的叙述,缺少框架性描述,也给理解带来了一些困难。
此外,这一部分的命名也很谜,很让人摸不着头脑。尤其是页写入异常那一块,被命名整麻了。

为了加深理解,同时也是为了弄清楚整个lab的逻辑结构,我将本次实验中的流程及难点整理成了思维导图:

系统调用

lab4学习1

IPC

lab4学习2

FORK

lab4学习3

image-20220527204104837

体会与感想

本次实验中,我明显感觉到了自己汇编的底子太差,好多汇编函数根本看不懂,而且还会下意识地回避阅读,导致在理解页写入异常的时候很难受,因为不阅读__asm_pgfault_handler就不能理解几个函数间的关系,但是不读又看不懂。这样子拖了两天才完成lab4-2。同时,这次实验中有很多相似的函数名,我做的时候会感觉有点烦闷,导致进度缓慢,在未来的实验中需要更加沉下心来才行。

lab4让我学到了很多有关用户进程的知识,也让我实操了系统调用,加深了对用户态和内核态的理解,感觉收获很大。当然指导书的描述能够更层次化一些就更好了

posted @ 2022-06-02 11:04  tantor  阅读(190)  评论(0编辑  收藏  举报