🗒️Xv6-Shell

2023-5-16|2023-5-22
Anthony
Anthony
type
status
date
slug
summary
tags
category
icon
password
本Blog基于任职于美国波特兰洲立大学的教授@hhp3
在Youtube上对xv6内核的讲解视频
本期讲解xv6中的用户Shell,视频链接在下面
 
 
XV6仅仅使用430行代码就完成了一个迷你的shell,它支持
  • options pgm -r -i /usr/filename
  • I/O redirection pgm < infile > outfile
  • pipes pgm1|pgm2|pgm3
  • sequencing pgm1;pgm2;pgm3
  • background pgm &
  • parentheses (pgm1;pgm2)|pgm3
 

XV6中提供的系统调用

系统调用为程序的书写提供了方便,在xv6中系统调用一共有11个,可以分成四类
进程控制
  • fork
  • exec
  • exit
  • wait
 
Pipes
  • pipe
  • dup
改变路径
chdir
IO
  • open
  • close
  • read
  • write
 
 

指令解析

在xv6中,shell程序从用户输入读取指令并写道一个大小为100的buffer。接下来解析buffer里面的数据,然后构造解析树。
假设指令(pgm1 -i arg < myFile|pgm2);pgm3 arg3
XV6 shell中的指令解析系统将构造一棵指令树如下:
  • ;
    • |
      • <
        • myFile
        • exec pgm1 -i arg
      • exec pgm2
    • exec pgm3 arg3
在xv6中,指令的解析与运行由两条函数执行。parsecmd函数递归地构造一棵解析树,而runcmd函数则递归地遍历解析树,遇到节点就运行,遇到pipe或者seq等等就fork子进程去执行

指令结构

XV6中定义了用于shell的若干指令的结构,可以在sh.c的头部找到相关定义
  • pipecmd 对于pipe类型的指令,结构体中的整形type 指明了指令类型,然后是两个cmd类型的指针,分别指向pipe指令左边和右边的指令。
  • listcmd 与pipecmd几乎一样,只是type不同
  • backcmd 指令类型为back,所以type应该等于5。同时结构体中的指针指向想要在后台运行的指令
  • execcmd执行指令。其中argv数组中结构为[程序名字,arg1, …, agr8, 结束]。其中eargv储存了指向程序名字或者参数的后一个byte的指针。
notion image
  • redircmd file指向文件名字的开头,efile指向文件名字的结尾。mode为打开文件的方式(READONLY,CREATE…)fd是文件描述符,0为标准输出,1为标准输出。cmd指向左边的运行指令。
notion image
 

Sh.c

sh.c的头部定义了相应指令的结构体pipecmd, execmd…。同时下文也包含了这些结构体的构造函数,比如
 
此外,对于解析指令,xv6中使用parsecmd()函数,以及其相应的帮助函数:parseline,parsepipe,parseredirs,parseblock, parseexec。
函数nullterminate将所有的指令串都以\o为终止符
函数gettoken找到下一个token在指令中出现的位置
函数getcmd在控制台输出一个”$”然后读取用户的输入进入buffer
函数runcmd遍历解析树并执行对应操作
函数panic用来输出错误信息
函数fork1用来创建子进程
 
 

Helper functions

Panic

该函数使用文件描述符“2”输出错误信息,然后exit(1)表示错误情况下的退出。
 

fork1

该函数调用系统调用fork,对于父进程而言fork函数会返回子进程的id,对于子进程而言,fork函数会返回0。
 

Main function

  • open保证每次都返回一个还没有被使用的文件描述符。第一个while循环被用来检查三个文件描述符是否都被打开。
  • 第二个while中的getcmd保证输入的是指令,如果getcmd读到了eof符号,它会返回负一。然后检查输入是否是“cd “也就是改变目录的操作。然后用0来替换用户输入的换行符。然后给系统调用c
  • 接下来调用fork1()创建子进程。对于父进程,它会返回还是的p_id,对于孩子它会返回0。由于子进程才是用来执行指令的,因此我们通过判断fork1()的返回值是不是0来决定要不要runcmd。对于父进程而言,它只需要等待子进程结束即可。
 

runcmd

这个函数太长了,我直接在代码里写注释(除了CASE=PIPE)单独讲
该函数接收一个指向解析树的指针,然后不断地递归执行。
该函数不会有任何的返回值,如果成功执行,它就直接终止进程
该函数的主要部分就是一个switch case判断不同的指令
 

runcmd → pipe

  • 首先类型转换为pcmd
  • 系统调用pipe()
对于pipe的系统调用,我们可以理解为其创建了一个管道,p[0]假设初始是3,指向管道尾端,p[1]假设初始是4指向管道头部
notion image
  • 想象一下,第一个子进程原先的标准输出是控制台,第二个子进程原先的标准输入是控制台,因此我们需要把第一个子进程的标准输出改成第二个子进程的标准输入
notion image
  • 因此在第一个子进程中,我们关闭了标准输出1。使用系统调用dup。dup复制了管道的描述符p[1],然后将它赋值为第一个可用文件描述符(即1)。所以子进程1的标准输出指向管道头部,标准输入仍然是原来的那个。接下来关闭p[0],p[1]也就是把原先的p[0]=3和p[0]=4去掉。最后指向左边的cmd
  • 对于第二个子进程,同理。子进程2的标准输入变成了管道的右边,标准输出还是原来的
  • 对于父亲进程,它要关闭掉自己的管道描述符,然后等待子进程结束
Xv6-LockXV6-FileSystem-Structure