【OS】BUAA-OS-Lab6-实验报告
OS Lab实验报告
思考题
Thinking 6.1
示例代码中,父进程操作管道的写端,子进程操作管道的读端。如果现在想让父进程作为“读者”,代码应当如何修改?
答:交换case和default即可
1 |
|
Thinking 6.2
上面这种不同步修改 pp_ref
而导致的进程竞争问题在 user/lib/fd.c
中的 dup
函数中也存在。请结合代码模仿上述情景,分析一下我们的 dup
函数中为什么会出现预想之外的情况?
答:在以下这种情况下:
1 |
|
可能会出现以下问题:
父进程执行 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_close
中fd
和pipeunmap
的顺序,是否可以解决上述场景的进程竞争问题?给出你的分析过程。 - 我们只分析了
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
段存放未初始化的全局变量。关于memsize
和filesize
,我们在 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=0
,memsize>0
。elf_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 |
|
fd = 0
: 打开控制台设备,返回文件描述符,未打开任何fd
,opencons()
返回最小可用fd
,验证返回值为0
,确保fd = 0
指向控制台输入。fd = 1
: 复制fd = 0
文件表项到fd = 1
,使fd = 1
与fd = 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,顿感身心轻松,通过本学习的学习,我对于操作系统这座大厦如何拔地而起有了更深刻的了解,也不禁为其中的设计智慧而感叹。虽然课程已经结束,但是对于操作系统的学习不会停止,继续努力!
原创性说明
本报告为本人原创