lab6 实验报告
思考题
-
示例代码中,父进程操作管道的写端,子进程操作管道的读端。如果现在想让父进程作为“读者”,代码应当如何修改?
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); } }
-
上面这种不同步修改 pp_ref 而导致的进程竞争问题在 user/lib/fd.c 中的 dup 函数中也存在。请结合代码模仿上述情景,分析一下我们的 dup 函数中为什么会出现预想之外的情况?
在 dup() 中,在两次的 syscall_mem_map 之间 可能发生 时间片结束 -> 进程切换。导致 pageref(pipe) 和 pageref(p[0/1]) 与预期不一致,导致错误发生。
具体分析见 tests/lab6_1/testpiperace.c 部分。
-
阅读上述材料并思考:为什么系统调用一定是原子操作呢?如果你觉得不是所有的系统调用都是原子操作,请给出反例。希望能结合相关代码进行分析说明。
我们 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)
。 -
-
思考以下三个问题。
- 认真回看 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; }
-
通过阅读代码空白段的注释我们知道,将标准输入或输出定向到文件,需要我们将其 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); }
-
在 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系的操作系统实验课安排的很好,学习体验良好。