BUAA OS LAB6 实验报告

Thinking

Thinking 6.1

Q:

示例代码中,父进程操作管道的写端,子进程操作管道的读端。

如果现在想 让父进程作为“读者”,代码应当如何修改?

A:

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], "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);
}

Thinking 6.2

Q:

1
2
3
4
5
6
7
8
pipe(p);
if(fork() == 0 ){
close(p[1]);
read(p[0],buf,sizeof buf);
}else{
close(p[0]);
}
write(p[1],"Hello",5);

• 假设 fork 结束后,子进程先执行。时钟中断产生在 close(p[1]) 与 read 之间,父进 程开始执行。

• 父进程在 close(p[0]) 过程中,已经解除了 p[0]对 pipe的映射(unmap),还没有来得 及解除对 p[0] 的映射。假设这时时钟中断产生,进程调度后子进程接着执行。

• 注意此时各个页的引用情况:pageref(p[0]) = 2(因为父进程还没有解除对 p[0] 的映 射),而 pageref(p[1]) = 1(因为子进程已经关闭了 p[1])。但注意,此时 pipe的 pageref 是2,子进程中 p[0]引用了 pipe,同时父进程中 p[0]刚解除对 pipe的映射,所以在 父进程中也只有 p[1]引用了 pipe。

• 子进程执行 read,read中首先判断写者是否关闭。比较 pageref(pipe)与 pageref(p[0]) 之后发现它们都是2,说明写端已经关闭,于是子进程退出。

上面这种不同步修改 pp_ref 而导致的进程竞争问题在 user/lib/fd.c 中 的dup 函数中也存在。

请结合代码模仿上述情景,分析一下我们的 dup函数中为什么会出 现预想之外的情况?

A:

之前定义的dup 函数默认行为先映射文件描述符,再映射文件内容,如果在映射完文件描述符后,进程在此时中断,另一个程序会认为文件映射完成,发生错误。

Thinking 6.3

Q:

阅读上述材料并思考:为什么系统调用一定是原子操作呢?

如果你觉得不是 所有的系统调用都是原子操作,请给出反例。希望能结合相关代码进行分析说明。

A:

1
2
3
4
5
6
7
.macro CLI
mfc0 t0, CP0_STATUS
li t1, (STATUS_CU0 | 0x1)
or t0, t1
xor t0, 0x1
mtc0 t0, CP0_STATUS
.endm

根据这一段代码可以看出,操作系统在系统调用后会屏蔽中断,因此系统调用期间不会被中断,是原子操作。

Thinking 6.4

Q:

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

• 按照上述说法控制 pipe_close中 fd和 pipeunmap的顺序,是否可以解决上述场 景的进程竞争问题?给出你的分析过程。

• 我们只分析了 close时的情形,在 fd.c中有一个 dup函数,用于复制文件描述符。 试想,如果要复制的文件描述符指向一个管道,那么是否会出现与 close类似的问 题?请模仿上述材料写写你的理解。

A:

1.可以解决,对于这样的写法:

1
2
3
4
5
6
7
c
static int pipe_close(struct Fd *fd) {
// Unmap 'fd' and the referred Pipe.
syscall_mem_unmap(0, fd);
syscall_mem_unmap(0, (void *)fd2data(fd));
return 0;
}

不难分析得出必然存在不等式page_ref(fd) <= page_ref(pipe),而当我们像上面这样设置unmap操作的顺序的话,在两次unmap中间对pipe_close进行中断的话,必然有page_ref(fd) < page_ref(pipe)成立,因此不会在这个过程中对管道的开关过程发生误判。

2.会出现问题。如果先对fd进行map后对page进行map的话,会诱发page_ref(fd) == page_ref(pipe)这样的情况,造成误判。

Thinking 6.5

Q:

• 认真回看Lab5文件系统相关代码,弄清打开文件的过程。

• 回顾Lab1与Lab3,思考如何读取并加载ELF文件。

• 在Lab1 中我们介绍了 data text bss 段及它们的含义,data 段存放初始化过的全 局变量,bss段存放未初始化的全局变量。关于memsize和filesize,我们在Note 1.3.4中也解释了它们的含义与特点。关于Note1.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数据分配空 间。

A:

第一问

user/lib/files.c文件中的open函数调用同文件夹下的fsipc open函数,fsipc open通过调用fsipc函数向服务进程进行进程间通信,并接收返回的消息。而相应的文件系统服务进程的serve_open函数调用file_open对文件进行打开操作,最终通过进程间通信实现与用户进程对文件描述符的共享。

第二问

kern/env.c文件中的load_icode函数实现。

第三问

  • elf_load_seg函数:在该函数处理程序的循环中,当处理到.bss段时,该函数会调用map_page把相应的虚拟地址映射到物理页上,但不会从文件中加载数据。而在map_page的内部会调用load_icode_mapper函数将页面进行映射并根据参数将内容置为0;
  • load_icode_mapper函数:当处理到.bss段时,不从源数据中复制任何内容。最终调用page_insert函数将其置入页表并指定权限。该函数会根据传入的参数在页表项中建立映射关系,并初始化页面为0.

Thinking 6.6

Q:

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

A:

在文件user/sh.b中,我们可以找到代码段:

1
2
3
4
5
6
7
8
9
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);
}

在shell进程初始化的时候,由我们规定文件描述符0是标准输入而文件描述符1是标准输出。

Thinking 6.7

Q:

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

A:

在文件shell.cmain函数中有如下代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c
for (;;) {
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);
}
}

可知在MOS中我们用到的shell命令除了echocmds和注释两种情况外,都需要fork一个子shell来处理输入的命令,是内置命令。

Linux的cd指令使用频率较高,若设置为外部指令必然会在cd的时候多次调用fork生成子进程,这显然是低效的。将其设置为内部指令可以切实提高我们操作系统的效率。

Thinking 6.8

Q:

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

  • 请问你可以在你的 shell 中观察到几次 spawn ?分别对应哪个进程?
  • 请问你可以在你的 shell 中观察到几次进程销毁?分别对应哪个进程?

A:

直接运行该命令得到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tex
[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 ...

通过debugf打log得到输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tex
[00002803] is forked in shell! [1]
[00002803] pipecreate
00002803 forked 00003004
[00002803] is calling a spawn function
[00003004] is calling a spawn function
00002803 spawns 00003805
00003004 spawns 00004006
[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 ...

也就是说整个总共spawn了两次,分别是由最初被fork出的2803进程spawn出了3805进程,以及3004(被2803进程在parsecmdfork得到)进程spawn出了4006进程。

第二问

观察到了四次进程销毁,下面分别介绍:

  • 2803进程:由主shell进程fork出来的子shell进程,用于解析并执行当前命令;
  • 3004进程:由2803进程fork出来的子进程,用于解析并执行管道右端的命令;
  • 3805进程:由2803进程spawn出来的子进程,用于执行管道左边的命令;
  • 4006进程:由3004进程spawn出来的子进程,用于执行管道右边的命令;

lab6难点

T1

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
static int _pipe_is_closed(struct Fd *fd, struct Pipe *p) {
// The 'pageref(p)' is the total number of readers and writers.
// The 'pageref(fd)' is the number of envs with 'fd' open
// (readers if fd is a reader, writers if fd is a writer).
//
// Check if the pipe is closed using 'pageref(fd)' and 'pageref(p)'.
// If they're the same, the pipe is closed.
// Otherwise, the pipe isn't closed.

int fd_ref, pipe_ref, runs;
// Use 'pageref' to get the reference counts for 'fd' and 'p', then
// save them to 'fd_ref' and 'pipe_ref'.
// Keep retrying until 'env->env_runs' is unchanged before and after
// reading the reference counts.
/* Exercise 6.1: Your code here. (1/3) */
do {
runs = env->env_runs;
fd_ref = pageref(fd);
pipe_ref = pageref(p);
} while (runs != env->env_runs);

return fd_ref == pipe_ref;
}

/* Overview:
* Read at most 'n' bytes from the pipe referred by 'fd' into 'vbuf'.
*
* Post-Condition:
* Return the number of bytes read from the pipe.
* The return value must be greater than 0, unless the pipe is closed and nothing
* has been written since the last read.
*
* Hint:
* Use 'fd2data' to get the 'Pipe' referred by 'fd'.
* Use '_pipe_is_closed' to check if the pipe is closed.
* The parameter 'offset' isn't used here.
*/
static int pipe_read(struct Fd *fd, void *vbuf, u_int n, u_int offset) {
int i;
struct Pipe *p;
char *rbuf;

// Use 'fd2data' to get the 'Pipe' referred by 'fd'.
// Write a loop that transfers one byte in each iteration.
// Check if the pipe is closed by '_pipe_is_closed'.
// When the pipe buffer is empty:
// - If at least 1 byte is read, or the pipe is closed, just return the number
// of bytes read so far.
// - Otherwise, keep yielding until the buffer isn't empty or the pipe is closed.
/* Exercise 6.1: Your code here. (2/3) */
p = (struct Pipe *)fd2data(fd);
rbuf = (char *)vbuf;
for (i = 0; i < n; ++i) {
while (p->p_rpos == p->p_wpos) {
if (_pipe_is_closed(fd, p) || i > 0) {
return i;
}
syscall_yield();
}
rbuf[i] = p->p_buf[p->p_rpos % PIPE_SIZE];
p->p_rpos++;
}
return n;
// user_panic("pipe_read not implemented");
}

/* Overview:
* Write 'n' bytes from 'vbuf' to the pipe referred by 'fd'.
*
* Post-Condition:
* Return the number of bytes written into the pipe.
*
* Hint:
* Use 'fd2data' to get the 'Pipe' referred by 'fd'.
* Use '_pipe_is_closed' to judge if the pipe is closed.
* The parameter 'offset' isn't used here.
*/
static int pipe_write(struct Fd *fd, const void *vbuf, u_int n, u_int offset) {
int i;
struct Pipe *p;
char *wbuf;

// Use 'fd2data' to get the 'Pipe' referred by 'fd'.
// Write a loop that transfers one byte in each iteration.
// If the bytes of the pipe used equals to 'PIPE_SIZE', the pipe is regarded as full.
// Check if the pipe is closed by '_pipe_is_closed'.
// When the pipe buffer is full:
// - If the pipe is closed, just return the number of bytes written so far.
// - If the pipe isn't closed, keep yielding until the buffer isn't full or the
// pipe is closed.
/* Exercise 6.1: Your code here. (3/3) */
p = (struct Pipe *)fd2data(fd);
wbuf = (char *)vbuf;
for (i = 0; i < n; ++i) {
while (p->p_wpos - p->p_rpos == PIPE_SIZE) {
if (_pipe_is_closed(fd, p)) {
return i;
}
syscall_yield();
}
p->p_buf[p->p_wpos % PIPE_SIZE] = wbuf[i];
p->p_wpos++;
}
// user_panic("pipe_write not implemented");

return n;
}

T2

1
2
3
4
5
6
7
static int pipe_close(struct Fd *fd) {
// Unmap 'fd' and the referred Pipe.
syscall_mem_unmap(0, fd);
syscall_mem_unmap(0, (void *)fd2data(fd));
// syscall_mem_unmap(0, fd);
return 0;
}

T3

交换顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
}
}
}

if ((r = syscall_mem_map(0, oldfd, 0, newfd, vpt[VPN(oldfd)] & (PTE_D | PTE_LIBRARY))) <
0) {
goto err;
}

T4

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
int spawn(char *prog, char **argv) {
// Step 1: Open the file 'prog' (the path of the program).
// Return the error if 'open' fails.
int fd;
if ((fd = open(prog, O_RDONLY)) < 0) {
return fd;
}

// Step 2: Read the ELF header (of type 'Elf32_Ehdr') from the file into 'elfbuf' using
// 'readn()'.
// If that fails (where 'readn' returns a different size than expected),
// set 'r' and 'goto err' to close the file and return the error.
int r;
u_char elfbuf[512];
/* Exercise 6.4: Your code here. (1/6) */
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;

// Step 3: Create a child using 'syscall_exofork()' and store its envid in 'child'.
// If the syscall fails, set 'r' and 'goto err'.
u_int child;
/* Exercise 6.4: Your code here. (2/6) */
child = syscall_exofork();
if (child < 0) {
r = child;
goto err;
}

// Step 4: Use 'init_stack(child, argv, &sp)' to initialize the stack of the child.
// 'goto err1' if that fails.
u_int sp;
/* Exercise 6.4: Your code here. (3/6) */
if ((r = init_stack(child, argv, &sp))) {
goto err1;
}

// Step 5: Load the ELF segments in the file into the child's memory.
// This is similar to 'load_icode()' in the kernel.
size_t ph_off;
ELF_FOREACH_PHDR_OFF (ph_off, ehdr) {
// Read the program header in the file with offset 'ph_off' and length
// 'ehdr->e_phentsize' into 'elfbuf'.
// 'goto err1' on failure.
// You may want to use 'seek' and 'readn'.
/* Exercise 6.4: Your code here. (4/6) */
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;
// Read and map the ELF data in the file at 'ph->p_offset' into our memory
// using 'read_map()'.
// 'goto err1' if that fails.
/* Exercise 6.4: Your code here. (5/6) */
r = read_map(fd, ph->p_offset, &bin);
if (r != 0) {
goto err1;
}

// Load the segment 'ph' into the child's memory using 'elf_load_seg()'.
// Use 'spawn_mapper' as the callback, and '&child' as its data.
// 'goto err1' if that fails.
/* Exercise 6.4: Your code here. (6/6) */
r = elf_load_seg(ph, bin, spawn_mapper, &child);
if (r != 0) {
goto err1;
}
}
}
close(fd);

struct Trapframe tf = envs[ENVX(child)].env_tf;
tf.cp0_epc = entrypoint;
tf.regs[29] = sp;
if ((r = syscall_set_trapframe(child, &tf)) != 0) {
goto err2;
}

// Pages with 'PTE_LIBRARY' set are shared between the parent and the child.
for (u_int pdeno = 0; pdeno <= PDX(USTACKTOP); pdeno++) {
if (!(vpd[pdeno] & PTE_V)) {
continue;
}
for (u_int pteno = 0; pteno <= PTX(~0); pteno++) {
u_int pn = (pdeno << 10) + pteno;
u_int perm = vpt[pn] & ((1 << PGSHIFT) - 1);
if ((perm & PTE_V) && (perm & PTE_LIBRARY)) {
void *va = (void *)(pn << PGSHIFT);

if ((r = syscall_mem_map(0, va, child, va, perm)) < 0) {
debugf("spawn: syscall_mem_map %x %x: %d\n", va, child, r);
goto err2;
}
}
}
}

if ((r = syscall_set_env_status(child, ENV_RUNNABLE)) < 0) {
debugf("spawn: syscall_set_env_status %x: %d\n", child, r);
goto err2;
}
return child;

err2:
syscall_env_destroy(child);
return r;
err1:
syscall_env_destroy(child);
err:
close(fd);
return r;
}

T5

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
int parsecmd(char **argv, int *rightpipe) {
int argc = 0;
while (1) {
char *t;
int fd, r;
int c = gettoken(0, &t);
switch (c) {
case 0:
return argc;
case 'w':
if (argc >= MAXARGS) {
debugf("too many arguments\n");
exit();
}
argv[argc++] = t;
break;
case '<':
if (gettoken(0, &t) != 'w') {
debugf("syntax error: < not followed by word\n");
exit();
}
// Open 't' for reading, dup it onto fd 0, and then close the original fd.
// If the 'open' function encounters an error,
// utilize 'debugf' to print relevant messages,
// and subsequently terminate the process using 'exit'.
/* Exercise 6.5: Your code here. (1/3) */
fd = open(t, O_RDONLY);
if (fd < 0) {
debugf("failed to open '%s'\n", t);
exit();
}
dup(fd, 0);
close(fd);
// user_panic("< redirection not implemented");

break;
case '>':
if (gettoken(0, &t) != 'w') {
debugf("syntax error: > not followed by word\n");
exit();
}
// Open 't' for writing, create it if not exist and trunc it if exist, dup
// it onto fd 1, and then close the original fd.
// If the 'open' function encounters an error,
// utilize 'debugf' to print relevant messages,
// and subsequently terminate the process using 'exit'.
/* Exercise 6.5: Your code here. (2/3) */
fd = open(t, O_WRONLY | O_CREAT | O_TRUNC);
if (fd < 0) {
debugf("failed to open '%s'\n", t);
exit();
}
dup(fd, 1);
close(fd);
// user_panic("> redirection not implemented");

break;
case '|':;
/*
* First, allocate a pipe.
* Then fork, set '*rightpipe' to the returned child envid or zero.
* The child runs the right side of the pipe:
* - dup the read end of the pipe onto 0
* - close the read end of the pipe
* - close the write end of the pipe
* - and 'return parsecmd(argv, rightpipe)' again, to parse the rest of the
* command line.
* The parent runs the left side of the pipe:
* - dup the write end of the pipe onto 1
* - close the write end of the pipe
* - close the read end of the pipe
* - and 'return argc', to execute the left of the pipeline.
*/
int p[2];
/* Exercise 6.5: Your code here. (3/3) */
r = pipe(p);
if (r != 0) {
debugf("pipe: %d\n", r);
exit();
}
r = 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;
}
// user_panic("| not implemented");

break;
}
}

return argc;
}