实验报告
思考题
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课程越办越好!