BUAA_OS_Lab6 实验报告

思考题

Thinking 6.1

示例代码中,父进程操作管道的写端,子进程操作管道的读端。如果现在想让父进程作为“读者”,代码应当如何修改?

修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

Thinking 6.2

上面这种不同步修改 pp_ref 而导致的进程竞争问题在 user/lib/fd.c 中的 dup 函数中也存在。请结合代码模仿上述情景,分析一下我们的 dup 函数中为什么会出现预想之外的情况?

此前的dup函数是先映射文件描述符,再映射文件内容。如果在映射完文件描述符后,进程被中断,那么另一程序会错误的认为文件映射已经完成,产生错误。

Thinking 6.3

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

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

仔细阅读上面这段话,并思考下列问题

  • 按照上述说法控制 pipe_close 中 fd 和 pipe unmap 的顺序,是否可以解决上述场景的进程竞争问题?给出你的分析过程。
  • 我们只分析了 close 时的情形,在 fd.c 中有一个 dup 函数,用于复制文件描述符。试想,如果要复制的文件描述符指向一个管道,那么是否会出现与 close 类似的问题?请模仿上述材料写写你的理解。

可以解决。先关闭缓冲区后,缓冲区的引用次数就发生了修改。此时,即使发生了中断,pipe也会阻止读写操作。
可以解决。先建立缓冲区,此时pipe就已经可以进行正常的读写功能了。

Thinking 6.5

思考以下三个问题。

  • 认真回看 Lab5 文件系统相关代码,弄清打开文件的过程。
  • 回顾 Lab1 与 Lab3,思考如何读取并加载 ELF 文件。
  • 回顾 Lab3 并思考:elf_load_seg() 和 load_icode_mapper()函数是如何确保加载 ELF 文件时,bss 段数据被正确加载进虚拟内存空间。bss 段在 ELF 中并不占空间,但 ELF 加载进内存后,bss 段的数据占据了空间,并且初始值都是 0。请回顾 elf_load_seg() 和 load_icode_mapper() 的实现,思考这一点是如何实现的?
  • 首先用户调用user/lib/files.c文件中的open函数,然后open函数调用了fsipc函数,这个函数会向文件系统发送一个请求,并等待返回的信息。文件系统中的serve函数会调用file_open函数来完成文件的打开操作,并返回返回值。
  • ELF文件中的段信息是通过Elf32_Phdr结构体数组来描述的,其中包含了各个段的类型、在文件中的偏移、在内存中的虚拟地址、大小等信息。对于bss段,虽然它在文件中不占据实际存储空间,但它在内存中有一个预期的大小,用于存放未初始化的全局变量和静态变量,这些变量默认值为0。
  • 加载器遍历ELF文件的各个段并调用elf_load_seg0()处理各个段的时候,关键在于如何处理sgsizebin_size之间的差异,也就是段在内存中的大小和段在文件中的大小之间的差异。如果段有实际的数据内容(.text.data),elf_load_seg()会通过调用load_icode_mapper()将这些内容从文件复制到对应的虚拟内存位置;对于bss段,不断分配页面直到达到sgsize指定的大小,所以新分配的页面不会从文件中复制任何数据,保持其内容为全0,自然就被初始化为全 0。

Thinking 6.6

通过阅读代码空白段的注释我们知道,将标准输入或输出定向到文件,需要我们将其 dup 到 0 或 1 号文件描述符(fd)。那么问题来了:在哪步,0 和 1 被“安排”为标准输入和标准输出?请分析代码执行流程,给出答案。

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

这是user/init.c的代码片段。

Thinking 6.7

在 shell 中执行的命令分为内置命令和外部命令。在执行内置命令时 shell 不需要 fork 一个子 shell,如 Linux 系统中的 cd 命令。在执行外部命令时 shell 需要 fork一个子 shell,然后子 shell 去执行这条命令。

据此判断,在 MOS 中我们用到的 shell 命令是内置命令还是外部命令?请思考为什么Linux 的 cd 命令是内部命令而不是外部命令?

cd命令用于改变当前工作目录,这是一个与 shell 会话状态紧密相关的操作。作为内部命令,它可以确保改变立即生效并影响后续命令的执行环境。

Thinking 6.8

在你的 shell 中输入命令 ls.b | cat.b > motd。

  • 请问你可以在你的 shell 中观察到几次 spawn ?分别对应哪个进程?
  • 请问你可以在你的 shell 中观察到几次进程销毁?分别对应哪个进程?

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ls.b | cat.b > motd
[00002803] pipecreate
[00003805] destroying 00003805
[00003805] free env 00003805
i am killed ...
[00004006] destroying 00004006
[00004006] free env 00004006
i am killed ...
[00003004] destroying 00003004
[00003004] free env 00003004
i am killed ...
[00002803] destroying 00002803
[00002803] free env 00002803
i am killed ...

一共有两次spawn,一次启动ls.b,另一次启动cat.b

一共有四次进程销毁

实验总结

Lab6 的核心是实现 Shell 和管道通信。这个实验要求我们不仅要弄懂 Shell 是如何启动的,还要深入探究其工作原理,例如:它如何读取我们输入的命令,然后创建相应的进程来执行这些命令,以及如何管理进程之间的通信。
总而言之,Lab6 是一个综合性很强的顶层实验。在整个 MOS(操作系统)的开发过程中,我们采用的是一种自底向上的构建方法:从最底层的基本功能开始,然后添加系统调用,再到用户态的文件系统。现在,我们已经将整个系统推进到了用户层面,而 Lab6 正是这个用户层面的终极挑战,它需要我们灵活运用之前搭建的所有模块来共同完成。

实验体会

与之前的实验相比,Lab6 的设计更加友好,并且没有安排上机考试,给了我们更宽松的探索空间。

正如之前提到的,Lab6 作为整个 MOS 项目的收官之作,它的完成标志着我们这门操作系统课程的学习也圆满结束。用这样一个需要融会贯通所有知识点的综合性实验来为整个学期画上句号,我觉得是再好不过的安排了。它真正让我们将一学期所学的知识付诸实践,看到了理论走向应用的成果。

总的来说,这学期的操作系统实验让我收获满满,非常感谢课程团队的精心设计和辛勤付出!