思考题

Thinking 6.1

  • 更改前的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int main()
    {
    switch (fork())
    {
    case -1:
    break;

    case 0: /* 子进程 - 作为管道的读者 */
    close(fildes[1]); /* 关闭不用的写端 */
    read(fildes[0], buf, 100); /* 从管道中读数据 */
    printf("child-process read:%s",buf); /* 打印读到的数据 */
    close(fildes[0]); /* 读取结束,关闭读端 */
    exit(EXIT_SUCCESS);

    default: /* 父进程 - 作为管道的写者 */
    close(fildes[0]); /* 关闭不用的读端 */
    write(fildes[1], "Hello world\n", 12); /* 向管道中写数据 */
    close(fildes[1]); /* 写入结束,关闭写端 */
    exit(EXIT_SUCCESS);
    }
    }
  • 更改后的代码:将子进程与父进程部分调换即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int main()
    {
    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("child-process read:%s",buf); /* 打印读到的数据 */
    close(fildes[0]); /* 读取结束,关闭读端 */
    exit(EXIT_SUCCESS);
    }
    }

Thinking 6.2

  • 关系式pageref(rfd) + pageref(wfd) = pageref(pipe) 在非原子的 close 函数调用时不能保证成立;作为非原子操作,在该函数进行时,有可能出现时钟中断,更换进程的问题,从而导致函数出现问题。

Thinking 6.3

  • 系统调用一定是原子操作。在系统调用 syscall 对应的异常处理程序 handle_sys 中,使用了汇编宏定义 CLI 来禁用全局中断,因此系统调用时不会被中断,是原子操作。CLI宏定义具体代码如下:

    1
    2
    3
    4
    5
    6
    7
    .macro CLI
    mfc0 t0, CP0_STATUS
    li t1, (STATUS_CU0 | 0x1)
    or t0, t1
    xor t0, 0x1
    mtc0 t0, CP0_STATUS
    .endm

Thinking 6.4

  • 可以解决。关系式pageref(rfd) + pageref(wfd) = pageref(pipe)中,ref(pipe)先减小,ref(fd)后减小,因此如果先解除p[0]的映射,将会永远达不到取等的临界条件。
  • dup函数是类似的,不过变为增加,也会出现与close类似的问题,pipe的引用次数总比fd要高。当管道的dup进行到一半时, 若先映射fd,再映射 pipe ,就会使得fd的引用次数的加一先于pipe。这就导致在两个map的间隙,会出现pageref(pipe) == pageref(fd)的情况。这个问题也可以通过调换两个map的顺序来解决。

Thinking 6.5

  • 整体流程:分配文件标识符fd、按照路径打开文件、编辑文件标识符、匹配存储单元、返回文件标识符特征值。
  • 读取加载方法:对于普通文件,我们需要编译形成ELF文件,之后才可以运行。
  • 实现方式:对于bss段中和text&data段共同占据一个页面的部分,分配页面并设置初值为0;对于bss段其它部分,仅使分配页面而不映射到任何内容。

Thinking 6.6

  • 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);
    }

Thinking 6.7

  • shell命令是外部命令,在执行shell命令时,进程通过fork()函数产生一个子进程,即子shell,之后子shell运行该命令所对应的可执行文件。
  • linux中的内部命令实际上是shell程序的一部分,其中包含一些比较简单的linux系统命令。这些命令由shell程序识别并在shell程序内部完成运行,通常在linux系统加载运行时shell就被加载并驻留在系统内存中。

Thinking 6.8

  • shell中进行了2次spawn,分别用于执行ls.bcat.b
  • shell中进行了2次进程销毁,这2个进程分别是shell执行ls.bcat.b时通过spawn生成的子进程。

难点分析

本次实验中,我遇到的难点主要有以下几点:

  • pipe相关的内存使用:本次实验中一个重点是管道的实现。出现管道时,为当前进程分出子进程,用父进程处理管道左端,用子进程处理管道右端。子进程和父进程的内存空间中分别为写端fd和读端fd分配了一个虚拟页,由于两个进程的fork关系,子进程和父进程的读端所在的虚拟页映射到了同一个物理页,同样他们的写端所在的虚拟也也都映射到了同一个物理页,因此可以实现读写的统一性。

  • spawn函数:在该函数中,主要有6部分,分别是读取文件描述符fd、读取ELF文件头部、使用fork生成子进程、初始化子进程的数据栈、读取文件中的项目头部、读取并匹配文件的ELF数据、加载segment。各部分均需实现报错后的逐层输出。难点主要在于各部分函数的使用以及整体的实现逻辑。

实验体会

Lab6主要让我们学习有关管道和shell的问题,了解MIPS系统中虚拟地址与物理地址的对应关系、多文件的读取与更改的内容。本次实验对比前几次lab内容较少,同时由于指导书和注释的详细,完成难度不大。但是由于前面lab遗留的前后程序不适配问题,导致出现bug且较难查明的问题,耗时较久。十分感谢助教学长的帮助和那些写博客的学长(虽然发现了两篇有所不同的博客,也没有尝试是否都是正确的),看了他们的博客后真的是茅塞顿开。作为最后一个常规lab,整个os学习也算是有始有终地完成了。整个学期下来困难很多,但是学到了很多知识和算法,提升也很大。