Shell-lab 实验记录¶
新年好!在兔年的前几天我竟然在肝Shell lab(DDL要临近了~) 在此我就记录一下自己实验的流程吧。
实验前准备¶
话不多说,首先还是要看一下官方的文档(http://csapp.cs.cmu.edu/3e/shlab.pdf)并且温习一下课本第八章的知识。
甚至CMU的课件里就有这次实验内容的大致实现(PPT15-ecf-signals) (当然是有缺陷版本啦)
实验目标¶
在这个实验中要干什么?Shell Lab 要求实现一个带有作业控制的 Unix Shell 程序,需要考虑基础的并发,进程控制以及信号和信号处理。
我们要实现以下7个函数
eval:解析命令行,并派生子进程执行builtin_cmd:检测是否为内置命令quit、fg、bg、jobsdo_bgfg:实现内置命令bg和fgwaitfg:等待前台作业执行完成 (阻塞,直到一个进程不在前台运行)sigchld_handler:处理SIGCHLD信号,即子进程停止或者终止sigint_handler:处理SIGINT信号,即来自键盘的中断ctrl-csigtstp_handler:处理SIGTSTP信号,即来自终端的停止信号ctrl-z
下面是CMU文档中的一些额外的补充点
-
Shell 的提示标记符为 "tsh> "
-
用户键入的命令行应包括命令名称和 0 个或多个参数,所有参数以一个或多个空格分隔。如果 name 是一个内置命令,则 tsh 应该立即处理它并等待下一个命令行。否则,tsh 假定该名称是一个可执行程序的路径,并在在一个初始子进程的上下文中加载并运行
- tsh 不需要支持管道(|)或 I/O 重定向(< 和 >)
- 键入 ctrl-c(ctrl-z)应该会发送一个 SIGINT(SIGTSTP)信号给当前的前台作业以及它的子进程。如果没有前台作业,则该信号应该没有任何作用
- 如果命令行以 & 结束,则 tsh 应该在后台运行该作业。否则,它应该在前台运行
- 每个作业都可以由一个进程 ID(PID)或一个作业 ID(JID)标识,该 ID 是一个由 tsh 分配的正整数。 JID应该在命令行上以前缀 “%” 表示。例如,“%5” 表示 JID 5,“ 5” 表示 PID 5
- tsh 应该支持以下内置命令:
- quit:终止 tsh 程序。
- jobs:列出所有后台作业
- bg
:向作业其发送 SIGCONT 信号来重新启动 ,然后在后台运行 -
fg
:向作业发送 SIGCONT 信号来重新启动 ,然后在前台运行 -
tsh 必须回收所有的僵死进程
辅助函数¶
- int main(int argc,char **argv) 主函数,我们需要看到eval来执行命令行
- int parseline(const char cmdline, char *argv)解析命令行参数,如果后台运行则返回 1,我们需要注意返回值
- void initjobs(struct job_t *jobs) 初始化任务队列
- void clearjob(struct job_t *job) 清空任务队列
- int maxjid(struct job_t *jobs) 获得最大的jid
- int addjob(struct job_t jobs,pid_t pid,int state,char cmdline)添加任务
- int deletejob(struct job_t *jobs,pid_t pid)删除任务
- pid_t fgpid(struct job_t *jobs)获得前台运行的pid
- struct job_t getjobpid(struct job_t jobs,pid_t pid)通过pid获得job
- struct job_t getjobjid(struct job_t jobs,int jid)通过jid获得job
- int pid2jid(pid_t pid) pid转化为jid
- void listjobs(struct job_t *jobs)列出任务
实验测试¶
- 每完成
tsh.c的一个功能,重新make后,进入./tsh,测试功能是否正常。具体用来测试的语句可参考tracexx.txt文件。 - 执行
make test01,比对与给定的参考程序tshref执行make rtest01的输出是否一致,依次检查完16个trace文件。
实验文件还包括了作者制作的参考 tsh,以及 16 个测试用例,通过跑用例查看结果与参考 tsh 是否相同,就能判断我们写的 tsh 有无问题。
知识回顾¶
回收子进程¶
一个终止了但是还未被回收的进程称为僵死进程。对于一个长时间运行的程序(比如 Shell)来说,内核不会安排init进程去回收僵死进程,而它虽不运行却仍然消耗系统资源,因此实验要求我们回收所有的僵死进程。
waitpid是一个非常重要的函数,一个进程可以调用waitpid函数来等待它的子进程终止或停止,从而回收子进程,在本实验大量用到,我们必须学习它的用法:
这个函数用来挂起调用进程的执行,直到pid对应的等待集合的一个子进程的改变才返回,包括三种状态的改变:
- 子进程终止
- 子进程收到信号停止
- 子进程收到信号重新执行
如果一个子进程在调用之前就已经终止了,那么函数就会立即返回,否则,就会阻塞,直到一个子进程改变状态。
等待集合以及监测那些状态都是用函数的参数确定的,函数定义如下:
pid_t waitpid(pid_t pid, int *wstatus, int options);
各参数含义及使用
- pid:判定等待集合成员
- pid > 0 : 等待集合为 pid 对应的单独子进程
- pid = -1: 等待集合为所有的子进程
- pid < -1: 等待集合为一个进程组,ID 为 pid 的绝对值
- pid = 0 : 等待集合为一个进程组,ID 为调用进程的 pid
- options:修改默认行为
- WNOHANG:集合中任何子进程都未终止,立即返回 0
- WUNTRACED:阻塞,直到一个进程终止或停止,返回 PID
- WCONTINUED:阻塞,直到一个停止的进程收到 SIGCONT 信号重新开始执行
-
也可以用或运算把 options 的选项组合起来。例如 WNOHANG | WUNTRACED 表示:立即返回,如果等待集合中的子进程都没有被停职或终止,则返回值为 0;如果有一个停止或终止,则返回值为该子进程的 PID
-
statusp:检查已回收子进程的退出状态
- waitpid 会在 status 中放上关于导致返回的子进程的状态信息。

并发编程原则¶
这里仅列出在本实验中用到的原则,后面的解析也会大量提及
- 注意保存和恢复 errno。很多函数会在出错返回式设置 errno,在处理程序中调用这样的函数可能会干扰主程序中其他依赖于 errno 的部分,解决办法是在进入处理函数时用局部变量保存它,运行完成后再将其恢复
- 访问全局数据时,阻塞所有信号。这里很容易理解,不再解释了
- 不可以用信号来对其它进程中发生的事情计数。未处理的信号是不排队的,即每种类型的信号最多只能有一个待处理信号。举例:如果父进程将要接受三个相同的信号,当处理程序还在处理一个信号时,第二个信号就会加入待处理信号集合,如果此时第三个信号到达,那么它就会被简单地丢弃,从而出现问题
- 注意考虑同步错误:竞争。
竞争¶
在如下例子中

父进程在一个全局列表中记录子进程,并设置了一个 SIGCHLD 处理程序来回收子进程,乍一看没问题,但是考虑如下可能的事件序列:
- 第 29 行,创建子进程运行
- 假设子进程在父进程运行到 32 行,即运行
addjob函数之前就结束了,并发送一个 SIGCHLD 信号 - 父进程接收到信号,运行信号处理程序,调用
deletejob函数,而这个job本来就没有添加入列表 - 返回父进程,调用
addjob函数,而这个子进程已经终止并回收了,job已经不存在了
也就是说,在这里,deletejob函数的调用发生在了addjos之前,导致错误。我们称addjob和deletejob存在竞争。
解决办法也就很容易想到了,即在父进程folk之前就阻塞 SIGCHLD 信号:

这样,父进程在fork后,addjob前一定不会处理 SIGCHLD 信号,保证了addjob一定在deletejob之前执行
显式地等待信号¶
考虑如下代码:
while(fgpid(jobs) != 0)
pause();
这里会有一个竞争,并且它可能引起致命的错误!考虑如下事件序列:
- 父进程调用
fgpid函数,此时有一个子进程仍然在前台运行,所以判断条件为真,进入循环 - 假定父进程在进入循环后,而执行
pause前,子进程终止 - 父进程接收到 SIGCHLD 信号,并处理结束后才调用
pause
由于pause仅在捕捉到信号后返回,而之后不会再有任何信号抵达,那么父进程就会永远休眠。
解决办法是用sleep函数:因为它不依赖信号来返回,通过每次循环来监测子进程状态
也可以用sigsuspend函数,这个函数相当于如下代码:
sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
在调用sigsuspend之前阻塞 SIGCHLD 信号,调用时又通过sigprocmask函数,在执行pause函数之前解除对信号的阻塞,从而正常休眠。有同学可能会问了:这里并没有消除竞争啊?如果在第 1 行和第 2 行之间子进程终止不还是会发生永久休眠吗?
这就是sigsuspend与上述代码的不同之处了,它相当于上述代码的原子版本,即第 1 行和第 2 行总是一起发生的,不会被中断。
实验过程¶
eval¶
这算是最需要好好考虑的函数了。这个函数功能是解析命令行后,判断为内置命令还是程序路径,分别执行。如果是前台作业,则要等待其完成,如果是后台作业,则要输出其相应信息。
在以上的过程中,我们需要考虑显式阻塞的安排,考虑什么地方需要阻塞什么信号。
课本上是有eval的缺陷版的,其缺陷在于无法回收僵尸子进程。
在 Write UP 中有提示:
After the fork, but before the execve, the child process should call setpgid(0, 0), which puts the child in a new process group whose group ID is identical to the child’s PID. This ensures that there will be only one process, your shell, in the foreground process group.
注释已经写得很详细了,这里用到了消除竞争的方法。
为什么要在执行前创建新线程组?这里主要是为了将子进程组与 tsh 进程组分开,防止发信号终止子进程组时也将 tsh 进程组终止了
/*
* 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)
{
/* $begin handout */
char *argv[MAXARGS]={NULL}; /* argv for execve() */
int bg; /* should the job run in bg or fg? */
pid_t pid; /* process id */
sigset_t mask; /* signal mask */
/*解析命令行*/
bg = parseline(cmdline, argv);
if (argv[0] == NULL)return;//空行,直接返回
if (!builtin_cmd(argv))
{
//这里需要一些技巧,就是在我们将job加到list之前一定要阻塞SIGCHLD,SIGINT,SIGTSTP
//这消除了在添加任务到列表和SIGCHLD,SIGINT,SIGTSTP信号抵达的竞争问题
if (sigemptyset(&mask) < 0)unix_error("sigemptyset error");
if (sigaddset(&mask, SIGCHLD))unix_error("sigaddset error");
if (sigaddset(&mask, SIGINT)) // SIGINT:Ctrl+c
unix_error("sigaddset error");
if (sigaddset(&mask, SIGTSTP)) // SIGTSTP:Ctrl+z
unix_error("sigaddset error");
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)unix_error("sigprocmask error");
//child
if ((pid = fork()) < 0)unix_error("fork error");
if (pid == 0){
//先解除堵塞
sigprocmask(SIG_UNBLOCK, &mask, NULL);
//每一个新job一定要有一个新的进程组id
//否则内核就会将ctrl-c,ctrl-z信号发给所有的job
if (setpgid(0, 0) < 0)//改变进程的进程组,不要跟tsh进程在一个进程组,然后调用exevce函数执行相关的文件。
unix_error("setpgid error");
//执行
if (execve(argv[0], argv, environ) < 0){
printf("%s: Command not found\n", argv[0]);
exit(0);//子线程执行完毕后一定要退出,否则当execve函数无法执行的时候,子进程开始运行主进程的代码,出现不可预知的错误。
}
}
//parent
//parent先添加job,然后解锁信号以便再次运行
addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
if (!bg)
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
return;
}
builtin_cmd¶
这个函数就是简简单单判断是否为内置命令,不断地strcmp即可。
/*
* 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")) //命令行为内置命令quit,直接退出
exit(0);
else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // 命令行为内置命令bg或fg,调用do_bgfg函数执行,并返回1(表示执行命令为内置命令)
{
do_bgfg(argv);
return 1;
}
else if (!strcmp(argv[0], "jobs")) // 命令行为内置命令jobs,调用listjobs函数执行,并返回1(表示执行命令为内置命令)
{
listjobs(jobs);
return 1;
}
return 0; /* not a builtin command */ // 命令非内置命令,返回0
}
do_bgfg¶
这个代码的难点主要在于命令行参数的判断。
-
区分bg和fg命令,以及传入pid或者jid参数对应的进程的状态。前者if,后者switch就可以包括所用的情况
-
注意用户输入错误处理,比如参数数量不够或者参数传入错误的情况
注意原来的后台进程可能会变成前台进程,所以应修改job结构体的相应信息
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
struct job_t *jobp = NULL;
if (argv[1] == NULL){//没带参数
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (isdigit(argv[1][0])){ //pid
pid_t pid = atoi(argv[1]);
if (!(jobp = getjobpid(jobs, pid)))
{
printf("(%d): No such process\n", pid);
return;
}
}
else if (argv[1][0] == '%'){ //jid
int jid = atoi(&argv[1][1]);
if (!(jobp = getjobjid(jobs, jid))){
printf("%s: No such job\n", argv[1]);
return;
}
}else{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//bg
if (!strcmp(argv[0], "bg")){
if (kill(-(jobp->pid), SIGCONT) < 0)unix_error("kill (bg) error");
jobp->state = BG;
printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);
}
//fg
else if (!strcmp(argv[0], "fg")){
if (kill(-(jobp->pid), SIGCONT) < 0)unix_error("kill (fg) error");
jobp->state = FG;
waitfg(jobp->pid);
}else{
printf("do_bgfg: Internal error\n");
exit(0);
}
return;
}
waitfg¶
这个函数的实现与"显式地等待信号"密切相关,前面已介绍,此处不多描述了,想看具体的还在课本。
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
sigset_t mask;
sigemptyset(&mask);
while (fgpid(jobs) != 0){
sigsuspend(&mask);//暂停时取消阻塞
}
return;
/*
while (pid == fgpid(jobs)) // 等待前台程序不再是pid(fgpid函数用于获取此时正在运行的前台进程)
{
sleep(1); // 休眠1s
}
return;
*/
}
sigchld_handler¶
实现一个 SIGCHLD 信号处理函数,能够回收所有僵死进程。
这里用到了并发编程原则 1,后面就是利用waitpid函数,检测子进程的退出状态来实现相应操作,如回收子进程,打印相关信息等等。
在这个过程中,我们要对原始errno值进行保护,最后恢复errno。
循环的判断条件是(pid = waitpid(-1, &status, WUNTRACED | WNOHANG),若等待集合中的子进程都未停止/终止,则waitpid返回0,不执行while循环;while循环只在子进程出现停止/终止时运行
阻塞信号的方法是sigprocmask(SIG_BLOCK, &mask, &prev)其中mask通过sigfillset保存着所有信号
还原信号的方法是sigprocmask(SIG_SETMASK, &prev, NULL);prev在之前的操作中保存着blocked中的原始信号
/*
* 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 status;
int olderrno = errno; // 保存原始errno
sigset_t mask, prev;
pid_t pid;
sigfillset(&mask); // 将所有信号都添加到mask中
while ((pid = waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0)
{// 若等待集合中的子进程都未停止/终止,则waitpid返回0,不执行while循环;while循环只在子进程出现停止/终止时运行
if (WIFEXITED(status)) {// 子进程通过调用exit或return正常终止
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
deletejob(jobs, pid); // 从任务列表中删除相应jobs
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
}
else if (WIFSIGNALED(status)){// 子进程因为一个未捕获的信号而终止
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
deletejob(jobs, pid); // 从任务列表中删除相应jobs
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
}
else if (WIFSTOPPED(status)){ // 引起返回的子进程当前是停止的
printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
getjobpid(jobs, pid)->state = ST; // 将该进程的状态改为挂起
}
}
errno = olderrno; // 恢复errno
return;
}
sigint_handler¶
实现一个 SIGINT 信号处理函数,将信号传送给前台进程.
为什么要在调用fpgid函数之前阻塞信号,与waitfg实现相似度比较高,又是之前介绍的"显式地等待信号"原理。
/*
* 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; // 保存原始errno
sigset_t mask, prev;
sigfillset(&mask); // 将所有信号都添加到mask中
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
pid_t pid = fgpid(jobs); // 寻找对应进程
if (pid)
if (kill(-pid, SIGINT) < 0) // 给对应进程发送SIGINT信号,若出错则输出提示
unix_error("kill error\n");
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
errno = olderrno; // 恢复errno
return;
}
sigtstp_handler¶
实现一个 SIGSTOP 信号处理函数,将信号传送给前台进程
void sigtstp_handler(int sig)
{
int olderrno = errno; // 保存原始errno
sigset_t mask, prev;
sigfillset(&mask); // 将所有信号都添加到mask中
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
pid_t pid = fgpid(jobs); // 寻找对应进程
if (pid)
if (kill(-pid, SIGTSTP) < 0) // 给对应进程发送SIGTSTP信号,若出错则输出提示
unix_error("kill error\n");
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
errno = olderrno; // 恢复errno
return;
}
代码与sigint函数几乎完全相同
总代码¶
/*
* tsh - A tiny shell program with job control
* 2023-dase-10211900416-tommy
* <Put your name and login ID here>
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
/* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1 << 16 /* max job ID */
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/
/* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
struct job_t
{ /* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
/* End global variables */
/* Function prototypes */
/* Here are the functions that you will implement */
void eval(char *cmdline);
int builtin_cmd(char **argv);
void do_bgfg(char **argv);
void waitfg(pid_t pid);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv);
void sigquit_handler(int sig);
void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs);
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *jobs, pid_t pid);
pid_t fgpid(struct job_t *jobs);
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);
struct job_t *getjobjid(struct job_t *jobs, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t *jobs);
void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
/*
* main - The shell's main routine
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);
/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF)
{
switch (c)
{
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
/* Install the signal handlers */
/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);
/* Initialize the job list */
initjobs(jobs);
/* Execute the shell's read/eval loop */
while (1)
{
/* Read command line */
if (emit_prompt)
{
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin))
{ /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}
exit(0); /* control never reaches here */
}
/*
* 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)
{
/* $begin handout */
char *argv[MAXARGS]={NULL}; /* argv for execve() */
int bg; /* should the job run in bg or fg? */
pid_t pid; /* process id */
sigset_t mask; /* signal mask */
/*解析命令行*/
bg = parseline(cmdline, argv);
if (argv[0] == NULL)return;//空行,直接返回
if (!builtin_cmd(argv))
{
//这里需要一些技巧,就是在我们将job加到list之前一定要阻塞SIGCHLD,SIGINT,SIGTSTP
//这消除了在添加任务到列表和SIGCHLD,SIGINT,SIGTSTP信号抵达的竞争问题
if (sigemptyset(&mask) < 0)unix_error("sigemptyset error");
if (sigaddset(&mask, SIGCHLD))unix_error("sigaddset error");
if (sigaddset(&mask, SIGINT)) // SIGINT:Ctrl+c
unix_error("sigaddset error");
if (sigaddset(&mask, SIGTSTP)) // SIGTSTP:Ctrl+z
unix_error("sigaddset error");
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)unix_error("sigprocmask error");
//child
if ((pid = fork()) < 0)unix_error("fork error");
if (pid == 0){
//先解除堵塞
sigprocmask(SIG_UNBLOCK, &mask, NULL);
//每一个新job一定要有一个新的进程组id
//否则内核就会将ctrl-c,ctrl-z信号发给所有的job
if (setpgid(0, 0) < 0)//改变进程的进程组,不要跟tsh进程在一个进程组,然后调用exevce函数执行相关的文件。
unix_error("setpgid error");
//执行
if (execve(argv[0], argv, environ) < 0){
printf("%s: Command not found\n", argv[0]);
exit(0);//子线程执行完毕后一定要退出,否则当execve函数无法执行的时候,子进程开始运行主进程的代码,出现不可预知的错误。
}
}
//parent
//parent先添加job,然后解锁信号以便再次运行
addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
if (!bg)
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
return;
}
/*
* parseline - Parse the command line and build the argv array.
*
* Characters enclosed in single quotes are treated as a single
* argument. Return true if the user has requested a BG job, false if
* the user has requested a FG job.
*/
int parseline(const char *cmdline, char **argv)
{
static char array[MAXLINE]; /* holds local copy of command line */
char *buf = array; /* ptr that traverses command line */
char *delim; /* points to first space delimiter */
int argc; /* number of args */
int bg; /* background job? */
strcpy(buf, cmdline);
buf[strlen(buf) - 1] = ' '; /* replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* ignore leading spaces */
buf++;
/* Build the argv list */
argc = 0;
if (*buf == '\'')
{
buf++;
delim = strchr(buf, '\'');
}
else
{
delim = strchr(buf, ' ');
}
while (delim)
{
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* ignore spaces */
buf++;
if (*buf == '\'')
{
buf++;
delim = strchr(buf, '\'');
}
else
{
delim = strchr(buf, ' ');
}
}
argv[argc] = NULL;
if (argc == 0) /* ignore blank line */
return 1;
/* should the job run in the background? */
if ((bg = (*argv[argc - 1] == '&')) != 0)
{
argv[--argc] = NULL;
}
return bg;
}
/*
* 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")) //命令行为内置命令quit,直接退出
exit(0);
else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // 命令行为内置命令bg或fg,调用do_bgfg函数执行,并返回1(表示执行命令为内置命令)
{
do_bgfg(argv);
return 1;
}
else if (!strcmp(argv[0], "jobs")) // 命令行为内置命令jobs,调用listjobs函数执行,并返回1(表示执行命令为内置命令)
{
listjobs(jobs);
return 1;
}
return 0; /* not a builtin command */ // 命令非内置命令,返回0
}
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
struct job_t *jobp = NULL;
if (argv[1] == NULL){//没带参数
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (isdigit(argv[1][0])){ //pid
pid_t pid = atoi(argv[1]);
if (!(jobp = getjobpid(jobs, pid)))
{
printf("(%d): No such process\n", pid);
return;
}
}
else if (argv[1][0] == '%'){ //jid
int jid = atoi(&argv[1][1]);
if (!(jobp = getjobjid(jobs, jid))){
printf("%s: No such job\n", argv[1]);
return;
}
}else{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//bg
if (!strcmp(argv[0], "bg")){
if (kill(-(jobp->pid), SIGCONT) < 0)unix_error("kill (bg) error");
jobp->state = BG;
printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);
}
//fg
else if (!strcmp(argv[0], "fg")){
if (kill(-(jobp->pid), SIGCONT) < 0)unix_error("kill (fg) error");
jobp->state = FG;
waitfg(jobp->pid);
}else{
printf("do_bgfg: Internal error\n");
exit(0);
}
return;
}
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
sigset_t mask;
sigemptyset(&mask);
while (fgpid(jobs) != 0){
sigsuspend(&mask);//暂停时取消阻塞
}
return;
/*
while (pid == fgpid(jobs)) // 等待前台程序不再是pid(fgpid函数用于获取此时正在运行的前台进程)
{
sleep(1); // 休眠1s
}
return;
*/
}
/*****************
* Signal handlers
*****************/
/*
* 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 status;
int olderrno = errno; // 保存原始errno
sigset_t mask, prev;
pid_t pid;
sigfillset(&mask); // 将所有信号都添加到mask中
while ((pid = waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0)
{// 若等待集合中的子进程都未停止/终止,则waitpid返回0,不执行while循环;while循环只在子进程出现停止/终止时运行
if (WIFEXITED(status)) {// 子进程通过调用exit或return正常终止
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
deletejob(jobs, pid); // 从任务列表中删除相应jobs
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
}
else if (WIFSIGNALED(status)){// 子进程因为一个未捕获的信号而终止
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
deletejob(jobs, pid); // 从任务列表中删除相应jobs
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
}
else if (WIFSTOPPED(status)){ // 引起返回的子进程当前是停止的
printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
getjobpid(jobs, pid)->state = ST; // 将该进程的状态改为挂起
}
}
errno = olderrno; // 恢复errno
return;
}
/*
* 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; // 保存原始errno
sigset_t mask, prev;
sigfillset(&mask); // 将所有信号都添加到mask中
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
pid_t pid = fgpid(jobs); // 寻找对应进程
if (pid)
if (kill(-pid, SIGINT) < 0) // 给对应进程发送SIGINT信号,若出错则输出提示
unix_error("kill error\n");
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
errno = olderrno; // 恢复errno
return;
}
/*
* 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; // 保存原始errno
sigset_t mask, prev;
sigfillset(&mask); // 将所有信号都添加到mask中
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
pid_t pid = fgpid(jobs); // 寻找对应进程
if (pid)
if (kill(-pid, SIGTSTP) < 0) // 给对应进程发送SIGTSTP信号,若出错则输出提示
unix_error("kill error\n");
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
errno = olderrno; // 恢复errno
return;
}
/*********************
* End signal handlers
*********************/
/***********************************************
* Helper routines that manipulate the job list
**********************************************/
/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job)
{
job->pid = 0;
job->jid = 0;
job->state = UNDEF;
job->cmdline[0] = '\0';
}
/* initjobs - Initialize the job list */
void initjobs(struct job_t *jobs)
{
int i;
for (i = 0; i < MAXJOBS; i++)
clearjob(&jobs[i]);
}
/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *jobs)
{
int i, max = 0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid > max)
max = jobs[i].jid;
return max;
}
/* addjob - Add a job to the job list */
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
{
if (jobs[i].pid == 0)
{
jobs[i].pid = pid;
jobs[i].state = state;
jobs[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = 1;
strcpy(jobs[i].cmdline, cmdline);
if (verbose)
{
printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
}
return 1;
}
}
printf("Tried to create too many jobs\n");
return 0;
}
/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *jobs, pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
{
if (jobs[i].pid == pid)
{
clearjob(&jobs[i]);
nextjid = maxjid(jobs) + 1;
return 1;
}
}
return 0;
}
/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *jobs)
{
int i;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].state == FG)
return jobs[i].pid;
return 0;
}
/* getjobpid - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *jobs, pid_t pid)
{
int i;
if (pid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid)
return &jobs[i];
return NULL;
}
/* getjobjid - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *jobs, int jid)
{
int i;
if (jid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid == jid)
return &jobs[i];
return NULL;
}
/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid)
{
return jobs[i].jid;
}
return 0;
}
/* listjobs - Print the job list */
void listjobs(struct job_t *jobs)
{
int i;
for (i = 0; i < MAXJOBS; i++)
{
if (jobs[i].pid != 0)
{
printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
switch (jobs[i].state)
{
case BG:
printf("Running ");
break;
case FG:
printf("Foreground ");
break;
case ST:
printf("Stopped ");
break;
default:
printf("listjobs: Internal error: job[%d].state=%d ",
i, jobs[i].state);
}
printf("%s", jobs[i].cmdline);
}
}
}
/******************************
* end job list helper routines
******************************/
/***********************
* Other helper routines
***********************/
/*
* usage - print a help message
*/
void usage(void)
{
printf("Usage: shell [-hvp]\n");
printf(" -h print this message\n");
printf(" -v print additional diagnostic information\n");
printf(" -p do not emit a command prompt\n");
exit(1);
}
/*
* unix_error - unix-style error routine
*/
void unix_error(char *msg)
{
fprintf(stdout, "%s: %s\n", msg, strerror(errno));
exit(1);
}
/*
* app_error - application-style error routine
*/
void app_error(char *msg)
{
fprintf(stdout, "%s\n", msg);
exit(1);
}
/*
* Signal - wrapper for the sigaction function
*/
handler_t *Signal(int signum, handler_t *handler)
{
struct sigaction action, old_action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */
if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}
/*
* sigquit_handler - The driver program can gracefully terminate the
* child shell by sending it a SIGQUIT signal.
*/
void sigquit_handler(int sig)
{
printf("Terminating after receipt of SIGQUIT signal\n");
exit(1);
}
艰难的调试纪实¶
这个Lab我并不是根据文档中给定的顺序实现的,而是一步步去调试trace来完善函数的。
1 sdriver.pl文件错误¶
不知道为什么,我从我们的课程仓库中git pull 得到的sdriver不对,然后运行时出现"Not found"型错误。之后我换成了官方的handout里面的sdriver就修复了。经过比对,两个sdriver确实不一样,但是打开后看到的代码却几乎一样,挺离谱的。

2 trace从前到后大概在干什么¶
trace1¶
这个目标是读取到了EOF信号便退出shell,初始代码已经实现了。
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
trace2,3¶
面对内置的quit命令,要能够退出。这个需要完善builtin_cmd函数
if (!strcmp(argv[0], "quit")) //命令行为内置命令quit,直接退出
exit(0);
在完善了之后,trace3也能通过了,这个是在测试前台运行quit
trace4¶
trace4是在测试后台运行myspin
这个需要解析命令行末尾的&,并且针对前后台做出不同的处理,然后我就需要去完善eval函数了。结合官方文档和课本知识,我终于写出了完善后的eval函数
void eval(char *cmdline)
{
/* $begin handout */
char *argv[MAXARGS]={NULL}; /* argv for execve() */
int bg; /* should the job run in bg or fg? */
pid_t pid; /* process id */
sigset_t mask; /* signal mask */
/*解析命令行*/
bg = parseline(cmdline, argv);
if (argv[0] == NULL)return;//空行,直接返回
if (!builtin_cmd(argv))
{
//这里需要一些技巧,就是在我们将job加到list之前一定要阻塞SIGCHLD,SIGINT,SIGTSTP
//这消除了在添加任务到列表和SIGCHLD,SIGINT,SIGTSTP信号抵达的竞争问题
if (sigemptyset(&mask) < 0)unix_error("sigemptyset error");
if (sigaddset(&mask, SIGCHLD))unix_error("sigaddset error");
if (sigaddset(&mask, SIGINT)) // SIGINT:Ctrl+c
unix_error("sigaddset error");
if (sigaddset(&mask, SIGTSTP)) // SIGTSTP:Ctrl+z
unix_error("sigaddset error");
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)unix_error("sigprocmask error");
//child
if ((pid = fork()) < 0)unix_error("fork error");
if (pid == 0){
//先解除堵塞
sigprocmask(SIG_UNBLOCK, &mask, NULL);
//每一个新job一定要有一个新的进程组id
//否则内核就会将ctrl-c,ctrl-z信号发给所有的job
//执行
if (execve(argv[0], argv, environ) < 0){
printf("%s: Command not found\n", argv[0]);
exit(0);//子线程执行完毕后一定要退出,否则当execve函数无法执行的时候,子进程开始运行主进程的代码,出现不可预知的错误。
}
}
//parent
//parent先添加job,然后解锁信号以便再次运行
addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
if (!bg)
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
return;
}
对于后台程序,输出了任务号,pid等之后便可以结束了。
但是对于前台程序,还需要去完善sigchld_handler才行,以便接收到其终止信号时将其移出jobs数组。完善后大致面貌如下.
void sigchld_handler(int sig)
{
int status;
int olderrno = errno; // 保存原始errno
sigset_t mask, prev;
pid_t pid;
sigfillset(&mask); // 将所有信号都添加到mask中
while ((pid = waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0)
{// 若等待集合中的子进程都未停止/终止,则waitpid返回0,不执行while循环;while循环只在子进程出现停止/终止时运行
if (WIFEXITED(status)) {// 子进程通过调用exit或return正常终止
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
deletejob(jobs, pid); // 从任务列表中删除相应jobs
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
}
}
errno = olderrno; // 恢复errno
return;
}
为了简略,也为了保障正确,最开始我的waitfg是这样写的
void waitfg(pid_t pid)
{
while (pid == fgpid(jobs)) // 等待前台程序不再是pid(fgpid函数用于获取此时正在运行的前台进程)
{
sleep(1); // 休眠1s
}
return;
}
trace5¶
trace05为实现jobs功能,在完成了前面的基本的操作后非常简单,只需要在builtin_cmd添加判定jobs的代码即可。
else if (!strcmp(argv[0], "jobs")) // 命令行为内置命令jobs,调用listjobs函数执行,并返回1(表示执行命令为内置命令)
{
listjobs(jobs);
return 1;
}
trace6,7,8¶
这三个trace是测试SIGINT和SIGSTOP能否被正确处理,值得注意的是,前台程序收到这两个信号都应该将其发送给其所在组的所有程序,而不是本身。

来自实验文档里面的一个小技巧,就是收到信号后给所在的整个组转发信号时,只需要给kill的pid为负数即可。
void sigint_handler(int sig)
{
int olderrno = errno; // 保存原始errno
sigset_t mask, prev;
sigfillset(&mask); // 将所有信号都添加到mask中
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
pid_t pid = fgpid(jobs); // 寻找对应进程
if (pid)
if (kill(-pid, SIGINT) < 0) // 给对应进程发送SIGINT信号,若出错则输出提示
unix_error("kill error\n");
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
errno = olderrno; // 恢复errno
return;
}
void sigtstp_handler(int sig)
{
int olderrno = errno; // 保存原始errno
sigset_t mask, prev;
sigfillset(&mask); // 将所有信号都添加到mask中
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
pid_t pid = fgpid(jobs); // 寻找对应进程
if (pid)
if (kill(-pid, SIGTSTP) < 0) // 给对应进程发送SIGTSTP信号,若出错则输出提示
unix_error("kill error\n");
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
errno = olderrno; // 恢复errno
return;
}
同时我也完善了sigchld_handler
while ((pid = waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0)
{// 若等待集合中的子进程都未停止/终止,则waitpid返回0,不执行while循环;while循环只在子进程出现停止/终止时运行
if (WIFEXITED(status)) {// 子进程通过调用exit或return正常终止
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
deletejob(jobs, pid); // 从任务列表中删除相应jobs
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
}
else if (WIFSIGNALED(status)){// 子进程因为一个未捕获的信号而终止
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号,完成后,blocked中所有信号均阻塞,prev中保存着blocked的原始情况
deletejob(jobs, pid); // 从任务列表中删除相应jobs
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞,还原blocked的原始情况
}
else if (WIFSTOPPED(status)){ // 引起返回的子进程当前是停止的
printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
getjobpid(jobs, pid)->state = ST; // 将该进程的状态改为挂起
}
}
据说啊,这里存在一个潜在的问题,在信号处理程序里不可以使用异步信号不安全的printf,我也没有仔细考虑了,这里确实是有一定的完善空间。
最后还有非常重要的一点就是,我们的shell程序本身是所有子进程的父进程,那么就会分配在同一个组里,终止子进程所在组会导致shell程序本身也被终止,这里的解决办法是给子进程设置一个单独的组,只需要添加在fork和execve之间。修改eval函数的写法如下:
sigprocmask(SIG_UNBLOCK, &mask, NULL);
//每一个新job一定要有一个新的进程组id
//否则内核就会将ctrl-c,ctrl-z信号发给所有的job
if (setpgid(0, 0) < 0)//改变进程的进程组,不要跟tsh进程在一个进程组,然后调用exevce函数执行相关的文件。
unix_error("setpgid error");
//执行
if (execve(argv[0], argv, environ) < 0){
printf("%s: Command not found\n", argv[0]);
exit(0);//子线程执行完毕后一定要退出,否则当execve函数无法执行的时候,子进程开始运行主进程的代码,出现不可预知的错误。
}
}
trace9,10¶
这两个trace是关于内置命令bg和fg的
其中为响应任务的PID或JID,如果为JID则需%作为前缀。fg和bg都是发送SIGCONT信号来将相应任务重启。
首先在builtin_cmd函数中判断是否为bg或fg,如果是则执行相应的操作。添加的代码
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // 命令行为内置命令bg或fg,调用do_bgfg函数执行,并返回1(表示执行命令为内置命令)
{
do_bgfg(argv);
return 1;
}
具体的do_bgfg函数首先根据有无%判断是PID还是JID,然后取得该job指针,然后给其所在进程组发送SIGCONT,最后根据其是fg还是bg来做出与eval中类似的行为。
void do_bgfg(char **argv)
{
struct job_t *jobp = NULL;
if (argv[1] == NULL){//没带参数
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (isdigit(argv[1][0])){ //pid
pid_t pid = atoi(argv[1]);
if (!(jobp = getjobpid(jobs, pid)))
{
printf("(%d): No such process\n", pid);
return;
}
}
else if (argv[1][0] == '%'){ //jid
int jid = atoi(&argv[1][1]);
if (!(jobp = getjobjid(jobs, jid))){
printf("%s: No such job\n", argv[1]);
return;
}
}else{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//bg
if (!strcmp(argv[0], "bg")){
if (kill(-(jobp->pid), SIGCONT) < 0)unix_error("kill (bg) error");
jobp->state = BG;
printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);
}
//fg
else if (!strcmp(argv[0], "fg")){
if (kill(-(jobp->pid), SIGCONT) < 0)unix_error("kill (fg) error");
jobp->state = FG;
waitfg(jobp->pid);
}else{
printf("do_bgfg: Internal error\n");
exit(0);
}
return;
}
trace11,12,13¶
这三个traces主要测试前面是否正确实现了SIGSTOP和SIGINT的处理程序,以及fg/bg的实现.
由于我们之前便考虑到了进程组的问题,这里通畅地运行了,没出现错误。
trace14¶
这个测试需要对fg和bg的输入参数进行一些错误处理,例如没有参数或参数非数值或所选任务或进程不存在等,修改do_bgfg函数即可。
trace15,16¶
综合性的测试,顺利通过了。
实验结果和总结¶
详细结果这里就不贴了,主要应要求放trace15的结果吧。


shell是一个让我不明觉厉的东西,本实验还是相当有趣的。在完成这个实验后,我对异常控制流和信号机制的基本原理有了更深的认识。