Thinking

Thinking 6.1

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

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
#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函数中为什么会出 现预想之外的情况?

在fdc/dup函数中,依次实现了以下两个过程

  • 将newfd文件描述符所在的虚拟页映射到oldfd所在的物理页(文件描述符的映射,类似于读写文件描述符的映射)
  • 将newfd的文件数据对应的虚拟页映射到oldfd的文件数据对应的物理页(类似于pipe的映射)
1
2
3
4
5
6
7
8
9
pipe(p);
if(fork() == 0){
close(p[1]); // 子进程关闭写端
read(p[0],buf,sizeof buf); // 子进程读
}else{
dup(p[0],newfd);
close(p[0]);
write(p[1],"hello", 5);
}

假设执行过程如下:

  • fork执行结束后,子进程先执行。时钟中断发生在close(p[1])与read之间,父进程开始执行
  • 父进程在dup(p[0],newfd)过程中,先增加了对p[0]的映射,还没有来得及增加对pipe的映射中断便已经发生,进程调度后子进程执行
  • 此时pageref(pipe)=3,而pageref(p[0])=3,错误地判断了写端已经关闭

Thinking 6.3

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

所有的系统调用均为原子操作。我们在执行具体的异常处理程序前均进行了关中断操作,防止系统调用操作被打断

1
2
3
4
5
exc_gen_entry:
SAVE_ALL
mfc0 t0, CP0_STATUS
and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
mtc0 t0, CP0_STATUS //关中断

Thinking 6.4

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

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

可以解决上述问题中的竞争问题

pageref(p[0])本就小于pageref(pipe),如果先减去pageref(p[0]),则不会出现由于pageref(p[0]) == pageref(pipe)短暂成立而导致的问题了

dup函数会出现类似close的问题

pageref(p[0])本就小于pageref(pipe),在dup中如果先增加pageref(p[0]),则会出现由于pageref(p[0]) == pageref(pipe)短暂成立而导致的端口关闭错判问题,依旧可以通过调整顺序,先增加pageref(pipe)来加大两者间的差距而避免这种错误

Thinking 6.5

bss段 在ELF中并不占空间,但ELF加载进内存后,bss段的数据占据了空间,并且初始 值都是0。请回顾elf_load_seg() 和 load_icode_mapper() 的实现,思考这一点 是如何实现的?

当文件大小小于内存空间大小时,对于空缺处(即bss字段)进行了0填充

1
2
3
4
5
6
7
// lib/elfloader.c中的elf_load_seg函数中进行了bss段0填充	
while (i < sgsize) {
if ((r = map_page(data, va + i, 0, perm, NULL, MIN(sgsize - i, PAGE_SIZE))) != 0) {
return r;
}
i += PAGE_SIZE;
}

Thinking 6.6

通过阅读代码空白段的注释我们知道,将标准输入或输出定向到文件,需要 我们将其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);
}

Thinking 6.7

在 shell 中执行的命令分为内置命令和外部命令。在执行内置命令时shell不 需要fork 一个子 shell,如 Linux 系统中的 cd 命令。在执行外部命令时 shell 需要 fork 一个子shell,然后子 shell 去执行这条命令。 据此判断,在MOS 中我们用到的 shell 命令是内置命令还是外部命令?请思考为什么 Linux 的 cd 命令是内部命令而不是外部命令?

在MOS中使用的shell命令是外置命令,会通过fork产生子进程shell来运行该进程

Linux中的cd用于改变工作目录。如果cd作为一个外部命令在子进程中运行,并不会改变父进程shell的工作目录,这不符合cd命令的目的。

Thinking 6.8

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

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

直接在shell中运行命令可以得到以下结果

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

spawn.c中最后添加如下信息:

1
debugf("spawn: father %x, child %x\n", syscall_getenvid(), child);

可得到如下运行信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ls.b | cat.b > motd
[00002803] pipecreate
spawn: father 2803, child 3805
spawn: father 3004, child 4006
[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 ...

可以看到,shell进行了2次spawn,生成的子进程用于执行ls.b与cat.b

进行了四次进程销毁,分别销毁了

  • 由fork创建的ls.b | cat.b > motd代表的子进程
  • spawn创建的完成ls.b的子进程
  • fork创建的 cat.b > motd子进程
  • spawn创建的cat.b的子进程

学习难点

学习过程中,lab6的spawn函数理解起来较难,具体流程如下:

  1. 调用open函数打开shell指令指定的文件
  2. 通过readn读取该可执行文件的ehdr,
  3. 通过系统调用syscall_exofork创建子进程
  4. 调用init_stack初始化子进程栈空间
  5. 遍历elf文件的程序头表将段数据加载到子进程内存空间
  6. 正确设置子进程的栈指针与epc
  7. 将父进程的共享页面映射至子进程地址空间
  8. 设置子进程状态为可运行

shell函数的调用关系

实验体会

在Lab6中我们实现了简单的匿名管道与shell机制,需要我们填写完成的部分较为简单。但是想要对于诸如shell的处理机制有深入认识与了解的话还是要进一步仔细阅读代码。

感觉突然就最后一个lab了,感觉还是稀里糊涂~~~