BUAA OS Lab4 实验笔记

一、思考题

Thinking 4.1

思考并回答下面的问题:

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

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

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

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

1.内核保存现场使用了include/stackframe.h中的SAVE_ALL宏,其中将除了k0的所有通用寄存器按照顺序放入了栈帧,从而避免了破坏。k0k1寄存器本身就是要被操作系统临时使用的,无需保存。

2.从陷入内核到调用syscall之间没有对内核空间做任何修改,因此可以直接从寄存器中得到信息。

3.从msyscallsys_*之间,栈指针未出现改变,指向同一个上下文;msyscall调用时参数入栈的顺序与放入sys_*函数的传入顺序一致(do_syscall函数)。

4.将epc值加4,从而使得从调用返回用户态时,返回syscall的下一条指令。

Thinking 4.2

思考envid2env 函数:

为什么 envid2env 中需要判断 e->env_id != envid的情况?如果没有这步判断会发生什么情况?

​ 如果一个旧进程被销毁,其进程控制块将被插入env_free_list,申请新进程时,该进程块可能会被重新取出。envid2env函数通过ENVX宏取得目标进程块与envs之间的偏移量,而ENVX即是取envid的末尾10位。mkenvid时,新envid的末尾10位仅由envse的地址偏移决定,因此由于是同一个进程块,地址偏移量一样,假如使用已销毁进程的envid调用该函数,会取出新进程的进程块,这是不允许出现的。e->env_id是新进程id,envid是传入的旧进程id,假如该情况后就杜绝了该问题。

Thinking 4.3

思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与envid2env() 函数的行为进行解释。

mkenvid()函数通过拼接一个非0的整数i与待分配进程块偏移量生成新的envid,这意味着它生成的envid永远不为0。envid2env()函数传入envid为0时,返回的进程块是当前进程的进程块。

IPC是进程间通信,需要在两个不同进程间传递信息。如果envid2env()返回当前进程,IPC相关系统调用函数就会失效(自己向自己传送信息);另一方面,取消envid2env()的返回当前进程功能的话,不便于一部分系统调用(如syscall_set_trapframe)的实现。因此只能这样处理。

Thinking 4.4

关于 fork 函数的两个返回值,下面说法正确的是:

A、fork 在父进程中被调用两次,产生两个返回值

B、fork 在两个进程中分别被调用一次,产生两个不同的返回值

C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值

D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

C

Thinking 4.5

我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.cenv_init 函数进行的页面映射、``include/mmu.h` 里的内存布局图以及本章的后续描述进行思考。

​ 映射 0 ~ USTACKTOP之间的空间。USTACKTOP以上到UXSTACKTOP(UTOP)的空间为异常栈,是保存异常处理信息的位置,无需映射;UXSTACKTOP再向上的空间不在用户空间内,不应该映射。

Thinking 4.6

在遍历地址空间存取页表项时你需要使用到 vpdvpt 这两个指针,请参考 user/include/lib.h 中的相关定义,思考并回答这几个问题:

vptvpd 的作用是什么?怎样使用它们?

• 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?

• 它们是如何体现自映射设计的?

• 进程能够通过这种方式来修改自己的页表项吗?

1.vpt是页表基地址,vpd是页目录基地址,如果想查询虚拟地址所对应的页目录项/页表项,可以使用vpd[页目录项偏移值]/vpt[页表项偏移值]

2.所有进程共用一个页表,不同进程的页面通过不同的asid来区分,故可以直接通过vpt读取自身页表。vpt的基地址在虚拟地址空间是固定的,为UVPT

3.以下是user/include/lib.h对其的定义:

#define vpt ((const volatile Pte *)UVPT)
#define vpd ((const volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT))) //体现自映射设计

4.不可以。页式内存管理对应用程序员透明,修改页表项只能在核心态下进行,必须调用系统调用,陷入内核后进行操作。

Thinking 4.7

do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe运行现场的过程,请思考并回答这几个问题:

• 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?

• 内核为什么需要将异常的现场 Trapframe 复制到用户空间?

1.当在处理页写入异常的中途中,被时钟中断打断,就会出现异常重入(对于 sp 已经在异常栈中的情况,就不再从异常栈顶开始分配栈帧,即实现“异常重入”)。

2.复制到用户空间是因为MOS操作系统按照微内核的设计理念,尽可能地将功能实现在用户空间中,其中也包括了页写入异常的处理,因此主要的处理过程是在用户态下完成的,需要保存到用户空间内的异常栈。在完成异常处理之后,cow_entry要根据该栈中的返回地址来返回异常发生前位置,从而恢复现场。

Thinking 4.8

在用户态处理页写入异常,相比于在内核态处理有什么优势?

​ 微内核设计主张将传统操作系统中的设备驱动、文件系统等可在用户空间实现的功能,移出内核,作为普通的用户程序来实现。这样,即使它们崩溃,也不会影响到整个系统的稳定。事实上,我们的 MOS 操作系统按照微内核的设计理念,尽可能地将功能实现在用户空间中,其中也包括了页写入异常的处理,因此主要的处理过程是在用户态下完成的。这样设计简化了操作系统的复杂度,并且减小处理失误时对操作系统造成的后果,提高了系统的稳定性。

Thinking 4.9

请思考并回答以下几个问题:

• 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?

• 如果放置在写时复制保护机制完成之后会有怎样的效果?

1.syscall_exofork后子进程被创建,父子进程都需要执行syscall_set_tlb_mod_entry,但是父进程已经为子进程写入了异常函数入口位置,所以可以更改位置。如果没有为子进程写入,子进程将不能正确处理异常。

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

二、难点分析

​ 有关系统调用的流程。以debugf这个需要使用系统调用的函数为例。debugf内与系统调用直接相关的函数调用是debug_output,作用是字符串输出,其中又调用了syscall_print_cons,它定义在syscall_lib.c内。这个文件内存储了所有的syscall_*函数,每个函数又都在调用msyscall的同时向其中传递了一个与系统调用功能相关的参数,即系统调用号(在这个例子里,是SYS_print_cons),并同时将字符串等相关数据传入msyscallmsyscall调用syscall汇编,至此用户态部分结束,核心态部分开始。

​ 异常触发时,程序自动跳转到.text.exc_gen_entry段(kern/entry.S),根据异常码在exception_handlers异常向量组(kern/traps.c)中寻找到接下来的地址。对于时钟中断会调转到handle_intkern/genex.S)并调用timer_irq进行时间片调度算法;对于系统调用会跳转到handle_syskern/genex.S),再分发到do_syscallkern/syscall_all.c),它再根据系统调用号转移到sys_print_conskern/syscall_all.c)函数进行字符串中字符的输出,完成后进行结果的逐步返回。

在这里插入图片描述

​ 有关一些特殊标志位。PTE_D标志位为0时,当进程尝试写这个页面,会触发TLB Mod异常。PTE_COW用来区分真正的“只读”页面与“写时复制”页面,写时复制页面的PTE_COW为1。写时复制的页面PTE_D为0,PTE_COW为1。只读页面PTE_D为0,PTE_COW为0。共享页面PTE_LIBRARY为1。正常页面PTE_D为1,PTE_LIBRARY为0,在duppage后需要设置为写时复制页面。

fork函数的流程。

  • 调用syscall_set_tlb_mod_entry系统调用,设置TLB Mod异常的处理函数为cow_entry。当触发TLB Mod异常,异常会被分发到do_tlb_mod(sp进入异常栈,a0存储原上下文信息),结束后回到用户态,跳转至cow_entry进行处理,原上下文传入cow_entrycow_entry为触发异常的页面建立写时共享(复制父进程的相应页面,并申请新页面重新映射),设置了新的权限位,最后通过syscall_set_trapframe恢复现场。
  • 调用syscall_exofork系统调用,创建了子进程,并将父进程的上下文信息复制给了子进程,将其状态设置为NOT_RUNNABLE,返回值设置为0。此时子进程不可运行。
  • 0 ~ USTACKTOP之间的页面使用duppage函数,它的作用是:为需要保护的页面设置写时共享权限,并且将设置权限后的页面共享给父子进程,这样实现了页面共享机制。
  • 设置子进程的 TLB Mod 异常处理函数为 cow_entry
  • 调用 syscall_set_env_status 将子进程状态设定为 RUNNABLE 并将其加入调度队列中。返回子进程的 envid。作为父进程 fork 的返回值。
  • 子进程参与调度,从fork中返回0。

三、实验体会

Lab4的难度明显比之前几个Lab的难度又有提升,完成的耗时也相当长。自感到光凭我自己的理解是无法完成课下任务的,最后求助了同学,并且参阅了往届学长学姐的笔记才完成。系统调用,fork等功能的实现都很复杂,涉及到多个函数的功能组合,还有多个进程之间的配合。当我第一次看完指导书完全是懵的,因为各种函数比较复杂,我并不明白互相的关系以及它们的具体作用。我看了很多遍博客,代码和指导书,边理解边整理边在上面的难点分析中记录,才在脑中对其有了大体的印象,但是细节仍然不太清晰。还需要在操作系统这门课上下更多的功夫。

​ 我对用户栈USTACKTOP等的使用仍然不太理解,包括向陷阱帧中传入a0的具体意义等,希望能在接下来的课程中有进一步理解。

gdb调试器很重要,但是自认为熟练度不足,这是我在未来的学习过程中能够不断提升的能力。

​ 可以预见,Lab4乃至Lab5的上机考试都会有相当的难度,这对我操作系统的学习提出了更高的要求,要对指导书中内容有更深的理解。望自己更加努力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值