lab6笔记
-
实验目的
pipe
shell
shell中设计pipe“|”的部分
-
管道
是进程间通信的另一种方式,显然是进程间单向通信的一种方式。
mos中pipe的使用与实现
首先分析Pipe这个结构体
struct Pipe {
u_int p_rpos; // read position,下一个将要从管道读的数据的位置
// 只有读者可以更新 p_rpos
u_int p_wpos; // write position,下一个将要向管道写的数据的位置
// 只有写者可以更新 p_wpos
u_char p_buf[PIPE_SIZE]; // data buffer
// 这个 PIPE_SIZE 大小的缓冲区发挥的作用类似于环形缓冲区
// 所以下一个要读或写的位置 i 实际上是 i%PIPE_SIZE
// 所以如果管道数据为空,即当 p_rpos >= p_wpos 时,应该进程切换到写者运行
};
这里就要分析管道的特殊作用。管道是一种只存在于内存中的文件(与磁盘中的文件啥的相对,linux一切皆文件)。这里会同步进行读和写,那么如何做到这一个呢,首先由于不希望进行通信,那么实际上会有同一块内存区域,然后为了考虑同步读和同步写,需要在内存区域中添加读和写的一个指针。
所以如果管道数据为空,即当 p_rpos >= p_wpos
时,应该进程切换到写者运行。
同时写者在写入的时候,也是将数据存入缓冲区,需要注意管道的缓冲区可能出现满溢的情况,所以写者必须得在p_wpos-p_rpos < PIPE_SIZE
时方可运行,否则要一直挂起。
同时为了解决部分死锁的问题,需要知道管道的另一端是否已经关闭,当出现缓冲区为空或满的情况是,要根据另一端是否关闭来判断是否要返回。如果另一端已经关闭,进程返回0即可;如果没有关闭,则切换进程运行。
然后分析一下pipe
函数的具体实现。父子进程的p[0] 和 p[1]访问到的内存区域应该是一致的。
那么在pipe
中是如何实现的呢,
首先通过fd_alloc
和syscall_mem_alloc
两个函数分别创建文件描述符并为文件描述符分配空间
然后给fd0
对应的虚拟地址分配一页物理内存,再将fd1
对应的虚拟地址映射到这一页物理内存。
注意这里创建文件描述符fd0 fd1
和为文件描述符对应的虚拟地址分配物理空间时用到的权限位都是PTE_D|PTE_LIBRARY
,PTE_D
可以理解,表示可写,PTE_LIBRARY
实际上还表达了父子进程可以对这个共享页面直接进行写操作,也就是共享可写页面!
这里讲一下为什么先要分配这三个物理页面
这里需要分析一下
fd_alloc
的实现,这里的关键实际上是*fd = (struct Fd*)va;
也就是说只是找到了一个没有使用过的虚拟地址,但是如果需要使用这个物理地址,是需要对应到实际的物理页面的。同时
fd
也需要对应到实际的物理页面。那么这里就需要创建三个页面了。
在MOS中,使用了_pipe_is_closed()
来判断管道的另一端是否已经关闭。
这个是如何实现的呢,又是什么原理呢。
确定几点,管道只有一端读一端写,每一个匿名管道分配了三夜空间,一页是读数据的文件描述符fd0
,一页是写数据的文件描述符fd1
,同时还有一页被两个文件描述符共享的管道数据缓冲区pipe
。
那么显然有pageref(rfd)+pageref(wfd)=pageref(pipe)
成立。
那么要判断另一端是否已经关闭实际上是判断另一端的pageref
是否为0,那么实际上就是判断pageref(fd) == pageref(pipe)
是否成立。
内核会对
pages
数组成员维护一个页引用变量pp_ref
来记录指向该物理页的虚页数量。
看看pipe_close
这个函数
这里进行了两个syscall_mem_unmap
的操作
目前的
unmap
的顺序是,先取消对
pipe
的映射,然后再取消对文件描述符的映射
在这个情况下我们考虑
pipe(p);
if (fork() == 0) {
close(p[1]);
read(p[0], buf, sizeof(buf));
} else {
close(p[0]);
write(p[1], "Hello", 5);
}
如果执行顺序是
-
子进程
close(p[1]);
-
父进程
close(p[0]);
仅完成了取消p[0]
对pipe
的映射此时每个页面的引用情况为:
pageref(p[1]) = 1;
pageref(p[0]) = 2;
pageref(pipe) = 2;
-
子进程执行read,判断写端已经关闭,因为
pageref(p[0]) == pageref(pipe)
,相当于认为pageref(p[1]) == 0
因此斗胆猜测,如果判断写端关闭的时候把两个都加上是不是就没问题了(x
那么此时
rbuf = (char*)vbuf; for (int 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++; }
那么此时就会发现
p->p_rpos == p->p_wpos
但是写端没有关闭,因此就会进行系统调用,然后父进程继续完成进行,搞定!
这里还解释一下把两个unmap
的顺序调整后可行的原因。
这里考虑的完全是pageref
显然有pageref(pipe) >= pageref(fd)
,而先取消pipe
的映射,可能导致应该是pageref(pipe)>pageref(fd)
变成pageref(pipe)==pageref(fd)
,但是先取消fd
的映射就不可能出现这个情况,因此不会误判另一端已经关闭。
然后就是读的时候也可能因为进程切换出现问题,这里好办,只要判断没有进行进程切换即可,也就是这个过程前后没有发生进程切换,那么用一个全局变量来判断即可,也就是env_runs
,记录了某个进程run
的次数
Shell
Shell程序需要完成的事情就是:不断读取用户的输入,根据命令创建对应的进程,运行对应的程序,同时实现进程间的通信。
通过简单的测试,发现ARGBEGIN ARGEND
可以解析出sh -ix;sh -i; sh -x
中的相应参数。
ARGBEGIN
ARGEND
readline
详细注释版:
void readline(char* buf, u_int n)
{
int r;
for (int i = 0; i < n; i++) {
if ((r = read(0, buf + i, 1)) != 1) {
if (r < 0) {
debugf("read error: %d\n", r);
}
exit();
}
// debugk_user("IN user/sh.c readline(), thel local i is %d",i);
if (buf[i] == '\b' || buf[i] == 0x7f /*DEL (Delete) 符号*/) {
// 因为之后会执行i++操作,表示如果已经在buf中有字符,则回退一个,否则留在原地
if (i > 0) {
i -= 2;
} else {
i = -1;
}
if (buf[i] != '\b') {
// 这个表示将光标左移一位
printf("\b");
}
}
if (buf[i] == '\r' || buf[i] == '\n') {
buf[i] = 0;
return;
}
}
// 理论上应该在上面的'\r' '\n'那里结束,没有结束说明太长了
debugf("line too long\n");
// 读完这一行,并将buf[0]置为'\0',表示为空字符串
while ((r = read(0, buf, 1)) == 1 && buf[0] != '\r' && buf[0] != '\n') {
;
}
buf[0] = 0;
}
_gettoken
函数的作用是: