OS:lab6课下基础

1. 管道

1.1 初窥管道

​ 管道是一种典型的进程间单向通信的方式,分为有名管道和匿名管道两种,匿名管道只能在有公共祖先的进程间使用,在MOS中,我们要实现匿名管道。

管道是一种只存在于内存中的文件,在MOS中,父进程调用pipe函数时会打开两个新的文件描述符:一个表示只读端,一个表示只写端,两个描述符都映射到同一片内存区域。

​ 在fork的配合下,子进程会复制父进程的两个文件描述符,从而在==父子进程间形成了四个(父子各有一读一写)的指向同一片内存区域的文件描述符,父子进程可根据需要关掉自己不用的一个,从而实现单向管道通信。==

1.2 MOS中pipe的使用与实现

​ MOS中pipe的实现

 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
int pipe(int pfd[2]) {
	int r;
	void *va;
	struct Fd *fd0, *fd1;

	/* Step 1: Allocate the file descriptors. */
	if ((r = fd_alloc(&fd0)) < 0 || (r = syscall_mem_alloc(0, fd0, PTE_D | PTE_LIBRARY)) < 0) {
		goto err;
	}

	if ((r = fd_alloc(&fd1)) < 0 || (r = syscall_mem_alloc(0, fd1, PTE_D | PTE_LIBRARY)) < 0) {
		goto err1;
	}

	/* Step 2: Allocate and map the page for the 'Pipe' structure. */
	va = fd2data(fd0);
	if ((r = syscall_mem_alloc(0, (void *)va, PTE_D | PTE_LIBRARY)) < 0) {
		goto err2;
	}
	if ((r = syscall_mem_map(0, (void *)va, 0, (void *)fd2data(fd1), PTE_D | PTE_LIBRARY)) <
	    0) {
		goto err3;
	}

	/* Step 3: Set up 'Fd' structures. */
	fd0->fd_dev_id = devpipe.dev_id;
	fd0->fd_omode = O_RDONLY;

	fd1->fd_dev_id = devpipe.dev_id;
	fd1->fd_omode = O_WRONLY;

	debugf("[%08x] pipecreate \n", env->env_id, vpt[VPN(va)]);

	/* Step 4: Save the result. */
	pfd[0] = fd2num(fd0);
	pfd[1] = fd2num(fd1);
	return 0;

err3:
	syscall_mem_unmap(0, (void *)va);
err2:
	syscall_mem_unmap(0, fd1);
err1:
	syscall_mem_unmap(0, fd0);
err:
	return r;
}
  • 首先为fd0fd1两个文件描述符分配空间(物理页)

  • 然后给fd0对应的虚拟地址分配一页物理内存并将fd1对应的虚拟地址映射到这一页物理内存

  • 通过管道pipe进行共享的实际上是共享页面机制PTE_LIBRARY

image-20240610195357171

image-20240610195405221

1.3 管道的读写

​ 关于管道结构体Pipe的定义如下

1
2
3
4
5
struct Pipe {
	u_int p_rpos;		 // read position
	u_int p_wpos;		 // write position
	u_char p_buf[PIPE_SIZE]; // data buffer
};
  • p_rpos:下一个要从管道读的数据的位置,读者更新

  • p_wpos:下一个要向管道写的数据的位置,写者更新

  • p_buf:缓冲区,类似于环形缓冲区,读写的位置i实际上是i%PAGE_SIZE

​ 当读者从管道读取数据时,要将p_buf[p_rpos%PIPE_SIZE]拷贝走,然后读自增一。==需要注意的是,管道的缓冲区此时可能还没有被写入数据,所以如果管道数据为空,即当p_rpos>=p_wpos时,进程切换到写者运行。==

​ 当写者相管道中写入数据时,将数据存入p_buf[p_wpos%PIPE_SIZE],然后写自增一。==需要注意缓冲区可能存在满溢的情况,所以需要在p_wpos-p_rpos<PIPE_SIZE时方可运行。==

​ 当缓冲区出现空或满的情况时,我们还需要根据另一端是否关闭来判断是否要返回,另一端已经关闭则返回0即可,否则切换进程运行。

​ 判断管道另一端是否关闭_pipe_is_closed:对于一个匿名管道,我们分配了三页空间:读数据的文件描述符rfd一页,写数据的文件描述符wfd一页,剩下一页是被两个文件描述符共享的管道数据缓冲区pipe.

​ 则有pageref(rfd)+pageref(wfd)=pageref(pipe),若另一端关闭则有pageref(rfd)=pageref(pipe)pageref(wfd)==pageref(pipe)

Exercise 6.1 pipe_read, pipe_write, _pipe_is_closed

 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
static int _pipe_is_closed(struct Fd *fd, struct Pipe *p) {
	int fd_ref, pipe_ref, runs;
	/* 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;
}

static int pipe_read(struct Fd *fd, void *vbuf, u_int n, u_int offset) {
	int i;
	struct Pipe *p;
	char *rbuf;
	/* 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 (i >= 1 || _pipe_is_closed(fd, p)) {
				return i;
			} else {
				syscall_yield();
			}
		}
		rbuf[i] = p->p_buf[(p->p_rpos++) % PIPE_SIZE];
	}

	return n;
}

static int pipe_write(struct Fd *fd, const void *vbuf, u_int n, u_int offset) {
	int i;
	struct Pipe *p;
	char *wbuf;
	/* 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;
			} else {
				syscall_yield();
			}
		}
		p->p_buf[(p->p_wpos++) % PIPE_SIZE] = wbuf[i];
	}
	
	return n;
}

1.4 管道关闭的正确判断

MOS采用时间片轮转的进程调度算法,抢占式的进程管理意味着用户进程随时可能会被打断。

​ 由于管道的共享性质,无法保证_pipe_is_closed用于管道另一端的判断一定返回正确的结果。

​ 进程使用pipe_close来关闭管道的端口,该函数的实质是通过两次系统调用unmap来解除文件描述符fd和数据缓存区pipe的映射。由于进程切换的存在,并不能保证两次系统调用可以在==同一进程时间片内被执行==,两次系统调用之间可能因为进程切换而被打断,故fd和对pipe的pp_ref也不能保证同步被写入,影响了管道是否关闭的正确性。

在MOS中只有syscall_*开头的系统调用函数是原子操作

_pipe_is_closed函数返回正确结果的条件为

  • 写端关闭pageref(p[0]) == pageref(pipe)
  • 读端关闭pageref(p[1]) == pageref(pipe)

​ 对于更一般的情况,pageref(p[0]) +pageref(p[1]) = pageref(pipe),**也就是说pipe的引用次数总比pipe要高。**在close函数中

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

​ 先解除pipe映射再解除fd映射,会使pipe引用次数-1先于fd,在两个unmap间隙导致pageref(pipe)=pageref(fd),这一点可以通过控制fdpipemap/unmap顺序解决进程竞争导致的非同步写入问题:使fd的引用次数-1先于pipe,这样即使在两个unmap的间隙,也有pageref(pipe) > pageref(fd)成立。

​ ==或者说改变map/unmap顺序使得pageref(pipe) > pageref(fd)在非管道关闭时恒成立==

Exercise 6.2 调整pipe_close中的umap顺序

  • 先解除fd
  • 再解除pipe
1
2
3
4
5
6
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;
}

Exercise 6.3 调整dup函数中的map顺序

  • 先映射pipe
  • 后映射fd
 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
int dup(int oldfdnum, int newfdnum) {
	int i, r;
	void *ova, *nva;
	u_int pte;
	struct Fd *oldfd, *newfd;

	/* Step 1: Check if 'oldnum' is valid. if not, return an error code, or get 'fd'. */
	if ((r = fd_lookup(oldfdnum, &oldfd)) < 0) {
		return r;
	}

	/* Step 2: Close 'newfdnum' to reset content. */
	close(newfdnum);
	/* Step 3: Get 'newfd' reffered by 'newfdnum'. */
	newfd = (struct Fd *)INDEX2FD(newfdnum);
	/* Step 4: Get data address. */
	ova = fd2data(oldfd);
	nva = fd2data(newfd);
	/* Step 5: Dunplicate the data and 'fd' self from old to new. */
	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;
	}

	return newfdnum;

err:
	/* If error occurs, cancel all map operations. */
	panic_on(syscall_mem_unmap(0, newfd));

	for (i = 0; i < PDMAP; i += PTMAP) {		panic_on(syscall_mem_unmap(0, (void *)(nva + i)));
	}

	return r;
}

2. Shell

shell指“为使用者提供操作界面”的软件,他接受用户的命令然后调用相关的应用程序,MOS实现了CLI shell

2.1 完善spawn函数

spawn函数的作用是帮助我们调用文件系统中的可执行文件并执行(初步观察我们的命令就是通过spawn函数运行的,例如输入ls.b,调用可执行文件ls.b)。

Exercise 6.5 spawn

  • 文件系统打开对应的文件(二进制ELF,*.b)
  • 申请新的进程控制块
  • 将目标程序加载到子进程的地址空间中并为他们分配物理页面
  • 为子进程初始化地址空间
  • 设置子进程的寄存器
  • 将父进程的共享页面映射到子进程的地址空间
  • 设置子进程可执行
  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
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)) {
		if (r >= 0) {
			r = -E_NOT_EXEC;
		}
		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) */
	if ((child = syscall_exofork()) < 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)) < 0) {
		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 = read(fd, elfbuf, ehdr->e_phentsize)) != ehdr->e_phentsize) {
			if (r >= 0) {
				r = -E_NOT_EXEC;
				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) */
			if ((r = read_map(fd, ph->p_offset, &bin)) < 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) */
			if ((r = elf_load_seg(ph, bin, spawn_mapper, &child)) != 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;
}

2.2 解释shell命令

​ 我们在user目录下提供了ls.c , cat.c , echo.c等几个用户程序模拟Linux下同名命令(后续Shell挑战性任务可以参考MIT-xv6中其他命令的实现),当我们输入命令时,Shell程序sh.c调用了spawn函数,使其能够读取相应的可执行文件,并加载到新进程中运行。

​ 解释shell命令通过parsecmd完成,或者说就是解析输入的字符串。

  • 如果碰到重定向符号 < | >
    • 读下一个单词,打开这个单词代表的文件,然后将文件内容复制给标准输入或标准输出
  • 如果碰到管道符号|
    • 首先建立管道pipe,然后fork
    • 对于父进程,将管道的写者复制给标准输出
    • 对于子进程,将管道的读者复制给标准输入

Exercise 6.5 parsecmd

 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
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) */
			if ((fd = open(t, O_RDONLY)) < 0) {
				debugf("failed to open %s\n");
				exit();
			}
			if ((r = dup(fd, 0)) < 0) {
				debugf("failed to duplicate file to <stdin>\n");
				exit();
			}
			close(fd);
			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) */
			if ((fd = open(t, O_WRONLY)) < 0) {
				debugf("failed to open %s\n");
				exit();
			}
			if ((r = dup(fd, 1)) < 0) {
				debugf("failed to duplicate file to <stdout>\n");
				exit();
			}
			close(fd);
			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) */
			if ((r = pipe(p)) < 0) {
				debugf("failed to create pipe\n");
				exit();
			}
			if ((*rightpipe = fork()) == 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;
			}
			break;
		}
	}

	return argc;
}