Featured image of post 北航OS Lab6 管道与SHELL

北航OS Lab6 管道与SHELL

实验报告

实验报告

思考题

Thinking 6.1

只需对父子进程的读写端口与管道行为等进行对调

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
switch(fork()) {
	case -1:
	break;

	case 0: /*子进程-作为管道的写者*/
	close(fildes[0]); /*关闭不用的读端*/
	write(fildes[1],"Helloworld\n",12); /*向管道中写数据*/
	close(fildes[1]); /*写入结束,关闭写端*/
	exit(EXIT_SUCCESS);

	default: /*父进程-作为管道的读者*/
	close(fildes[1]); /*关闭不用的写端*/
	read(fildes[0],buf, 100); /*从管道中读数据*/
	printf("father-processread:%s",buf); /*打印读到的数据*/
	close(fildes[0]); /*读取结束,关闭读端*/
	exit(EXIT_SUCCESS);
	}

Thinking 6.2

下面是原来的dup函数的映射部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
if ((r = syscall_mem_map(0, oldfd, 0, newfd, 
			vpt[VPN(oldfd)] & (PTE_D | PTE_LIBRARY))) < 0) {
	goto err; // 文件描述符的映射
}

if (vpd[PDX(ova)]) {
	for (i = 0; i < PDMAP; i += PTMAP) {
		pte = vpt[VPN(ova + i)];

		if (pte & PTE_V) {
			// should be no error here -- pd is already allocated
			if ((r = syscall_mem_map(0, (void *)(ova + i), 0, (void *)(nva + i),
						 pte & (PTE_D | PTE_LIBRARY))) < 0) {
				goto err; // 文件内容的映射
			}
		}
	}
}

可以看到dup先对文件描述符进行映射,后对文件内容进行映射。

如果在这两个系统调用中发生进程的中断,根据文件描述符的pp_ref将判断映射已经完成,而实际上文件内容并未被映射。

Thinking 6.3

我们的MOS是单核的,一般来说只有一个内核线程。

那么引发系统调用被中断,只有可能是外部中断。

而我们的MOS保证进入核心态后,会屏蔽外部中断。

1
2
3
4
5
6
7
8
9
exc_gen_entry:
	SAVE_ALL
	mfc0    t0, CP0_STATUS
	and     t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
	mtc0    t0, CP0_STATUS  // unset IE to globally disable interrupts
	mfc0    t0, CP0_CAUSE
	andi    t0, 0x7c
	lw      t0, exception_handlers(t0)
	jr      t0

Thinking 6.4

其实指导书说的也很明白了:

一般情况下pageref(pipe)肯定大于读或写其中一个fd的pp_ref。通过调整顺序,先降低fd的pp_ref,保证了pageref(pipe) > pageref(fd)在彻底close前恒成立,从而解决了中途pageref(pipe)先降低导致出现pageref(pipe) == pageref(fd)的问题。

对于未修改的dup函数,同样地也会出现这个问题。正如Thinking 6.2中所说,若先增加fd的pp_ref,后增加文件内容(也就是pipe)的pp_ref,就不能保证始终有pageref(pipe) > pageref(fd)。在两个map的间隙,就会存在pageref(pipe) == pageref(fd)的时刻,导致误判。

倘若修改map的顺序,先对pipe进行map,就可以解决这一问题。

Thinking 6.5

打开文件的过程

用户调用open函数,提供路径和打开方式

1
2
3
4
	int fd;
	if ((fd = open(prog, O_RDONLY)) < 0) {
		return fd;
	}

open函数在file.c中,负责申请fd,并利用fsipc完成文件的open与map,返回fd的编号

fsipc_open为例,他将路径与打开方式打包为struct Fsreq_open放进fsipcbuf(即req),并return fsipc(FSREQ_OPEN, req, fd, &perm);

其中fsipc函数借助ipc通信,把value与页面传递给1号进程,也就是文件服务进程。

1
2
3
4
5
6
static int fsipc(u_int type, void *fsreq, void *dstva, u_int *perm) {
	u_int whom;
	// Our file system server must be the 2nd env.
	ipc_send(envs[1].env_id, type, fsreq, PTE_D);
	return ipc_recv(&whom, dstva, perm);
} // 给文件管理进程通信,传的值是type,共享的页面是一个结构体(一段内存)fsreq

发出的信息在fs/serv.c中的死循环函数serve中被接收

1
req = ipc_recv(&whom, (void *)REQVA, &perm);

并将其转化为serve_open函数

1
2
	func = serve_table[req];
	func(whom, REQVA);

serve_open函数则进行真正的打开文件操作。 其实也就是为它申请好了一个struct Open。

如何读取并加载ELF文件

我们在spawn中已经打开了二进制文件,接下来便使用readn进行读取。

readn在fd.c中,实现了少量多次read的过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int readn(int fdnum, void *buf, u_int n) {
	int m, tot;
	for (tot = 0; tot < n; tot += m) {
		m = read(fdnum, (char *)buf + tot, n - tot);
		if (m < 0) {
			return m;
		} if (m == 0) {
			break;
		}
	} return tot;
}

读取完毕后使用elf_from进行检查,并得到ehdr。

1
2
3
4
5
6
7
8
9
const Elf32_Ehdr *elf_from(const void *binary, size_t size) {
	const Elf32_Ehdr *ehdr = (const Elf32_Ehdr *)binary;
	if (size >= sizeof(Elf32_Ehdr) && ehdr->e_ident[EI_MAG0] == ELFMAG0 &&
	    ehdr->e_ident[EI_MAG1] == ELFMAG1 && ehdr->e_ident[EI_MAG2] == ELFMAG2 &&
	    ehdr->e_ident[EI_MAG3] == ELFMAG3 && ehdr->e_type == 2) {
		return ehdr;
	}
	return NULL;
}

整体流程就是下面所示,得到了elf的入口entrypoint。

1
2
3
4
5
6
7
8
9
if ((r = readn(fd, elfbuf, sizeof(Elf32_Ehdr))) != sizeof(Elf32_Ehdr)) {
	goto err;
}
const Elf32_Ehdr *ehdr = elf_from(elfbuf, sizeof(Elf32_Ehdr));
if (!ehdr) {
	r = -E_NOT_EXEC;
	goto err;
}
u_long entrypoint = ehdr->e_entry;

接下来就是加载ELF文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ELF_FOREACH_PHDR_OFF (ph_off, ehdr) {
		if ((r = seek(fd, ph_off)) < 0) {
			goto err1;
		}
		if ((r = readn(fd, elfbuf, ehdr->e_phentsize)) != ehdr->e_phentsize) {
			goto err1;
		}
		Elf32_Phdr *ph = (Elf32_Phdr *)elfbuf;
		if (ph->p_type == PT_LOAD) {
			void *bin;
			r = read_map(fd, ph->p_offset, &bin);
			if (r != 0) {
				goto err1;
			}
			r = elf_load_seg(ph, bin, spawn_mapper, &child);
			if (r != 0) {
				goto err1;
			}
		}
	}

bss段如何占据空间并初始化为0

这个就涉及到具体的加载过程。 详见Lab3博客 Thinking 3.3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int elf_load_seg(Elf32_Phdr *ph, const void *bin, elf_mapper_t map_page, void *data) {
	...
	...
	while (i < sgsize) { // 文件大小<内存,将剩余的内存都写入NULL
		if ((r = map_page(data, va + i, 0, perm, NULL, MIN(sgsize - i, PAGE_SIZE))) != 0) {
			return r;
		}
		i += PAGE_SIZE;
	}
	return 0;
}

文件大小小于内存大小时,仍继续映射,用NULL写入剩余内存

而回调函数spawn_mapper的逻辑保证不对NULL的部分作处理,就可以保证空间仍然为最开始的0。

1
2
3
4
5
6
7
8
9
static int spawn_mapper(void *data, u_long va, size_t offset, u_int perm, const void *src,
			size_t len) {
	u_int child_id = *(u_int *)data;
	try(syscall_mem_alloc(child_id, (void *)va, perm));
	if (src != NULL) {
		...
	}
	return 0;
}

Thinking 6.6

如指导书所说,0和1是在init.b执行之后所规范的。

1
2
3
4
	// stdin should be 0, because no file descriptors are open yet
	if ((r = opencons()) != 0) {
		user_panic("opencons: %d", r);
	}

opencons() 函数打开控制台设备,并返回一个文件描述符。 由于进程初始时没有打开任何文件描述符,第一个打开的文件描述符默认是 0(最小可用值)。 如果返回值不是 0,程序会触发 panic,确保标准输入必须绑定到文件描述符 0。

1
2
3
4
	// stdout
	if ((r = dup(0, 1)) < 0) {
		user_panic("dup: %d", r);
	}

dup(0, 1) 函数将文件描述符 0(已绑定到控制台输入)复制到文件描述符 1。 这使得文件描述符 1 也指向控制台设备,从而实现标准输出的功能。 如果复制失败,程序同样会触发 panic。

Thinking 6.7

外部命令。在我们的spawn中,每次运行都要使用child = syscall_exofork();

cd 命令的作用是改变当前进程的工作目录。

如果像普通外部命令一样,运行时创建新进程,那么它对文件系统的操作就不会影响父进程的环境,当前目录不会改变,这显然不符合用户预期。

Thinking 6.8

下面是运行结果: (我在spawn函数的开头加了一句debugf)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$  ls.b | cat.b > motd
[00003003] pipecreate 
spawn!
spawn!
[00004005] destroying 00004005
[00004005] free env 00004005
i am killed ... 
[00004806] destroying 00004806
[00004806] free env 00004806
i am killed ... 
[00003804] destroying 00003804
[00003804] free env 00003804
i am killed ... 
[00003003] destroying 00003003
[00003003] free env 00003003
i am killed ... 

可以观察到2次spawn,分别对应ls.b与cat.b的启动

可以观察到4次进程销毁,分别对应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
	if (r != 0) {
		debugf("pipe: %d\n", r);
		exit();
	}
	r = fork();   // 为了管道进行一次fork
	if (r < 0) {
		debugf("fork: %d\n", r);
		exit();
	}
	*rightpipe = r;
	if (r == 0) {
		dup(p[0], 0);
		close(p[0]);
		close(p[1]);
		return parsecmd(argv, rightpipe);  // 管道右端再运行一次
	} else {
		dup(p[1], 1);
		close(p[1]);
		close(p[0]);
		return argc;
	}

难点分析

本次作业建立在前面所有lab的基础上,尤其是lab5,需要我们对文件系统足够了解才能顺利搭建管道和SHELL。

管道的核心难点在于pageref的管理,这也是MOS首次出现关于调整先后执行顺序的问题。问题的核心原因在于判断读端或写端是否关闭时,使用了pageref的方法,而这个条件在某些中间状态下并不充分。

SHELL需要我们熟悉流程,关键是parsecmd和spawn两个函数。从SHELL中读取token并解析命令,然后调用spawn对ELF文件进行打开加载运行等等操作。

整体来说,lab6实现的主要是上层建筑,将MOS的功能以可交互的形式展现了出来,不再只能运行既定程序,而是支持了命令行的各项操作。

实验体会

随着lab6的结束,这个学期也要走向了尾声。不过这个博客并不是OS系列的终点,我们还有看起来非常有挑战性的挑战性任务!

OS课程走到现在,虽然做的只是一些补全代码的工作,但是也极大地锻炼了我们阅读代码的能力,尤其是在不能直接Ctrl+Click用跳转到环境下,我们只能使用grep -nr xxx ./去寻找,或者对MOS的架构烂熟于心。

最后感谢课程组的辛苦付出!希望OS课程越办越好!

comments powered by Disqus
Easy Life and Easy Learning
使用 Hugo 构建
主题 StackJimmy 设计