MIT 6.1810 Lab: Xv6 and Unix utilities
lab网址:https://pdos.csail.mit.edu/6.828/2022/labs/util.html
xv6Book:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf
Boot xv6
这部分主要完成系统的启动,环境配置可以看这篇
然后学习了一下git
的使用,推荐一个可视化git
教程,可以理解一下git
各个命令的效果
[https://learngitbranching.js.org/]接下来使用make qemu
可以自动编译你写的代码,并启动qemu
。make grade
可以给你写的代码打分,具体的使用./grade-lab-util codeyouedit
可以给单独的代码打分。
sleep
这个代码很简单主要是理解从用户空间到内核空间执行一个代码的过程。
在user/
文件夹中创建sleep.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
if(argc != 2){
fprintf(2, "Usage: sleep <seconds>\n");
exit(1);
}
int mseconds=atoi(argv[1]);
sleep(mseconds);
exit(0);
}
这个代码主要就是读取用户参数,传递给sleep
函数。sleep
函数在user/user.h
中声明,具体的函数定义则通过其他模块的动态连接最终合成可执行文件_sleep
。
接下来看user/usys.S
这个文件,这个汇编文件是通过perl
程序生成的。
# generated by usys.pl - do not edit
#include "kernel/syscall.h"
.global fork
fork:
li a7, SYS_fork
ecall
ret
······
.global sleep
sleep:
li a7, SYS_sleep
ecall
ret
······
这里的sleep
就是上面那个sleep
函数的定义,就是说执行sleep
时,就是执行这段汇编代码。可以看到在用户空间sleep
函数的任务就是将SYS_sleep
放入a7
中,然后中断。SYS_sleep
在kernel/syscall.h
中宏定义为一个系统调用号。可以看到这个usys.S
文件也包含了这个头文件。
······
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
······
接下来在kernel/syscall.c
中,可以看到SYS_sleep
转换为了对应的sys_sleep
,这里的对应关系还不是很理解,好像下一个lab
有相应的内容。
// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};
//kernel/syscall.c
最后就来到了sys_sleep
这个内核函数。
uint64
sys_sleep(void)
{
int n;
uint ticks0;
argint(0, &n);
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(killed(myproc())){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
//kernel/sysproc.c
观察到这个内核函数的参数是void
,但是通过argint(0, &n)
获取刚才用户空间传递的参数。
$ ./grade-lab-util sleep
make: 'kernel/kernel' is up to date.
== Test sleep, no arguments == sleep, no arguments: OK (0.9s)
== Test sleep, returns == sleep, returns: OK (1.0s)
== Test sleep, makes syscall == sleep, makes syscall: OK (1.0s)
pingpong
系统调用
按照提示,pingpong
使用了pipe
、fork
、read
、write
、getpid
等系统调用。在开始我们的代码之前,不妨看看这些UNIX
系统调用的实现原理。
pipe
pipe
在用户空间user/user.h
被声明为int pipe(int*)
。同样的,按照上文的中断方法,最终到达内核空间/kernel/sysfile.c
中的sys_pipe
。
uint64
sys_pipe(void)
{
uint64 fdarray; // user pointer to array of two integers
struct file *rf, *wf;
int fd0, fd1;
struct proc *p = myproc();
argaddr(0, &fdarray);
if(pipealloc(&rf, &wf) < 0)
return -1;
fd0 = -1;
if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
if(fd0 >= 0)
p->ofile[fd0] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
p->ofile[fd0] = 0;
p->ofile[fd1] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
return 0;
}
这段代码大致的作用就是,首先创建两个struct file
的指针*rf, *wf
,在这两个file
之间通过pipealloc
创建管道,创建成功之后为这两个文件申请fd
,并把这两个fd
放入用户函数pipe
传递的指针数组int *
中。
这里的copyout
完成最后放入的过程,它的作用是将内核数据拷贝到用户进程,可以查看它的声明copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
,在当前情景sys_pipe
下,pagetable
为用户进程的页表,dstva
为用户进程的虚拟地址,src
为内核准备复制的起始指针,len
为要复制的长度。
fork
同样的,fork
用户函数由sys_fork
接收,sys_fork
跳转到内核函数fork
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
release(&np->lock);
acquire(&wait_lock);
np->parent = p;
release(&wait_lock);
acquire(&np->lock);
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
fork
通过allocproc
获取一个新的进程描述符nb
,通过uvmcopy
拷贝页表内容,注意这里拷贝的不是页表指针,父子进程通过相同的页表条目来保证一致性。接下来,复制了保存的寄存器、返回pid
、复制打开文件并增加的引用计数、维护父子关系并设置进程状态。
write
首先是write
系统调用的handler
,sys_write
uint64
sys_write(void)
{
struct file *f;
int n;
uint64 p;
argaddr(1, &p);
argint(2, &n);
if(argfd(0, 0, &f) < 0)
return -1;
return filewrite(f, p, n);
}
完成参数传递,然后执行内核函数filewrite
int
filewrite(struct file *f, uint64 addr, int n)
{
int r, ret = 0;
if(f->writable == 0)
return -1;
if(f->type == FD_PIPE){
ret = pipewrite(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
return -1;
ret = devsw[f->major].write(1, addr, n);
} else if(f->type == FD_INODE){
// write a few blocks at a time to avoid exceeding
// the maximum log transaction size, including
// i-node, indirect block, allocation blocks,
// and 2 blocks of slop for non-aligned writes.
// this really belongs lower down, since writei()
// might be writing a device like the console.
int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
int i = 0;
while(i < n){
int n1 = n - i;
if(n1 > max)
n1 = max;
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
f->off += r;
iunlock(f->ip);
end_op();
if(r != n1){
// error from writei
break;
}
i += r;
}
ret = (i == n ? n : -1);
} else {
panic("filewrite");
}
return ret;
}
这里判断文件的类型,如果是PIPE
则执行pipewrite
,如果是DEVICE
则执行devsw
。这里只分析一下用INODE
描述的普通文件。通过writei
向该文件的inode
结构体f->ip
,执行写操作。
int
writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
{
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off)
return -1;
if(off + n > MAXFILE*BSIZE)
return -1;
for(tot=0; tot<n; tot+=m, off+=m, src+=m){
uint addr = bmap(ip, off/BSIZE);
if(addr == 0)
break;
bp = bread(ip->dev, addr);
m = min(n - tot, BSIZE - off%BSIZE);
if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
brelse(bp);
break;
}
log_write(bp);
brelse(bp);
}
if(off > ip->size)
ip->size = off;
// write the i-node back to disk even if the size didn't change
// because the loop above might have called bmap() and added a new
// block to ip->addrs[].
iupdate(ip);
return tot;
}
getpid
uint64
sys_getpid(void)
{
return myproc()->pid;
}
myproc
通过mycpu
获取struct cpu
,进而获得当前进程的struct proc
,从而获得pid
。
更改文件标准输入输出
在学习xv6文档的时候发现一个有意思的用法
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if(fork() == 0) {
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);
} else {
close(p[0]);
write(p[1], "hello world\n", 12);
close(p[1]);
}
当子进程close(0)
关闭标准输入之后,那么如果这个进程打开了新的文件,那么这个新的文件的fd
就会被分配为0,也就是成立新的标准输入。这里打开新文件,可以说open
也可以是dup
等形式。在更改了标准输入后,exec
新程序,那么这个新程序的标准输入就是新的文件了。
pingpong实现
pingpong
的实现并不复杂,建立两个管道,一个父进程读子进程写,另一个子进程读父进程写。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
if(argc > 1){
fprintf(2,"no argv");
exit(1);
}
int pwcr[2];//parent write child read
int prcw[2];
pipe(pwcr);
pipe(prcw);
int pid;
pid = fork();
if(pid == 0){//child process
char crstr[10];
if(read(pwcr[0],crstr,5)==5){
fprintf(1,"%d: received ping\n",getpid());
write(prcw[1],"ok",2);
exit(0);
}
}
write(pwcr[1],"hello",5);
char prstr[10];
if(read(prcw[0],prstr,2)==2){
fprintf(1,"%d: received pong\n",getpid());
exit(0);
}
exit(0);
}
测试可以顺利通过
$ ./grade-lab-util pingpong
make: 'kernel/kernel' is up to date.
== Test pingpong == pingpong: OK (0.7s)
primes
Your goal is to use pipe and fork to set up the pipeline. The first process feeds the numbers 2 through 35 into the pipeline. For each prime number, you will arrange to create one process that reads from its left neighbor over a pipe and writes to its right neighbor over another pipe. Since xv6 has limited number of file descriptors and processes, the first process can stop at 35.
这道题一开始没有读懂题目,这道题实际上是想说,可以回忆一下质数算法,因为如果发现了一个质数,那么该质数的倍数都不会是质数。因此,对于每个质数,可以创造一个进程,将队列中该质数倍数的数剔除,然后传递给下一个进程。通常这个过程我们习惯于使用递归来处理,而这道题目则要创建新的进程。我在递归的基础上做简单修改,也顺利的完成了测试。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int nextprocess(int pp_read){
int pid=fork();
if(pid>0){
wait((int *)0);
exit(0);
}
int prime;
int cnt=read(pp_read,&prime,4);
if(!cnt){
exit(0);
}
fprintf(1,"prime %d\n",prime);
int pp[2];
pipe(pp);
int num;
while(read(pp_read,&num,4)){
if(num%prime!=0)
write(pp[1],&num,4);
}
close(pp[1]);
nextprocess(pp[0]);
exit(0);
}
int
main(int argc, char *argv[])
{
if(argc > 1){
fprintf(2,"no argv");
exit(1);
}
int pp[2];
pipe(pp);
//this is init process
for(int i=2;i<=35;i++){
write(pp[1],&i,4);
}
close(pp[1]);
nextprocess(pp[0]);
exit(0);
}
$ ./grade-lab-util primes
make: 'kernel/kernel' is up to date.
== Test primes == primes: OK (4.7s)
P.S. 这个代码没有在IDE上写,直接运行了一下,报错直接满天飞,好在gcc给的错误提示还是非常清晰的,修改了之后,就直接通过了。
find
路径处理分析
首先,我们研究一下输入的路径会怎样被处理。
当我们该程序传递一个路径之后,在用户程序中,往往会使用fd = open(path, 0)
,以只读的方式打开文件。接着,我们转到对应的sys_open()
。
uint64
sys_open(void)
{
char path[MAXPATH];
int fd, omode;
struct file *f;
struct inode *ip;
int n;
argint(1, &omode);
if((n = argstr(0, path, MAXPATH)) < 0)
......
} else {
if((ip = namei(path)) == 0){
end_op();
return -1;
}
......
sys_open
使用namei(path)
来获取该路径的inode
结构。namei
作为包装函数,实际上调用namex
完成这一过程。
static struct inode*
namex(char *path, int nameiparent, char *name)
{
struct inode *ip, *next;
if(*path == '/')
ip = iget(ROOTDEV, ROOTINO);
else
ip = idup(myproc()->cwd);
while((path = skipelem(path, name)) != 0){
ilock(ip);
if(ip->type != T_DIR){
iunlockput(ip);
return 0;
}
if(nameiparent && *path == '\0'){
// Stop one level early.
iunlock(ip);
return ip;
}
if((next = dirlookup(ip, name, 0)) == 0){
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = next;
}
if(nameiparent){
iput(ip);
return 0;
}
return ip;
}
我们可以观察到,如果传递的path
为 '/',则会从根目录开始路径访问,否则从当前进程的目录开始路径访问。skipelem
用于逐一读取路径参数。我们试着分析路径为.
、./
、../
会发生什么。
static char*
skipelem(char *path, char *name)
{
char *s;
int len;
while(*path == '/')
path++;
if(*path == 0)
return 0;
s = path;
while(*path != '/' && *path != 0)
path++;
len = path - s;
if(len >= DIRSIZ)
memmove(name, s, DIRSIZ);
else {
memmove(name, s, len);
name[len] = 0;
}
while(*path == '/')
path++;
return path;
}
该函数用于解析用户输入的path
,会去除多余的斜杠,将当前路径的第一个文件名放入name
中,并移动path
指针到路径的下一个文件名处。因此.
与..
,(后面有多少斜杠无影响),会被当作一个文件名放入name
中,并传递给dirlookup(ip, name, 0)
,dirlookup
会遍历当前目录,将文件项的名字与传入的做比较,返回新的inode
,返回值用于更新当前位置ip
。因此用户输入路径中,.
会留着当前目录,..
会到上一级目录
因此题目也提醒我们,不要去递归目录中的.
与..
。
find实现
通过改写/user/ls.c
,增加了目录递归,可以顺利通过。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
char*
fmtname(char *path)
{
char *p;
// Find first character after last slash.
for(p=path+strlen(path); p >= path && *p != '/'; p--)
;
p++;
return p;
}
void
find(char* path,char* name)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
if((fd = open(path, 0)) < 0){
fprintf(2, "ls: cannot open %s\n", path);
return;
}
if(fstat(fd, &st) < 0){
fprintf(2, "ls: cannot stat %s\n", path);
close(fd);
return;
}
switch(st.type){
case T_DEVICE:
case T_FILE:
if(!strcmp(fmtname(path),name))
printf("%s\n", path);
break;
case T_DIR:
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("find: path too long\n");
break;
}
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/';
while(read(fd, &de, sizeof(de)) == sizeof(de)){
if(de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
int rfd;
struct stat rst;
if((rfd = open(buf, 0)) < 0){
printf("find: cannot open %s\n", buf);
close(rfd);
continue;
}
if(fstat(rfd, &rst) < 0){
printf("find: cannot stat %s\n", buf);
close(rfd);
continue;
}
if(!strcmp(fmtname(buf),name))
printf("%s\n", buf);
if(rst.type == T_DIR && strcmp(fmtname(buf),".") && strcmp(fmtname(buf),"..")){
printf("next %s\n", buf);
char next_path[512];
strcpy(next_path,buf);
find(next_path,name);
}
else
close(rfd);
}
break;
}
close(fd);
}
int
main(int argc, char *argv[])
{
if(argc != 3){
fprintf(2,"usage: find dir name");
exit(0);
}
find(argv[1],argv[2]);
exit(0);
}
$ ./grade-lab-util find
make: 'kernel/kernel' is up to date.
== Test find, in current directory == find, in current directory: OK (1.6s)
== Test find, recursive == find, recursive: OK (1.1s)
xargs
管道符
xargs
作为Unix标准,用于给命令添加参数,例如echo hello too
将输出通过管道符发送xargs
,然后xargs
去执行echo bye hello too
得到输出。
$ echo hello too | xargs echo bye
bye hello too
首先我需要解决我的一个疑问,管道符是如何工作的,是直接将前一个命令的输出文件直接作为后一个命令的标准输入吗,如果是这样那么比如执行echo xxx | grep x ./file
时,grep
会用哪一个作为输入文件。因为这个问题跟shell紧密相关,有关shell的学习我放到了后文optional处。在学习完成后,可以了解到管道符是通过改变标准输入来影响程序的。因此echo xxx | grep x ./file
的输入文件,要看grep
程序的具体编写方法。
在xv6的grep
中,当argc <= 2
时,就从标准输入读,否则就尝试打开参数的文件来读取。
if(argc <= 2){
grep(pattern, 0);
exit(0);
}
for(i = 2; i < argc; i++){
if((fd = open(argv[i], 0)) < 0){
printf("grep: cannot open %s\n", argv[i]);
exit(1);
}
grep(pattern, fd);
close(fd);
}
xargs实现
从标准输入读取数据,每读到一个'\n'
,执行一条命令,这并不困难。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"
int
main(int argc, char *argv[])
{
char buf[512];
char* s=buf;
char* nargv[MAXARG];
for(int i=0;i<argc-1;i++){
char* na=malloc(sizeof(**argv));
strcpy(na,argv[i+1]);
nargv[i]=na;
}
memset(buf,0,512);
while(read(0,s,1)){
if(*s=='\n'){
*s=0;
if(fork()==0){
char* na=malloc(sizeof(**argv));
strcpy(na,(const char*)buf);
nargv[argc-1]=na;
nargv[argc]=0;
exec(nargv[0],nargv);
}
wait((int *)0);
memset(buf,0,512);
s=buf-1;
}
s++;
}
exit(0);
}
$ ./grade-lab-util xargs
make: 'kernel/kernel' is up to date.
== Test xargs == xargs: OK (0.8s)
optional
题目希望我们可以完善一下shell的功能,这里我对shell保留历史比较感兴趣,但是没有什么思路,就先分析了一下这个简易shell的代码
shell 分析
main函数
我们首先看一下/user/sh.c
的main
函数。
int
main(void)
{
static char buf[100];
int fd;
// Ensure that three file descriptors are open.
while((fd = open("console", O_RDWR)) >= 0){
if(fd >= 3){
close(fd);
break;
}
}
// Read and run input commands.
while(getcmd(buf, sizeof(buf)) >= 0){
if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
// Chdir must be called by the parent, not the child.
buf[strlen(buf)-1] = 0; // chop \n
if(chdir(buf+3) < 0)
fprintf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0)
runcmd(parsecmd(buf));
wait(0);
}
exit(0);
}
首先保证console
被打开3次,且fd
的使用为0、1、2,作为标准输入、标准输出和标准错误。然后调用getcmd
读取命令,我们简单看一下源码。
int
getcmd(char *buf, int nbuf)
{
write(2, "$ ", 2);
memset(buf, 0, nbuf);
gets(buf, nbuf);
if(buf[0] == 0) // EOF
return -1;
return 0;
}
首先向fd=2
输出$
,然后输入的命令会通过get
从fd=0
读取传递给buf
。这里的疑问就是,0、1、2代表的都是同一个文件,他们之间不会互相干扰吗?我们接着看main
函数的主循环,这里集成了cd
的功能,如果不是cd
命令,就fork
一个子进程调用runcmd
运行命令,并且永远不会返回,这里的疑问是父进程仍然会等待子进程的结束才结束,这样每执行一次命令,就fork
一个进程,不会导致资源浪费吗?
指令解析
接下来我们看一下main
函数中调用的parsecmd
函数,这个函数用于在执行命令前,对输入的命令(刚刚储存在buf
中)进行处理。
parsecmd(char *s)
{
char *es;
struct cmd *cmd;
es = s + strlen(s);
cmd = parseline(&s, es);
peek(&s, es, "");
if(s != es){
fprintf(2, "leftovers: %s\n", s);
panic("syntax");
}
nulterminate(cmd);
return cmd;
}
parseline(&s, es)
用于解析命令为结构体struct cmd
,这里在c语言中,使用了类的多态思想,struct cmd
为基类,该结构体中只有type
一个数据,其派生类有struct execcmd
、struct redircmd
、struct pipecmd
、struct listcmd
、struct backcmd
,在使用时,可以通过读取struct cmd*
类型的指针的type
,将指针转换为相应的类型。peek(&s, es, "")
在这里可以用于去除字符串s
前的空白字符。因为字符串s
已经解析完了,去掉空白字符后,仍然s != es
,就会输出错误。nulterminate
用于给结构体中所有的字符串命令添加'\0'
。
下面我们看一下parseline
作为解析命令的前导函数。
struct cmd*
parseline(char **ps, char *es)
{
struct cmd *cmd;
cmd = parsepipe(ps, es);
while(peek(ps, es, "&")){
gettoken(ps, es, 0, 0);
cmd = backcmd(cmd);
}
if(peek(ps, es, ";")){
gettoken(ps, es, 0, 0);
cmd = listcmd(cmd, parseline(ps, es));
}
return cmd;
}
ps
为指向字符串的指针,es
为结束字符指针。首先判断命令中的管道。然后判断命令中是否有"&"
,如果有就通过“后台执行”结构体存储。接着判断命令中是否有;
,如果有就创建listcmd
,并解析后文命令。这里似乎不能区别&
与&&
?我们接下来看parsepipe
。
struct cmd*
parsepipe(char **ps, char *es)
{
struct cmd *cmd;
cmd = parseexec(ps, es);
if(peek(ps, es, "|")){
gettoken(ps, es, 0, 0);
cmd = pipecmd(cmd, parsepipe(ps, es));
}
return cmd;
}
parseexec
会处理好指令群组()
,否则会取出在|)&;
这些特殊字符前的命令作为执行指令。如果有"|"
,则会创建管道指令。管道指令的创建只是通过一个结构体,分别储存管道左和管道右指令的指针。那么到这里,指令的解析就完成了,接下来,我们来看指令的执行。
指令执行
void
runcmd(struct cmd *cmd)
{
int p[2];
struct backcmd *bcmd;
struct execcmd *ecmd;
struct listcmd *lcmd;
struct pipecmd *pcmd;
struct redircmd *rcmd;
if(cmd == 0)
exit(1);
switch(cmd->type){
default:
panic("runcmd");
case EXEC:
ecmd = (struct execcmd*)cmd;
if(ecmd->argv[0] == 0)
exit(1);
exec(ecmd->argv[0], ecmd->argv);
fprintf(2, "exec %s failed\n", ecmd->argv[0]);
break;
case REDIR:
rcmd = (struct redircmd*)cmd;
close(rcmd->fd);
if(open(rcmd->file, rcmd->mode) < 0){
fprintf(2, "open %s failed\n", rcmd->file);
exit(1);
}
runcmd(rcmd->cmd);
break;
case LIST:
lcmd = (struct listcmd*)cmd;
if(fork1() == 0)
runcmd(lcmd->left);
wait(0);
runcmd(lcmd->right);
break;
case PIPE:
pcmd = (struct pipecmd*)cmd;
if(pipe(p) < 0)
panic("pipe");
if(fork1() == 0){
close(1);
dup(p[1]);
close(p[0]);
close(p[1]);
runcmd(pcmd->left);
}
if(fork1() == 0){
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
runcmd(pcmd->right);
}
close(p[0]);
close(p[1]);
wait(0);
wait(0);
break;
case BACK:
bcmd = (struct backcmd*)cmd;
if(fork1() == 0)
runcmd(bcmd->cmd);
break;
}
exit(0);
}
指令的执行由runcmd
完成。这个函数由父进程fork的子进程执行,并且永远不会返回。该函数会判断指令的类型,作为最普通的指令,执行指令会通过exec
直接执行。exec
作为内核函数,也永远不会返回,如果返回了就会打印执行错误。LIST
指令,储存为二叉树结构,会通过fork
执行指令,并通过wait
保证前后顺序。
case PIPE:
......
if(fork1() == 0){
close(1);
dup(p[1]);
close(p[0]);
close(p[1]);
runcmd(pcmd->left);
}
if(fork1() == 0){
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
runcmd(pcmd->right);
}
......
pipe指令,我们发现确实是通过将前一个程序的标准输出作为后一个程序的标准输入,来实现管道符的。
问题思考
1.我们输入的命令和shell的输出是怎么显示在屏幕上的。我们可以看到shell也是打开了同一个文件console
作为标准输入、标准输出、标准错误的,console是一个什么文件,它与shell的关系是什么?
2.观察shell的main函数,似乎在shell完成命令后,就退出了,但我们在使用时,shell一直存在,而且环境变量也是在运行期间一直存在的。
3.shell使用同一文件作为标准输入、标准输出、标准错误,为什么不会相互干扰?