banner
Hi my new friend!

OS-Lab6

Scroll down

OS-Lab6

一、思考题

Thinking 6.1

代码修改如下:

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
#include <stdlib.h>
#include <unistd.h>

int fildes[2];
char buf[100];
int status;

int main(){

status = pipe(fildes);

if (status == -1 ) {
printf("error\n");
}


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("child-process read:%s",buf); /* 打印读到的数据 */
close(fildes[0]); /* 读取结束,关闭读端 */
exit(EXIT_SUCCESS);
}
}

Thinking 6.2

分析:

dup函数的作用是将olddfnum代表的文件描述符指向的数据完全复制给newfdnum文件描述符。首先将newfd所在的虚拟页映射到oldfd所在的物理页,之后将newfd的数据所在的虚拟页映射到oldfd的数据所在的物理页。

设计代码如下:

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

分析过程:

  • 子进程进行,但在执行read函数前发生时钟中断,换至父进程开始执行。
  • 父进程执行dup函数,并完成了对p[0]的映射,但在完成对pipe的映射前发生时钟中断。
  • 换至子进程执行,子进程执行read函数,此时pageref(p[0]) = 3(由于p[0]本身ref为2且在dup函数后加1),pageref(p[1]) = 1(close(p[1])使其ref减1),pageref(pipe) = 2 + 1 = 3(pipe在dup中尚未映射),pageref(p[0]) = pageref(pipe) ,子进程判断写端关闭,子进程退出。

Thinking 6.3

系统调用是原子操作。在宏SAVE_ALL中通过设置CP0_STATUS寄存器关闭中断,从而在用户执行系统调用过程中不会因中断而导致其他用户程序执行,因此系统调用是原子操作。

Thinking 6.4

  • 调换顺序可以解决上述问题。
  • 分析:由于pageref(fd) 始终小于等于pageref(pipe), 因此若先解除fd的映射,pageref(fd) 会变得更小,就不会出现pageref(fd) == pageref(pipe)的情况,因此也就不会出现进程竞争问题。
  • dup函数也会出现类似close的问题。如果在dup函数中先映射fd,再映射pipe,若在dup进行到一半即只映射了fd还没有映射pipe时切换进程,就很可能导致pageref(fd) == pageref(pipe)的情况出现,而调换fd和pipe的映射顺序就可以解决这个问题。

Thinking 6.5

用户进程尝试打开一个文件,运行open函数,open函数会调用fsipc_open和fsipc_map函数,这些函数会调用fsipc函数向文件系统服务进程发送请求,文件系统服务进程接到请求时摆脱阻塞态,运行serve_open函数,该函数会调用file_open函数,最终打开成功。

读取ELF文件使用lab1中实现的readelf函数。load_icode 函数会从 ELF 文件中解析出每个 segment 的段头 ph,以及其数据在内存中的起始位置 bin,再由 elf_load_seg 函数将参数指定的程序段(program segment)加载到进程的地址空间中。

在elf_load_seg中,如果该段在文件中的内容的大小达不到为填入这段内容新分配的页面大小,即分配了新的页面但没能填满(如 .bss 区域),那么余下的部分用 0 来填充。代码如下:

1
2
3
4
5
6
7
/* Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`. */
while (i < sgsize) {
if ((r = map_page(data, va + i, 0, perm, NULL, MIN(bin_size - i, BY2PG))) != 0) {
return r;
}
i += BY2PG;
}

Thinking 6.6

user/init.c中:

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

尚无文件描述符打开,因此标准输入是0。通过dup将1设置为标准输出。

Thinking 6.7

  • MOS中的shell命令是外部命令。因为shell在执行命令是会fork一个子进程即子shell来执行这条命令。
  • linux中的内部命令属于shell程序的一部分,这些命令被写进原码中,在linux系统启动时就加载进内存中,在调用时被shell识别并在其内部运行。

Thinking 6.8

有2次spawn。对应ls.b和cat.b的执行进程。

有2次进程销毁。对应ls.b和cat.b的执行进程。

二、实验难点

1.管道内存分配

子进程和父进程都分别对写端和读段分配有一个虚拟页,但是子进程的写端和读端都分别映射到了同一个物理页。并且管道文件也仅使用一个物理页,因此整个管道只需要3个物理页。

2.spawn函数流程

  1. 调用open函数打开从shell指定的ELF文件。
  2. 通过系统调用syscall_exofork函数为子进程创建进程控制块。
  3. 调用init_stack 函数为子进程栈空间初始化。
  4. 使用elf_load_seg 函数将 ELF 文件的各个段加载进子进程。
  5. 设置子进程的运行现场寄存器。
  6. 将父进程的共享页面映射给子进程。
  7. 使用系统调用 syscall_set_env_status 唤醒子进程。

三、心得体会

通过本次实验,我了解了管道的底层逻辑实现以及创建基于MOS的shell程序。实验难度并不是很高,但是也需要阅读一定量的代码,收获很多。

其他文章
cover
OS_Lab4_Challenge
  • 23/06/14
  • 19:35
  • 2.6k
  • 9