OS

OS lab6

Posted by Steel Shadow on June 8, 2023

lab6 实验报告

思考题

  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
    31
    32
    33
    
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
    
     int fildes[2];
     char buf[100];
     int status;
    
     int main() {
         status = pipe(fildes);
    
         if (status == -1) {
             printf("error\n");
         }
    
         switch (fork()) {
             case -1:
                 break;
    
             case 0:               /* 子进程 */
                 close(fildes[0]); /* 关闭不用的写端 */
                 write(fildes[1], "Hello world\n", 12); /* 向管道中写数据 */
                 close(fildes[1]); 
                 exit(EXIT_SUCCESS);
    
             default:                       /* 父进程*/
                 close(fildes[1]);         
                 read(fildes[0], buf, 100); /* 从管道中读数据 */
                 printf("father-process read:%s", buf); /* 打印读到的数据 */
                 close(fildes[0]); 
                 exit(EXIT_SUCCESS);
         }
     }
    
  2. 上面这种不同步修改 pp_ref 而导致的进程竞争问题在 user/lib/fd.c 中的 dup 函数中也存在。请结合代码模仿上述情景,分析一下我们的 dup 函数中为什么会出现预想之外的情况?

    在 dup() 中,在两次的 syscall_mem_map 之间 可能发生 时间片结束 -> 进程切换。导致 pageref(pipe) 和 pageref(p[0/1]) 与预期不一致,导致错误发生。

    具体分析见 tests/lab6_1/testpiperace.c 部分。

  3. 阅读上述材料并思考:为什么系统调用一定是原子操作呢?如果你觉得不是所有的系统调用都是原子操作,请给出反例。希望能结合相关代码进行分析说明。

    我们 MOS 运行的 R3000 中,cpu 在遇到异常(此处为系统调用)时,会自动将 KUc 和IEc 设置为 0,进入内核态并关闭中断,在异常结束(系统调用结束时) ret_from_exception,调用 rfe 指令,恢复异常前的 KUc 和 IEc(o p c三级栈可实现异常重入)。

    以下内容摘自,北航《操作系统》课程设计修订版MIPS R3000手册 虽然我不知道为什么课程组反转了 KU 的取值,与官方手册相反 KUp, IEp ‘‘KU previous, IE previous’’:on an exception, the hardware takes the values of KUc and IEcand saves them here;
    at the same time as changing the values ofKUc, IEc to [0, 0] (kernel mode, interrupts disabled). Theinstruction rfe can be used to copy KUp, IEp back into KUc, IEc.

    • 按照上述说法控制 pipe_close 中 fd 和 pipe unmap 的顺序,是否可以解决上述场景的进程竞争问题?给出你的分析过程。

      是的。正常情况下就有 pageref(pipe) >= pageref(fd[0/1])。如果在 pipe_close 中先关闭 fd 再关闭 pipe,那么在 close(int fdnum) 中,pipe 和 fd 之间中断时, pageref(pipe) > \old(pageref(fd))-1

      1
      2
      
         r = (*dev->dev_close)(fd);
         fd_close(fd);
      
    • 我们只分析了 close 时的情形,在 fd.c 中有一个 dup 函数,用于复制文件描述符。试想,如果要复制的文件描述符指向一个管道,那么是否会出现与 close 类似的问题?请模仿上述材料写写你的理解。

    是的。同理,在正常状态时,pageref(pipe) >= pageref(fd[0/1]),但是在 dup 的过程中,如果在两次 syscall_mem_map 之间发生中断切换进程,其它进程就有可能认为 pageref(pipe) == \old(pageref(fd))-1

    要避免这个问题,就可以先 syscall_mem_map pipe,再 syscall_mem_map fd,保证 \old(pageref(pipe))+1 > pageref(fd)

  4. 思考以下三个问题。

    • 认真回看 Lab5 文件系统相关代码,弄清打开文件的过程。
    • 回顾 Lab1 与 Lab3,思考如何读取并加载 ELF 文件。
    • 在 Lab1 中我们介绍了 data text bss 段及它们的含义,data 段存放初始化过的全局变量,bss 段存放未初始化的全局变量。关于 memsize 和 filesize ,我们在 Note1.3.4中也解释了它们的含义与特点。关于 Note 1.3.4,注意其中关于“bss 段并不在文件中占数据”表述的含义。回顾 Lab3 并思考:elf_load_seg() 和 load_icode_mapper()函数是如何确保加载 ELF 文件时,bss 段数据被正确加载进虚拟内存空间。bss 段在 ELF 中并不占空间,但 ELF 加载进内存后,bss 段的数据占据了空间,并且初始值都是 0。请回顾 elf_load_seg() 和 load_icode_mapper() 的实现,思考这一点是如何实现的?下面给出一些对于上述问题的提示,以便大家更好地把握加载内核进程和加载用户进程的区别与联系,类比完成 spawn 函数。

      elf_load_seg() 部分代码如下,在 bss 段加载入内存时,填充为0。

      1
      2
      3
      4
      5
      6
      
         while (i < sgsize) {
             if ((r = map_page(data, va + i, 0, perm, NULL, MIN(bin_size - i, BY2PG))) != 0) {
                 return r;
             }
             i += BY2PG;
         }
      
  5. 通过阅读代码空白段的注释我们知道,将标准输入或输出定向到文件,需要我们将其 dup 到 0 或 1 号文件描述符(fd)。那么问题来了:在哪步,0 和 1 被“安排”为标准输入和标准输出?请分析代码执行流程,给出答案。

    user/init.c 中,有如下代码段

    1
    2
    3
    4
    5
    6
    7
    8
    
     // stdin should be 0, because no file descriptors are open yet
     if ((r = opencons()) != 0) {
         user_panic("opencons: %d", r);
     }
     // stdout
     if ((r = dup(0, 1)) < 0) {
         user_panic("dup: %d", r);
     }
    
  6. 在 shell 中执行的命令分为内置命令和外部命令。在执行内置命令时 shell 不需要 fork 一个子 shell,如 Linux 系统中的 cd 命令。在执行外部命令时 shell 需要 fork一个子 shell,然后子 shell 去执行这条命令。
    据此判断,在 MOS 中我们用到的 shell 命令是内置命令还是外部命令?请思考为什么Linux 的 cd 命令是内部命令而不是外部命令?

    MOS 中的命令都是外部命令,通过 fork() 生成子进程,子进程 runcmd(buf) (runcmd 内部 spawn 加载运行指定命令) 后 exit() 销毁。

    cd 是简单的轻量命令,在linux系统加载运行时shell就被加载并驻留在系统内存中。内部命令是写在bashy源码里面的,其执行速度比外部命令快,因为解析内部命令shell不需要创建子进程。

实验难点

本次任务的难点在于 pipe 中对于竞争的理解,需要明白在我们的 MOS 以及 Linux 内核中“文件”的概念。

在 shell,还需要理清楚 打开文件 加载入内存 进程调度运行 的过程,了解运行命令的 shell 逻辑。

实验体会

本次实验较为耗时,我难得花了2天,以往的课下实验一般一天足矣。
本次实验完成了 pipe 和 shell,最后在自己完成的简单内核中打开 shell 的那一刻,真的十分兴奋。
6系的操作系统实验课安排的很好,学习体验良好。