OS lab6 思考题

OS lab6 思考题

rainbowYao Lv3

Thinking 6.1

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

操作相反即可:父进程关掉写端1,子进程关掉读端0

1
2
3
4
5
6
7
8
9
10
11
12
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("child-process read:%s",buf); /* 打印读到的数据 */
close(fildes[0]); /* 读取结束,关闭读端 */
exit(EXIT_SUCCESS);

Thinking 6.2

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

  • dup 函数中,首先复制文件描述符的映射关系,然后,复制文件描述符内文件页的映射关系。
  • 如果文件描述符指向的是一个管道文件,那么在文件描述符复制之后、文件页映射复制之前,会出现:文件描述符的页引用次数等于管道页的引用次数。
  • 此时如果进程被中断,就可能导致对引用次数的判断错误。这种错误的判断类似于指导书中的例子。

Thinking 6.3

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

  • 只有 syscall_ 开头的系统调用函数是原子操作,进程切换是通过定时器产生时钟中断,触发时钟中断切换进程。但是syscall跳转到内核态时,CPU将SR寄存其的IEC置位0,关闭了时钟中断,故是原子操作
  • 而较为复杂的系统调用,如 write 函数,尽管操作系统会尽力保证写操作的一致性,但在某些情况下,write 操作可能并不是完全原子的。例如,当进行大块数据写入时,可能会被分成多个部分来写,每个部分写入之间可能会被中断。

Thinking 6.4

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

  • 按照上述说法控制 pipe_close 中 fd 和 pipe unmap 的顺序,是否可以解决上述场景的进程竞争问题?给出你的分析过程。
  • 我们只分析了 close 时的情形,在 fd.c 中有一个 dup 函数,用于复制文件描述符。 试想,如果要复制的文件描述符指向一个管道,那么是否会出现与 close 类似的问题?请模仿上述材料写写你的理解。
  • 可以,之前的原因来自于大数(ref_pipe)先减小而小数(ref_fd)还没减(但是需要减小,只是被中断打断了)导致大数和小数相等,如果调整顺序,可以保证小数先减少,而大数若想和小数相等,只可能是管道⼀端进程关闭
  • pipe的引⽤次数总⽐fd要⾼。当dup进⾏到⼀半时,即只增加了fd的映射,还未增加pipe的映射,就会出现pageref(pipe) == pageref(fd) 的情况。显然,也可以通过调换顺序解决问题,即先加大数,再加小数

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 数据分配空 间。

(1)打开文件的过程:fd = open(prog, O_RDONLY),调用lab5实现好的open函数,返回一个文件描述符fd

(2)加载ELF的部分比较复杂

  • 先是用readnelf_from,读出ELF头
  • 使用宏 ELF_FOREACH_PHDR_OFF 遍历每个程序头(Elf32_Phdr),并判断其类型是否为 PT_LOAD。如果是,则需要加载该段。
  • 先用 read_map(fd, ph->p_offset, &bin) 从文件系统中读出程序内容
  • 再通过 elf_load_seg 函数加载段,并使用回调函数 spawn_mapper 分配内存并映射段。

(3)

1
2
3
4
5
6
while (i < sgsize) {
if ((r = map_page(data, va + i, 0, perm, NULL, MIN(sgsize - i, PAGE_SIZE))) != 0) {
return r;
}
i += PAGE_SIZE;
}

elf_load_seg 函数负责加载一个段,其中会处理 BSS 段的初始化。假设 filesz (i)小于 memsz(sgsize),则需要将额外的部分初始化为 0

Thinking 6.6

通过阅读代码空白段的注释我们知道,将标准输入或输出定向到文件,需要我们将其 dup 到 0 或 1 号文件描述符(fd)。那么问题来了:在哪步,0 和 1 被“安排”为 标准输入和标准输出?请分析代码执行流程,给出答案。

通过 opencons()dup() 函数,它们都被指向控制台设备,实现输入和输出的功能。

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
//init.c
// 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);
}

//console.c
int opencons(void) {
int r;
struct Fd *fd;

if ((r = fd_alloc(&fd)) < 0) {
return r;
}
if ((r = syscall_mem_alloc(0, fd, PTE_D | PTE_LIBRARY)) < 0) {
return r;
}
fd->fd_dev_id = devcons.dev_id;
fd->fd_omode = O_RDWR;
return fd2num(fd);
}

opencons之后,文件描述符 0(stdin)已经指向控制台设备;调用 dup(0, 1) 复制文件描述符 0(stdin)到文件描述符 1(stdout),此时,文件描述符 1(stdout)也指向控制台设备。

Thinking 6.7

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

  • shell是外部命令,MOS执行shell依靠spawnl("sh.b", "sh", "-x" , NULL)(位于user/init.c)
  • cd命令需要改变父进程的状态,即所在的路径,通过fork之后,只改变了子进程,所以需要是内部命令。

Thinking 6.8

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

  • 请问你可以在你的 shell 中观察到几次 spawn ?分别对应哪个进程?
  • 请问你可以在你的 shell 中观察到几次进程销毁?分别对应哪个进程?
image-20240612100536255
  • 两次spawn,对应ls.bcat.b两个可执行文件
  • 四次进程销毁
    • 第一个进程是sh.c文件中,fork了一个子进程来处理这一整行命令
    • 其中有两个进程对应上面spawn的两个可执行文件
    • 当命令中有管道符号时,命令解释步骤中需要调⽤ fork() ,对管道两边的命令分别处理,此处产生一个进程。
  • 标题: OS lab6 思考题
  • 作者: rainbowYao
  • 创建于 : 2024-07-02 21:25:32
  • 更新于 : 2024-09-18 09:28:38
  • 链接: https://redefine.ohevan.com/2024/07/02/OS-lab6-思考题/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
OS lab6 思考题