Contents

OS-Lab6实验报告

思考题

如果不关闭自己不会用到的管道端会怎么样?

如果读端不关闭写端的话,管道用完后他不会关闭,而是认为仍然有一个写端打开正在写入内容,因此就会一直阻塞等待写端写入,导致后面的代码永远得不到执行。同理,如果写端不关闭读端,管道可能因为认为一直有进程在读而让写端一直写内容导致缓冲区写满。

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

case0default互换。

MOS 中的管道缓冲区大小 PIPE_SIZE 过小有什么影响?缓冲区大小是怎么影响吞吐和切换效率的?

PAGE_SIZE过小会导致缓冲区满的频率更高,此时必须主动阻塞,放弃CPU,因此陷入内核进行调度下一个进程的频率也会更高,由此影响吞吐和切换效率。

上面这个恒等式在所有时刻都成立吗?请给出你的分析过程(若不成立,会在哪些时刻不成立?),如果不成立,是否会造成误判?

不一定在所有时刻都成立。假设管道正在进行端口变动,如果没有发生进程切换的情况下,该等式成立,但如果发生了进程切换,可能导致实际的pageref计算结果与真实情况不同,该等式不再成立。因此必须保证该操作为原子操作,代码中使用do while循环来保证只有在进程未发生切换时返回结果。

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

dup函数分别对fd和其对应的data执行了syscall_mem_map操作,如果先执行fd的map,可能出现pipe的ppref恰好等于了fd[0],而如果此时发生了进程切换且新进程执行了_pipe_is_closed,此时就可能触发关闭状态,进程错误认为管道已关闭,发生错误。

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

// in entry.S
exc_gen_entry:
	SAVE_ALL
	/*
	* Note: When EXL is set or UM is unset, the processor is in kernel mode.
	* When EXL is set, the value of EPC is not updated when a new exception occurs.
	* To keep the processor in kernel mode and enable exception reentrancy,
	* we unset UM and EXL, and unset IE to globally disable interrupts.
	*/
	mfc0    t0, CP0_STATUS
	and     t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE) 
	mtc0    t0, CP0_STATUS
/* Exercise 3.9: Your code here. */
	mfc0 t0, CP0_CAUSE
	andi t0, 0x7c
	lw t0, exception_handlers(t0)
	jr t0

可以看到,陷入内核后,我们通过设置IE寄存器的值禁用了中断,因此系统调用中一定不会发生进程切换,故可以认为是原子操作。

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

  1. 可以。先解除fd再解除pipe,可以保证pageref(fd)和pageref(pipe)不可能有相等的机会,无论进程调度过程中发生什么也不会错误触发。
  2. 会出现。dup中所做的操作是将ref++,如果先对fd++,那么fd有可能等于pipe的ref,此时如果发生进程切换,可能被误判为端口关闭。而先将pipe的ref++,他的ref绝对没有可能和某个端口的fd相等,因此不会出现这样的问题。

思考以下三个问题。
• 认真回看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()函数,函数调用fsipc模块对应的fsipc_open(),通过IPC向fs进程发送请求,fs进程接收到后,转发给对应的file_open()函数返回文件结构体。
  • 在env.c的load_icode()函数中,先使用elf_from()读取elf头,并据此获取段信息,调用elf_load_seg()将其加载到内存中。
  • 将bss段的空间加载到内存中在此处实现。
// elf_load_seg() in elfloader.c
while (i < sgsize) {
		if ((r = map_page(data, va + i, 0, perm, NULL, MIN(sgsize - i, PAGE_SIZE))) != 0) {
			return r;
		}
		i += PAGE_SIZE;
	}

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

// in user/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);
	}

由于启动时,系统还未分配任何fd,因此opencons()调用中分配的第一个fd就是0号fd,随后将其复制给1号,因此将0和1作为标准输入输出,而0为输入1为输出则是MOS的约定。

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

  • MOS中shell命令为外部命令,因为需要fork一个子进程去执行。
  • cd直接切换了shell的工作目录,因此写作内部命令可以减少上下文切换等的开销。

在你的shell 中输入命令ls.b | cat.b > motd。
• 请问你可以在你的shell 中观察到几次spawn ?分别对应哪个进程?
• 请问你可以在你的shell 中观察到几次进程销毁?分别对应哪个进程?

  • 两次spawn,对应ls.b cat.b
  • 四次destroy,对应ls.b cat.b 右侧命令shell 左侧命令shell

难点分析

本次实验主要难点如下:

  • 管道:管道的创建与关闭,特别是在关闭或检测是否关闭时涉及到的进程竞争。
  • Shell:需要理解fork()与spawn(),以及命令的解析与程序的执行流程。

实验体会

是OS最后一次的实验体会了。来到今天,我虽然不敢说我对MOS代码的理解有多么深刻——事实上前面很多的代码都已经忘却了,刚才写到elf的思考题时看着Lab3的代码感觉如此陌生——但我至少知道了,一个电脑从我按下开机键,到我可以输入一个ls并且给我输出了根目录的文件的整个过程,需要经历这么多这么多的事情,我们面对的,也不仅仅是大一学过的链表队列这些数据结构,还有多进程之间的竞争与同步,还有陷入内核甚至于与汇编打交道,还有各种函数指针回调函数,还有用C实现的类似面向对象的继承多态……虽然具体的代码细节很多已经模糊,但是看着Lab5、Lab6中的代码,仔细回忆还能想起来某一处一个轻飘飘的函数调用是Lab2、Lab3中多少行代码的支撑,心里竟有种莫名的敬畏。事已至此,祝我OS理论课期末考试顺利吧。是时候睡觉了,晚安。