文章目录
CS:APP Lab 5 : Shell lab
一.Lab要求
本次shell lab要求实现一个简易的shell的功能(tiny shell / tsh).
实验资料中的tsh.c
已经给出了主要框架以及一些辅助函数来帮助我们实现功能.需要完成的是补全内部的7个函数:
void eval(char *cmdline) //解析cmdline并运行
int builtin_cmd(char **argv) //识别出内置命令
void do_bgfg(char **argv) //执行bg和fg命令
void waitfg(pid_t pid) //实现阻塞等待前台程序运行结束
void sigchld_handler(int sig) //SIGCHID信号的处理函数
void sigint_handler(int sig) //SIGINT(ctrl-c)信号的处理函数
void sigtstp_handler(int sig) //SIGTSTP(ctrl-z)信号的处理函数
实验要求最终形成的shell能通过给出的16个test.
二.Lab过程
1.eval函数
eval函数主要是解析cmdline命令行并求值运行.
先调用parseline函数来解析命令行,并得到放在前台还是后台信息;其次根据builtin_cmd函数来执行命令行,若第一个是内置命令,就立即执行,否则就当作一个可执行文件来执行.执行需要创建子进程,然后利用显式阻塞信号的手段来避免父进程子进程竞争,以及前台后台竞争.
以下是实现代码:
void eval(char *cmdline)
{
char *argv[MAXARGS]; //argument array
int state = UNDEF; //job state
pid_t pid; //process id
sigset_t mask_all, mask_one, prev_one; //signal set
/*parseline解析cmdline*/
if(parseline(cmdline, argv) == 1)
state = BG;
else
state = FG;
if(argv[0] == NULL)
return; //cmdline为空直接return
/*根据是否为内置命令进行操作*/
if(!builtin_cmd(argv)) {
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask_one, &prev_one);//block SIGCHLD
if((pid = fork()) == 0){
setpgid(0, 0); //当前进程创建一个进程组
sigprocmask(SIG_SETMASK, &prev_one, NULL);//解除阻塞
if(execve(argv[0], argv, environ) == -1){
printf("%s: Command not found\n",argv[0]);
exit(0);
}
}
sigprocmask(SIG_BLOCK, &mask_all, NULL);//父进程阻塞其他信号
addjob(jobs, pid, state, cmdline);//add the child to the job list
sigprocmask(SIG_SETMASK, &mask_one, NULL);//接触阻塞
if(state == FG)
waitfg(pid);
else{
sigprocmask(SIG_SETMASK, &mask_all, NULL);
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
sigprocmask(SIG_SETMASK, &prev_one, NULL);//接触阻塞
}
return;
}
2.builtin_cmd函数
builtin_cmd函数比较简单,只需要判断第一个参数是否为内置命令,若是,则完成相关行为,并返回1;否则返回0.
实现代码如下:
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0], "quit"))
exit(0);
else if(!strcmp(argv[0], "bg")||!strcmp(argv[0], "fg"))
do_bgfg(argv);
else if(!strcmp(argv[0], "jobs"))
listjobs(jobs);
else
return 0; /* not a builtin command */
return 1; /* is a builtin command */
}
3.do_bgfg函数
do_bgfg函数主要是根据对命令行的解析进行
bg
(通过向其发送SIGCONT信号来重新启动job,然后在后台运行它.job参数可以是 pid 或 jid.)
fg
(命令通过向它发送SIGCONT信号来重新启动job,然后在前台运行它.job参数可以是 pid 或 jid.)
主要思路是:
- 解析fg还是bg
- 检查fg/bg的参数
- 发送信号给进程组,并执行bg/fg的功能
实现代码如下:
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
struct job_t *job;
/*判别是fg/bg*/
int job_state = (!strcmp(argv[0], "bg")) + 1;
/*参数为空*/
if(argv[1] == NULL){
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
/*检查fg/bg的参数*/
int is_pid = (argv[1][0] >= '0' && argv[1][0] <='9');//是pid
int is_jid = (argv[1][0] == '%');//是jobid
if(!is_pid && !is_jid){
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
if(is_pid){
if((job = getjobpid(jobs, atoi(argv[1]))) == NULL){
printf("(%d): No such process\n", atoi(argv[1]));
return;
}
}
else if(is_jid){
if((job = getjobjid(jobs, atoi(argv[1]+1))) == NULL){
printf("%s: No such job\n",argv[1]);
return;
}
}
/*针对fg/bg情况进行操作*/
job->state = job_state; //设置状态
kill(-job->pid, SIGCONT);//采用负数发送信号给整个进程组,重启
if(job_state == BG)
printf("[%d] (%d) %s",job->jid,job->pid,job->cmdline);
else if(job_state == FG)
waitfg(job->pid);
else{
printf("do_bgfg: Internal error\n");
exit(0);
}
return;
}
4.waitfg函数
waitfg函数是等待一个前台作业结束,阻塞一个前台的进程直到这个进程变为后台进程.
实现代码如下:
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
sigset_t mask,prev;
sigemptyset(&mask);
struct job_t *job = getjobpid(jobs, pid);
while(job->state == FG){
sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
}
return;
}
5.sigchld_handler函数
sigchld_handler函数为信号处理函数,当子进程由于:正常退出,有一个未被捕获的信号,当前为停止状态 这三种原因而终止时,会向父进程发送sigchld信号,本函数应针对三种情况来删除作业.
实现代码如下:
void sigchld_handler(int sig)
{
int olderrno = errno;
int status;
pid_t pid;
struct job_t *job;
sigset_t mask,prev;
sigfillset(&mask);
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
sigprocmask(SIG_BLOCK, &mask, &prev);
job = getjobpid(jobs, pid);
if(WIFEXITED(status)) //正常退出
deletejob(jobs, pid);
else if(WIFSIGNALED(status)){ //收到信号退出
printf("Job [%d] (%d) terminated by signal %d\n", job->jid, job->pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if(WIFSTOPPED(status)){ //停止
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, job->pid, WSTOPSIG(status));
job->state = ST;
}
sigprocmask(SIG_SETMASK, &prev, NULL);
}
errno = olderrno;
return;
}
6.sigint_handler函数
sigint_handler函数处理sigint信号,调用kill函数将sigint信号发给整个进程组.
实现代码如下:
void sigint_handler(int sig)
{
int olderrno = errno;
pid_t fg_pid = fgpid(jobs);
if(fg_pid){
kill(-fg_pid,sig);
}
errno = olderrno;
return;
}
7.sigtstp_handler函数
这个和上面那个一样.
void sigtstp_handler(int sig)
{
int olderrno = errno;
pid_t fg_pid = fgpid(jobs);
if(fg_pid){
kill(-fg_pid,sig);
}
errno = olderrno;
return;
}
最终测试
利用助教老师写的脚本来检测如下,全部通过:
脚本代码如下:
#!/usr/bin/python3
# run driver on all traces and compare results against the reference
import subprocess as sp
import re
prog = "./sdriver.pl"
cmd = lambda t, s: " ".join([prog, "-t", t, "-s", s, "-a", "'-p'"])
def test(i):
mine = sp.Popen(cmd("trace%s.txt" % i, "./tsh"), stdout=sp.PIPE, shell=True)
ref = sp.Popen(cmd("trace%s.txt" % i, "./tshref"), stdout=sp.PIPE, shell=True)
try:
mine.wait(timeout=10)
except sp.TimeoutExpired:
print("Tsh timeout (failed to exit itself)")
mine.terminate(), ref.terminate()
return False
mout, _ = mine.communicate() # stdout, stderr
rout, _ = ref.communicate()
# handle output of 'ps a'
def cut(lines):
ps = []
for j in range(len(lines)):
if not lines[j].startswith(" "):
continue
for k in range(j, len(lines)):
if lines[k].find("mysplit") != -1:
# match patterns like " 35089 pts/10 "
pat = r"\s*\d+\s[a-z]+/\d+\s+"
# extract state of a mysplit process
ps.append(re.sub(pat, "", lines[k]))
lines = lines[:j]
break
return lines, ps
mout, mps = cut(mout.decode().split('\n'))
rout, rps = cut(rout.decode().split('\n'))
if len(mout) != len(rout):
print("tsh result:\n", "\n".join(mout))
print("tshref result:\n", "\n".join(rout))
print("Number of lines different from ref")
return False
for l1, l2 in zip(mout, rout):
# ignore pid
cl1 = re.sub(r"\([0-9]+\)", "", l1)
cl2 = re.sub(r"\([0-9]+\)", "", l2)
if cl1 != cl2:
print("Line\n'%s'\nshould be\n'%s'" % (l1, l2))
return False
# ‘ps a’ results have some randomness,
# so check them manually
if mps or rps:
print("mysplit entries in 'ps a':")
print("STAT TIME COMMAND")
if mps == rps:
print("\n".join(mps))
else:
print("tsh's:")
print("\n".join(mps))
print("tshref's:")
print("\n".join(rps))
print("Passed test %s" % i)
return True
# run test on all 16 traces
for i in range(16):
i = "0" + str(i+1)
if not test(i[-2:]):
break
三.Lab总结
本实验通过制作一个简易的Unix shell,来帮助我们更加熟悉过程控制和信号这些概念,涉及到异常控制流,进程,系统调用,信号处理函数等多方面知识.其中,通过显式阻塞信号以及事后恢复来恰当处理好并发错误是个关键的问题.完成本实验,加强了对异常控制流和Uinx shell的理解,认识和应用.
参考博客: