CSAPP: Shell Lab实现思路与细节
目录
题目要求
今天,我们要实现一个简易的shell。通过这个实验,也会让我们对shell这个神秘的东西减少一丝陌生。下面进入正题:
shell程序框架已经帮你搭建好了,我们只需完成以下六个函数:
- eval: 主要对命令行进行分词并执行指令
- builtin_cmd: 检查是否为内置指令
- do_bgfg: 执行fg、bg指令
- waitfg: 等待fg指令执行完毕
- sigchld_handler: SIGCHLD信号处理函数
- sigint_handler: SIGINT信号处理函数(ctrl-c)
- sigtstp_handler: SIGTSTP信号处理函数(ctrl-z)
题目具体的细节要求在指导书中都有,一定要多读指导书和课本,一定要多读指导书和课本!!!
整体思路
整个shell的思路大体上是这样的。
首先我们在命令行中输入指令,eval
函数获取这行命令,其首要任务是调用parseline
函数对命令进行分割,明确指令和参数,并构造最终会传递给execve
的argv向量。
命令行最后一个参数如果是’&'字符,那么该命令为一个后台命令(shell不会等待它完成)。否则需要前台执行该命令(shell会等待它完成)。
当解析参数后,如果命令为quit
、jobs
、bg
、fg
四种题目要求的内置命令,则立刻执行,否则shell创建一个子进程。当进程完成后,回收子进程并进行下一轮迭代。
实现细节
指导书中建议通过使用trace01~trace15一步一步构建shell,我认为这是一个很好的思路,下面我也将按照这一思路构建整个程序。程序在最后会全部贴上,大家可以在测试trace过程中将本文该章节作为参考。
代码的运行结果可以通过输入以下命令查看,其*
为01~15
make rtest*
trace02.txt和trace03.txt
首先我们需要做的,是把书中最简易的shell程序原封不动的敲进去看看运行结果。
trace02.txt
,trace03.txt
这两个文件实现测试了对内置指令quit
的测试,这一部分很简单,和书中相同,调用exit(0)
就能直接实现。
trace04.txt
trace04要求运行一个后台工作,这个指令再不修改书中程序时是能够实现的。
#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &
trace05.txt
真正的问题在测试这个文件时才真正展现。通过调用jobs
,因为完全没有任务管理,且书中程序并不认为jobs
为一个内置命令,会出现严重的错误。
#
# trace05.txt - Process jobs builtin command.
#
/bin/echo -e tsh> ./myspin 2 \046
./myspin 2 &
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo tsh> jobs
jobs
那么,我们的思路是这样的,首先在builtin_cmd
函数中,加入对jobs
指令的检测,接着调用listjobs(jobs);
,这在程序中人家已经给你定义好了。
struct job_t jobs[MAXJOBS]; /* The job list */
void listjobs(struct job_t *jobs);
那么如果要调用该函数,job list也必须进行相应的改变,这一实现必须在eval
的父进程中实现。思路是每执行一个任务,就在job list中添加一项,每结束一个任务,就将对应pid的任务在list中删除。在访问全局变量以及创建子进程的过程中需要将SIGCHLD信号进行阻塞,否则会发生意想不到的错误。思路与书中642页程序大致相同。
trace06.txt
trace05的工作量其实还蛮大的,如果完成后,下面就要开始编写信号处理函数。trace06.txt是对ctrl-c进行处理,即SIGINT信号。它运行了一个./myspin 4
并在两秒后对其中断。那么思路很明显,这部分要让我们编写sigint_handler
函数。
#
# trace06.txt - Forward SIGINT to foreground job.
#
/bin/echo -e tsh> ./myspin 4
./myspin 4
SLEEP 2
INT
其思路就是键盘按下ctrl-c后,进入信号处理函数,然后终止前台正在运行的函数。不过这地方又一个小小的trick,就是在处理函数中,需要获取前台正在运行进程的pid,而访问全局变量时最好对信号量进行阻塞。
trace07.txt
trace07实质上是对前面两个trace的组合,如果之前函数没问题的话,这个部分可以直接通过。jobs
会显示当前运行的程序。
trace08.txt
这一条测试是对前台程序进行停止(挂起),即对sigtstp_handler
函数进行编写。该函数与sigint_handler
几乎一模一样,只是在kill
中的参数不同而已。
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
trace09.txt和trace10.txt
本次作业的又一个难点来了,它是对bg
和fg
指令进行解析并运行。因此,这里需要完成do_fg
函数。该函数需要做到以下几点:
- 对
bg
和fg
命令进行区分 - pid和jib参数的打印与错误提示
该函数的大致思路是这样的
- 首先判断输入指令是否正确
- 获取指令的pid、jid以及当前操作pid号的job结构体指针(这一步异常重要,因为输入
fg/bg %num
指令进行操作时,是无法获取当前指令的pid的,因此更改运行状态等会遇到一些小麻烦,解决的一种方法是自己写一个jid2pid
以及更改对应status
的函数,这里我嫌麻烦直接就获取job的指针直接修改了) - 利用switch语句对几种不同
status
分别进行操作(fg
的实质就是**后台进程在前台等待,bg
只是简单的重启进程即可)
这里会遇到一些细小的问题:
do_bgfg
函数在处理子进程回收上会遇到一个奇怪的问题,当我在mac和linux上运行是,执行fg %1
后程序必然会卡住。在网上搜了许久后,终于找到了答案。
在mac端输入man signal
后会看到
20 SIGCHLD discard signal child status has changed
因此,问题就出在这里,书中的对SIGCHLD
记录是:一个子进程停止或终止。而真实情况是当子进程从stop转换为running时,子进程仍然会发送一个SIGCHLD
信号!!!这就是导致fg
指令后一直卡住的原因。
解决这个问题的方法是创建一个stopped_child_pid
的全局变量,专门用来保存停止状态子进程的pid。在sigchld_handler
函数中专门对该情况进行处理即可。这里参考了这位大佬的解答
trace11~trace15
如果能过顺利完成前10个测试条例,其余的都是对之前的一些组合,基本都能通过测试。
代码
eval
/*
* eval - Evaluate the command line that the user has just typed in
*
* If the user has requested a built-in command (quit, jobs, bg or fg)
* then execute it immediately. Otherwise, fork a child process and
* run the job in the context of the child. If the job is running in
* the foreground, wait for it to terminate and then return. Note:
* each child process must have a unique process group ID so that our
* background children don't receive SIGINT (SIGTSTP) from the kernel
* when we type ctrl-c (ctrl-z) at the keyboard.
*/
void eval(char *cmdline)
{
char *argv[MAXARGS]; /* Arguments list execve() */
char buf[MAXLINE]; /* Holds modified command line */
int bg; /* Should the job run in bg or fg */
pid_t pid; /* Process id */
sigset_t mask; /* mask of signal */
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL) { /* Ignore empty line */
return;
}
if (!builtin_cmd(argv)) { /* not a build line */
/* Block SIGCHLD signal */
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
if ((pid = fork()) == 0) { /* Child runs user job */
/* unblock SIGCHLD signal */
sigprocmask(SIG_UNBLOCK, &mask, NULL);
/* puts the child in a new process group */
setpgid(0, 0);
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* Parent waits for foreground job to terminal or print message of background job */
if (!bg) {
fg_pid = pid;
stopped_child_pid = 0;
/* add job into job list */
addjob(jobs, pid, FG, cmdline);
/* unblock SIGCHLD signal */
sigprocmask(SIG_UNBLOCK, &mask, NULL);
waitfg(pid);
} else {
addjob(jobs, pid, BG, cmdline);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}
return;
}
builtin_cmd
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "&")) { /* Ignore singleton */
return 1;
}
if (!strcmp(argv[0], "quit")) { /* quit command */
exit(0);
}
if (!strcmp(argv[0], "jobs")) { /* jobs command */
listjobs(jobs);
return 1;
}
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) { /* bg or fg command */
do_bgfg(argv);
return 1;
}
return 0; /* Not a builtin command */
}
do_bgfg
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
char *cmd = argv[0];
char *param = argv[1];
struct job_t *job;
sigset_t mask, prev;
int pid, jid;
sigfillset(&mask);
if (param[0] == '%') {
jid = atoi(&(param[1]));
} else {
pid = atoi(&(param[0]));
jid = pid2jid(pid);
}
sigprocmask(SIG_BLOCK, &mask, &prev);
job = getjobjid(jobs, jid);
if (job == NULL) {
param[0] == '%' ? printf("%s", param) : printf("(%s)", param);
printf(": No such process\n");
return;
}
/* bg or fg */
if (!strcmp(cmd, "bg")) { /* bg command */
switch (job->state) {
case ST:
job->state = BG;
stopped_child_pid = job->pid;
kill(-(job->pid), SIGCONT);
printf("Job [%d] (%d) %s", job->jid, job->pid, job->cmdline);
break;
case BG:
break;
case UNDEF:
case FG:
unix_error("error use bg command.");
break;
}
} else {
switch (job->state) { /* fg command */
case ST:
job->state = FG;
stopped_child_pid = job->pid;
kill(-(job->pid), SIGCONT);
waitfg(job->pid);
break;
case BG:
job->state = FG;
stopped_child_pid = job->pid;
waitfg(job->pid);
break;
case FG:
case UNDEF:
unix_error("error use fg command.\n");
break;
}
}
sigprocmask(SIG_SETMASK, &prev, NULL);
return;
}
waitfg
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
while (!stopped_child_pid) {
sleep(1);
}
stopped_child_pid = 0;
return;
}
sigchld_handler
/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig)
{
int olderrno = errno;
if (stopped_child_pid) {
stopped_child_pid = 0;
return;
}
sigset_t mask, prev;
pid_t pid;
int status;
sigfillset(&mask);
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
sigprocmask(SIG_BLOCK, &mask, &prev);
if (pid == fgpid(jobs)) {
stopped_child_pid = 1;
}
if (WIFSTOPPED(status)) {
getjobpid(jobs, pid)->state = ST;
printf("Job [%d] (%d) stopped by signal %d.\n", pid2jid(pid), pid, WSTOPSIG(status));
} else {
if (WIFSIGNALED(status)) {
printf("Job [%d] (%d) terminated by signal %d.\n", pid2jid(pid), pid, WTERMSIG(status));
}
deletejob(jobs, pid);
}
fflush(stdout);
sigprocmask(SIG_SETMASK, &prev, NULL);
}
errno = olderrno;
}
sigtstp_handler
/*
* sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
* the user types ctrl-z at the keyboard. Catch it and suspend the
* foreground job by sending it a SIGTSTP.
*/
void sigtstp_handler(int sig)
{
int olderrno = errno;
sigset_t mask, prev;
pid_t pid;
sigfillset(&mask);
sigprocmask(SIG_BLOCK, &mask, &prev);
pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &prev, NULL);
if (pid != 0) {
kill(-pid, SIGTSTP);
}
errno = olderrno;
return;
}
sigint_handler
/*
* sigint_handler - The kernel sends a SIGINT to the shell whenver the
* user types ctrl-c at the keyboard. Catch it and send it along
* to the foreground job.
*/
void sigint_handler(int sig)
{
int olderrno = errno;
sigset_t mask, prev;
pid_t pid;
sigfillset(&mask);
sigprocmask(SIG_BLOCK, &mask, &prev);
pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &prev, NULL);
if (pid != 0) {
kill(-pid, SIGINT);
}
errno = olderrno;
return;
}
一些疑问
当我在mac上测试trace10.txt
运行完fg %1
后,会出现如下结果
Job [1] (759) stopped by signal 18.
而在linux端则会有
Job [1] (759) stopped by signal 20.
这个地方我一直没搞明白到底是咋回事,借此也问问大家。
最后
这是我第一次编写博客,是对之前学习进行的总结,也希望能把我的思路与理解分享给大家。最后,感谢网络上的各路大佬,没有他们的帮助,我是无法完成本次实验的,最后,再次感谢。
下一篇: axios和drf结合的增删改查