Lab6-Report
Lab6 Report
思考题
Thinking 6.1
示例代码中,父进程操作管道的写端,子进程操作管道的读端。如果现在想让父进程作为“读者”,代码应当如何修改?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21switch (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);
运行结果
father-process read:Hello world
}
Thinking 6.2
- 上面这种不同步修改 pp_ref 而导致的进程竞争问题在 user/lib/fd.c 中的
dup 函数中也存在。请结合代码模仿上述情景,分析一下我们的 dup
函数中为什么会出现预想之外的情况?
- 如果先复制了 fd 描述符再复制内容,完成了描述符而未完成对应的文件内容映射时被打断切换,另一个进程访问这个 newfd 的内容,此时描述符复制完毕但映射仍是 oldfd 对应的文件内容,访问到旧文件内容,出现错误。
- 先复制文件内容再复制 fd 时,如果被打断,fd 尚未复制无法访问,不会出现错误。
Thinking 6.3
- 阅读上述材料并思考:为什么系统调用一定是原子操作呢?如果你觉得不是所有的系统调用都是原子操作,请给出反例。希望能结合相关代码进行分析说明。
- 系统调用一定是原子操作,因为在通过系统调用陷入内核态时汇编代码关闭了时钟中断。
- R3000 中 SR 寄存器的低六位中,一组数值表示一种 CPU 的运行状态,其中 KU 位表示是否位于用户模式下,为 1 表示位于用户模式下;IE 位表示中断是否开启,为 1 表示开启,否则不开启,而 KUc 和 IEc 则为 CPU 当前实际的运行状态。陷入内核态处理系统调用时,通过对 IEc 置 0 已关闭所有中断。
- 系统调用过程一般比较复杂而且非常常用,如果它是可被打断的,那么会给用户程序带来很大不确定性,需要讨论设计更多同步互斥情况。
- 系统调用中包括设备的读写,如果这些操作不是原子的,可能被打断覆盖了将要读或写的数据或读到尚未写完毕的数据,出现类似 dup 中的情况。
- 有些系统调用直接和上下文打交道。比如 syscall_set_trapframe(envid, tf); 我们执行这个语句后显然期望接下来 envid 在新的 tf 夏运行,但如果这个被切换了,恰好切换到 envid 进程,就可能在原 tf 错误地继续执行。
- 系统调用一定是原子操作,因为在通过系统调用陷入内核态时汇编代码关闭了时钟中断。
Thinking 6.4
- 仔细阅读上面这段话,并思考下列问题
- 按照上述说法控制 pipe_close 中 fd 和 pipe unmap
的顺序,是否可以解决上述场景的进程竞争问题?给出你的分析过程。
- 可以。判断 pipe_is_closed 的条件是 pageref(fd)==pageref(pipe),且一定有 pageref(fd)<=pageref(pipe)。
- 先 unmap 掉 fd,先把 pageref(fd)减小,使 pipe_close 中 pageref(pipe) > pageref(fd)永远成立,即可以不会出现 pipe_is_closed 误判
- 我们只分析了 close 时的情形,在 fd.c 中有一个 dup
函数,用于复制文件描述符。试想,如果要复制的文件描述符指向一个管道,那么是否会出现与
close 类似的问题?请模仿上述材料写写你的理解。
- 会出现,如果先对 fd 进行 map 后对 page 进行 map 的话,会诱发 page_ref(fd) == page_ref(pipe)这样的情况。假如在执行完 syscall_mem_map(0, oldfd, 0, newfd, vpt[VPN(oldfd)] & (PTE_D | PTE_LIBRARY))语句之后立刻切换到该管道的另一个同为读或写的进程执行,此时该进程通过 pageref(pipe)得到 pipe 页面的引用次数为 2,又因为 pageref(oldfd)也变成了 2,两者相等,该进程调用_pipe_is_closed 后得出管道另一侧关闭的结论,从而造成程序运行错误。
- 按照上述说法控制 pipe_close 中 fd 和 pipe unmap
的顺序,是否可以解决上述场景的进程竞争问题?给出你的分析过程。
Thinking 6.5
思考以下三个问题。
认真回看 Lab5 文件系统相关代码,弄清打开文件的过程。
- user/lib/file.c 中 open 函数先 fd_alloc 分配一个新文件的 fd,然后用 user/lib/fsipc.c 中 fsipc_open 打开
- fsipc_open 调用 fsipc(FSREQ_OPEN, req, fd, &perm)并返回结果,fsipc 则通过 ipc_send 发送请求给文件系统服务程序 serv 进程,ipc_recv 接收返回值
- serv 进程中 serve 接受 env->env_ipc_value 返回值到 req,发送的 FSREQ_OPEN 和接收的 case FSREQ_OPEN 对应,转入调用 serve_open
- serve_open 中调用 open_alloc 分配出一个 open 结构体,准备保存打开文件内容方式,再调用 file_open 通过文件路径找到文件,之后完善 open 结构体,发送链接后的 Filefd 到 envid
- 最后在 open 中获取该 Filefd,根据其中的文件信息调用 fsipc_map,将文件全部内容映射到内存
回顾 Lab1 与 Lab3,思考如何读取并加载 ELF 文件。
- 先用 open 打开二进制文件,将所有内容就都加载进内存
- 由 kern/env.c 文件中的 load_icode 得到 ELF 文件的的 elf 头,获取段头表、段头数量,文件大小等信息,然后使用 ELF_FOREACH_PHDR_OFF 对于其中每一段用 elf_load_seg 映射到由段头规定的虚地址开头的连续虚拟内存中
- 最后把 cp0_epc 设置为 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() 的实现,思考这一点是如何实现的?
- elf_load_seg()中:size_t bin_size = ph->p_filesz; size_t sgsize = ph->p_memsz; 如果磁盘实际使用的大小 bin_size 小于段的大小 sgsize,就将 map_page 中的 src 设为 NULL,相应空间会加载到内存,但其中回调函数 load_icode_mapper 中不进行 memcpy,所以数据保持初始值 0
Thinking 6.6
- 通过阅读代码空白段的注释我们知道,将标准输入或输出定向到文件,需要我们将其
dup 到 0 或 1 号文件描述符(fd)。那么问题来了:在哪步,0 和 1
被“安排”为标准输入和标准输出?请分析代码执行流程,给出答案。
user/sh.b:
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 命令是外部命令,在 user/fs.c 的 main 的循环中,readline 得到的命令全都 fork()出了子 shell 去执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22for (;;) {
if (interactive) {
printf("\n$ ");
}
readline(buf, sizeof buf);
if (buf[0] == '#') {
continue;
}
if (echocmds) {
printf("# %s\n", buf);
}
if ((r = fork()) < 0) {
user_panic("fork: %d", r);
}
if (r == 0) { // 子进程
runcmd(buf);
exit();
} else {
wait(r);
}
}linux 的 cd 命令之所以为内部命令,若设置为外部指令,在 cd 的时候多次调用 fork 生成子进程,过于低效。将其作为 shell 的一个内置函数实现,在解析到 cd 命令时,只需要调用该函数即可,提高系统的运行效率。
Thinking 6.8
- 在你的 shell 中输入命令 ls.b | cat.b > motd。
- 请问你可以在你的 shell 中观察到几次 spawn ?分别对应哪个进程?
- 可以观察到 2 次 spawn,分别对应 3805 和 4006 进程,也就是 ls.b 命令和 cat.b 命令的两个进程。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50$ 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 ...
- 请问你可以在你的 shell 中观察到几次进程销毁?分别对应哪个进程?
- 可以观察到 4 次进程销毁,分别对应 3805、4006、3004、2803 四个进程,即 ls.b 命令进程、cat.b 命令进程、管道 fork 出的子 shell 进程以及开始 main 函数中 fork 出的子 shell 进程。
## 实验难点
### 管道
- Pipe 流程
- 用户进程创建 pipe
- 分配两个文件描述符 fd
- 将两个文件描述符的数据对应的虚拟地址空间映射到同一个物理页
- 设置两个文件描述符的属性
- 返回 fd[0] 和 fd[1]
- 用户进程使用 pipe
- 读者
- 当读指针落后于写指针时,从循环缓冲区中读取 1 字节,将读指针后移
- 当读指针追上写指针时
- 若写者已经关闭或已经读取了部分内容,返回当前读取的字节数
- 否则,阻塞等待(写者没有关闭且未读取任何内容)
- 写者
- 当写指针领先于读指针不超过 BY2PIPE 时,将 1 字节写入循环缓冲区,将写指针后移
- 当写指针领先于读指针等于 BY2PIPE 时
- 若读者已经关闭,成功返回
- 否则,阻塞等待(读者没有关闭且未写完所有内容
- 因为对 pipe 的操作不是原子操作,所以会出现不同步修改导致的进程竞争问题。在判断 pipe 是否关闭时,可通过调整关闭管道解除文件映射时 unmap、map 的顺序解决
- _pipe_is_closed 中要读取 pageref(fd) 和 pageref(pipe),但这个用户态函数随时可能被打断,数据可能被改变,判断错误。这里的处理是两个 pageref 之前存下 env_runs, 读之后和之后 env_runs 比较,如果相同就代表过程中没有被切换,结果是可靠,否则重读。
```c
do {
runs = env->env_runs;
fd_ref = pageref(fd);
pipe_ref = pageref(p);
}while(runs != env->env_runs);
- 请问你可以在你的 shell 中观察到几次 spawn ?分别对应哪个进程?
Shell
- spawn :调用文件系统中的可执行文件并执行,产生的子进程装载新的 ELF
文件
- 使用文件系统提供的 open 函数打开即将装载的 ELF 文件 prog
- 使用系统调用 syscall_exofork 函数为子进程申请一个进程控制块。
- 使用 init_stack 函数为子进程初始化栈空间,将需要传递的参数 argv 传入子进程。
- 使用 elf_load_seg 将 ELF 文件的各个段加载进子进程。(这里与 lab3 不同的是,当前我们处于用户态,具体的 mapper 需要系统调用)
- 设置子进程的运行现场寄存器,将 tf->cp0_epc 设置为程序入口点,tf->regs[29] 设置为装载参数后的栈顶指针,从而在子进程被唤醒时以正确的状态开始运行。
- 将父进程的共享页面映射给子进程,与 fork 不同的是,这里只映射共享页面。
- 使用系统调用 syscall_set_env_status 唤醒子进程。
实验感想
- 对于稍大的系统工程,理解能力还是有待提高。
- 对于 mos 的整个系统的理解以及一些部分的细节逻辑处理还不够熟悉,一些流程和补全内容还有些不理解,在之后的挑战性任务中还需要继续阅读代码理解。