BUAA-OS-lab4-系统调用与ipc与fork
发表于:2023-06-10 | 分类: BUAA操作系统
字数统计: 2.8k | 阅读时长: 10分钟 | 阅读量:

BUAA-OS lab4实验报告,关于mos系统的系统调用的实现,与ipc的实现,写时复制fork的实现。

lab4_log

实验目标

  • 系统调用的概念及流程。
  • 实现进程间的通信。
  • 实现fork函数。
  • 页写入异常的处理流程。

主要关注文件

  • user/lib/syscall_lib.c 在用户空间中,syscall_*是最接近内核的函数
  • user/lib/syscall_wrap.S mysyscall函数
  • kern/syscall_all.c 包含系统调用的所有操作,sys_*函数与上文中的syscall_*一一对应

Thinking

Thinking 4.1

Thinking 4.1 思考并回答下面的问题:
• 内核在保存现场的时候是如何避免破坏通用寄存器的?
• 系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用 msyscall留下的信息吗?
• 我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?
• 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?

  • SAVE_ALL(include/stackframe.h),将所有的通用寄存器保存进PCB中。
  • 当时的a0-a3寄存器的参数可能并不是传入的参数了,因为在异常分发函数时就保存进PCB了。但是一般没有被破坏,所以还是可以的。
  • 通过tf结构体中a0寄存器的值,即syscall_*函数传入的系统调用号,在func指针数组中取出函数头。然后将其他传入的参数进一步传入到sys_*函数中。
  • 改变了reg[2]即v0寄存器的值,就是系统调用函数的返回值,这个返回值可以标志系统调用是否成功,不成功就输出相应的错误码。

Thinking 4.2

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

  • 我们可以看到,在mkenvid中,envid的生成是通过asid与在envs中的偏移地址得到的。
  • 所以有可能envid越界的情况,所以需要判断申请到的env的id是不是等于请求的envid。

Thinking 4.3

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

  • mkenvid函数不会返回0,所以不会有进程的envid为0,但是通过envid2env的实现我们可以看到若传入的envid参数为0,我们直接返回当前进程。
  • 而ipc中或其他函数中有时候需要获取当前进程的PCB,所以使用0这一位可以很好的索引。

Thinking 4.4

Thinking 4.4 关于 fork 函数的两个返回值,下面说法正确的是:
A、fork 在父进程中被调用两次,产生两个返回值
B、fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

  • C,fork函数的调用只发生在父进程中,并通过调用syscall_exofork函数创建了进程。此时,内核给两个进程设置了不同的返回值,父进程中返回子进程的id,子进程返回0。

Thinking 4.5

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

  • 首先肯定映射用户空间的页面,而且应该映射有效的页(PTE_V)。
  • 然后就是用户空间中ULIM到USTACKTOP之间的部分,储存的是页表信息,异常处理栈等,或是已经完成了拷贝的(页表),或是父子进程共享的(异常处理栈),或是都不需要用到的。所以不需要映射。
  • 只需要映射USTACKTOP以下的区域。

Thinking 4.6

Thinking 4.6 在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参考 user/include/lib.h 中的相关定义,思考并回答这几个问题:
• vpt 和 vpd 的作用是什么?怎样使用它们?
• 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
• 它们是如何体现自映射设计的?
• 进程能够通过这种方式来修改自己的页表项吗?

  • vpt与vpd分别是用户页表与用户页目录的地址,当作数组使用就好啦
  • 在lib.h文件中我们看到实现为:
1
2
#define vpt ((volatile Pte *)UVPT)
#define vpd ((volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))

vpd为自映射机制的寻址。

  • vpd为vpt地址的高10位置到中间10位实现的,等价于UVPT | (UVPT >> 10)
  • 不能,页表项是内核下维护的,只能在内核态修改。内核为进程提供了内存访问的接口,进程并不能直接修改页表项。

Thinking 4.7

Thinking 4.7 在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe运行现场的过程,请思考并回答这几个问题:
• 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?
• 内核为什么需要将异常的现场 Trapframe 复制到用户空间?

  • 因为mos的缺页异常处理是自定义的,通过sys_set_tlb_mod_entry实现指定处理函数的目的,就算这里的实现暂时没有异常重入的现象,也很难保证后续自定义的异常处理函数中会不会写PTE_COW页造成重入。
  • 由于微内核结构,异常的处理是自定义的,所以在用户态进行,用户态访问不到内核空间,所以需要将现场保存在用户空间。

Thinking 4.8

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

  • 这里也是微内核结构的优点,介绍了内核出错的可能,防止整个系统的崩溃。

Thinking 4.9

Thinking 4.9 请思考并回答以下几个问题:
• 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
• 如果放置在写时复制保护机制完成之后会有怎样的效果?

  • 因为这个写时复制的异常处理在父进程与子进程中都需要设置,先设置再执行syscall_exofork函数,这样,父子进程中的tlb_mod异常出口都设置完毕。
  • 我们看到父进程在执行完syscall_exofork后就返回了,等到写时复制机制完成后再设置的话,父进程的异常处理就不是写时复制了。
1
2
3
4
5
child = syscall_exofork();
if (child == 0) {
env = envs + ENVX(syscall_getenvid());
return 0;
}

Exercise

Exercise 4.1-4.2

  • 完成系统调用陷入内核的基本操作
  • 系统调用的步骤就是:
    • 先在用户空间中调用syscall_*的函数,这个函数在用户空间中,是一个库函数(user/lib/syscall_lib.c)。
    • 然后它调用msyscall函数(user/lib/syscall_wrap.S),也在用户空间中,并且传入系统调用号参数(在include/sys call.h中有定义)及其他不定长的参数。
    • msyscall调用syscall诱发系统异常陷入内核态。
  • 然后就到了lab3中系统异常的部分。
    • 先是到了异常分发函数(kern/entry.S)。
    • 然后按照Cause寄存器的异常码到了handle_sys(kern/genex.S)函数。
  • handle_sys调用do_syscall(kern/syscall_all.c)进行系统调用的进一步分发。

Exercise 4.3-4.7

  • 这几个训练需要完成一系列的系统调用函数。
  • 首先就是完成envid2env的函数,这个函数在几乎在所有的函数中都需要用到,因为传递参数的时候都只传递了一个进程的id。
    • 若传入envid==0,则直接返回当前进程。
    • 不然可以通过ENVX(envid)宏来获取index。
    • 但是后续还需执行一系列的检查。
      • 特别注意的时checkperm这一个参数,在不为零时需要检查取出的进程应该时本进程或是本进程的子进程(可以通过parent_id来判断哦),不然报错-E_BAD_ENV。
        • 这一点的考虑其实是多数的系统调用一般都只能在子进程中或此进程中调用,如果调飞了就不好了
        • 但是注意后续的ipc通信就是个不需要checkperm的特例了,通信不应局限在自己家,

Exercise 4.8

  • 实现IPC进程通过陷入到内核中与其他进程通信的行为。
  • 进程间通信是通过修改PCB里的东西来实现的。
  • 对于页面的操作使用pmap.c中的封装好的函数来实现。

Exercise 4.9-4.15

  • 实现fork
  • 实现sys_exofork来区分父子进程的返回值
    • 进程上下文是以指针形式压栈还是数据形式
  • 写时复制页面的准备
  • duppage将需要与子进程共享的页面映射给子进程的页表,并设置页表项的权限位置位
  • 写时复制会触发页写入异常,handle_mod,但是它没有写时复制的机制。由于MOS的微内核设计理念,这个功能我们可以实现在用户空间中。
    • 在do_tlb_mod中我们可以设置一个地址储存异常处理函数,即将cp0_epc改变到一个固定地址,这样我们就能通过修改这个地址的值达到自定义异常处理函数的目的。
    • 通过sys_set_tlb_mod_entry设置返回地址为cow_entry,do_tlb_mod处理时返回。
    • cow_entry用户态的处理函数,作用是把va映射到一个新的物理页面。然后来修改。
  • sys_set_env_status,这这个是fork的最后一步,设置子进程的可运行状态。

实验体会

  • lab4这一章的内容还是很多的。
  • 完成了系统调用,知道了陪伴我们多年的STL背后的那个男人,在内核和用户态的反复横跳中明白了操作系统这个中介十分的不好干。
  • 然后就是在此基础上完成了ipc与fork这两个系统调用的示例叭,特别是fork,搭配上写时复制的机制,使得原本的问题复杂了不少。
  • 虽说过程比较的艰难,但是最后做完之后再整理的过程中看到整个系统每个部分的紧密协同,还是有比较大的成就感的!
上一篇:
BUAA-OS-lab5-文件系统
下一篇:
BUAA-OS-lab3-进程与调度