【OS】BUAA-OS-Lab6-实验报告

OS Lab实验报告

思考题

Thinking 6.1

示例代码中,父进程操作管道的写端,子进程操作管道的读端。如果现在想让父进程作为“读者”,代码应当如何修改?
答:交换case和default即可

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
#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\n", buf);
close(fildes[0]);
exit(EXIT_SUCCESS);
}
}

Thinking 6.2

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

1
2
3
4
5
6
7
pipe(p);
if (fork() == 0) {
read(p[0], buf, sizeof(buf));
} else {
dup(p[0], newfd);
write(p[1], "a", 5);
}

可能会出现以下问题:
父进程执行 dup(p[0], newfd)pageref(p[0]) 变为2,尚未更新管道数据块的 pp_ref。子进程执行 read(p[0], buf, sizeof(buf)) ,发现 pageref(p[0])为2,子进程退出。父进程继续执行 write(p[1], "a", 5),子进程退出 read,父进程 write 可能永远无法结束。
问题主要是由于修改pp_ref不是原子操作

Thinking 6.3

阅读上述材料并思考:为什么系统调用一定是原子操作呢?如果你觉得不是所有的系统调用都是原子操作,请给出反例。希望能结合相关代码进行分析说明。
答:MOS中系统调用是原子的,在系统调用前关闭了中断,同时其他进程也无法进入内核,所以系统调用是原子的

Thinking 6.4

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

  • 按照上述说法控制 pipe_closefdpipeunmap 的顺序,是否可以解决上述场景的进程竞争问题?给出你的分析过程。
  • 我们只分析了 close 时的情形,在 fd.c 中有一个 dup 函数,用于复制文件描述符。试想,如果要复制的文件描述符指向一个管道,那么是否会出现与 close 类似的问题?请模仿上述材料写写你的理解。
    答:
  • 可以,先 unmap fd,减少 pageref(fd),再 unmap pipe,减少 pageref(pipe),保证 pageref(pipe) >= pageref(fd)始终成立。其他进程不会观察到 pageref(fd) > pageref(pipe) ,不会误判管道状态。
  • 会出现类似问题,dup 先增加 pageref(fd),后增加 pageref(pipe),中间状态可能出现 pageref(fd) > pageref(pipe)。其他进程如果观察到这一状态,会误判管道的一端已关闭。

Thinking 6.5

  • 认真回看 Lab5 文件系统相关代码,弄清打开文件的过程。
  • 回顾 Lab1 与 Lab3,思考如何读取并加载 ELF 文件。
  • 在 Lab1 中我们介绍了 data text bss 段及它们的含义,data 段存放初始化过的全局变量,bss 段存放未初始化的全局变量。关于 memsizefilesize,我们在 Note 1.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 函数。

    关于第一个问题,在 Lab3 中我们创建进程,并且通过 ENV_CREATE(...) 在内核态加载了初始进程,而我们的 spawn函数则是通过和文件系统交互,取得文件描述块,进而找到 ELF 在“硬盘”中的位置,进而读取。

    关于第二个问题,各位已经在 Lab3 中填写了 load_icode 函数,实现了 ELF 可执行文件中读取数据并加载到内存空间,其中通过调用 elf_load_seg 函数来加载各个程序段。在 Lab3 中我们要填写 load_icode_mapper 回调函数,在内核态下加载 ELF 数据到内存空间;相应地,在 Lab6 中 spawn 函数也需要在用户态下使用系统调用为 ELF 数据分配空间。
    答:

  • 先调用 open() 函数,然后调用 fdipc_open() 联系文件服务进程,文件服务进程接收到消息后调用 serve_open() ,创建文件,打开文件,传递回FileFd
  • 解析ELF头获取程序头表位置,读程序头表定位LOAD段,遍历LOAD段,调用elf_load_seg(),调用load_icode_mapper()分配物理页;文件数据段:复制内容到内存;BSS段:分配零初始化页。
  • BSS中 filesize=0memsize>0elf_load_seg() 发现 bin_size < mem_size 时,调用 load_icode_mapper(NULL, size) ,进行 page_insert ,保证有足够空间放置 bss ,同时设置 src = NULL 得到设置初始值为 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);
}
  • fd = 0: 打开控制台设备,返回文件描述符,未打开任何 fdopencons() 返回最小可用 fd,验证返回值为 0,确保fd = 0指向控制台输入。
  • fd = 1: 复制 fd = 0 文件表项到 fd = 1,使 fd = 1fd = 0 指向同一控制台设备,将标准输出定向到控制台。

Thinking 6.7

shell 中执行的命令分为内置命令和外部命令。在执行内置命令时 shell 不需要 fork 一个子 shell,如 Linux 系统中的 cd 命令。在执行外部命令时 shell 需要 fork 一个子 shell,然后子 shell 去执行这条命令。
据此判断,在 MOS 中我们用到的 shell 命令是内置命令还是外部命令?请思考为什么 Linux 的 cd 命令是内部命令而不是外部命令?
答:内置命令是shell的功能,不需要创建新的进程;外部命令是磁盘上的程序,需要创建新的进程来执行。MOS中的shell为外部命令。在sh.c的主循环中,无论何种命令,shell都会先执行fork()创建子进程,再通过runcmd()执。这说明所有命令都在子进程中运行,是外部命令。cd是内置命令,如果是外部命令,在子进程中执行,目录变更不会影响父shell。

Thinking 6.8

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

  • 请问你可以在你的 shell 中观察到几次 spawn?分别对应哪个进程?
  • 请问你可以在你的 shell 中观察到几次进程销毁?分别对应哪个进程?
    答:观察到两次 spawn ,第一次生成运行 ls.b 的进程;第二次生成运行 cat.b 的进程。观察到四次进程销毁。

难点分析

我认为本次实验并不简单,难点主要是管道关闭存在的竞态条件,但是通过对其解决方案的学习可以帮助理解这些问题。同时Shell的实现也需要仔细思考,理解好ELF文件的加载。

实验体会

终于完成了操作系统课程的最后一个Lab,顿感身心轻松,通过本学习的学习,我对于操作系统这座大厦如何拔地而起有了更深刻的了解,也不禁为其中的设计智慧而感叹。虽然课程已经结束,但是对于操作系统的学习不会停止,继续努力!

原创性说明

本报告为本人原创


【OS】BUAA-OS-Lab6-实验报告
http://example.com/2025/06/13/【OS】BUAA-OS-Lab6-实验报告/
作者
mRNA
发布于
2025年6月13日
许可协议