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_sleepkernel/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使用了pipeforkreadwritegetpid等系统调用。在开始我们的代码之前,不妨看看这些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系统调用的handlersys_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.cmain函数。

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输出$ ,然后输入的命令会通过getfd=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 execcmdstruct redircmdstruct pipecmdstruct listcmdstruct 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使用同一文件作为标准输入、标准输出、标准错误,为什么不会相互干扰?

posted @ 2023-01-14 14:09  benoqtr  阅读(285)  评论(0编辑  收藏  举报