本文是CS:APP学习笔记的一部分:

相关的代码都放在了GitHub下了:RayZhang13/CSAPP-Labs

关于学习资源和视频等问题可以参考第一次写的Data Lab开头中提及的相关问题。

日常肝不动本科毕设,一直对自己的本专业知识水平感到堪忧,自动化大概是真的不适合我… 在看论文找解决方案最累的时候还是看CMU的网课最能振奋人心,晚上睡不着的时候就靠它了(不是

逃避可耻但是有用,起码看CMU的网课能很好的调节我的身心,而不产生很强烈的摸鱼愧疚感,也感谢老师的不杀之恩没有三天两头“关心”我的毕设进度…

回到正题,这次的Shell Lab,为啥叫Shell Lab呢,原来他真的是要你写个Shell出来,当然是一个简易的版本,没要求我们把所有的功能都实现出来… 听着David O’Halloran教授絮絮叨叨讲了好久,简单的把通过信号实现进程间通信的一些注意点了解了一下,感觉就是有点难受,配上机翻但没完全机翻的字幕,注意力被分散的有些厉害,勉勉强强过完了网课… 建议还是在看网课之前好好看一遍书

准备

对应课程

这次的Shell Lab作业。如果是自学,在B站课程中,应该大致需要完成P14~P15的学习,也就是书中第8章的内容,大致了解异常、中断等概念,了解进程上下文切换的实现,需要学会使用fork(), wait(), waitpid(), kill(), execve()等函数,同时通过安装信号处理函数和设置信号掩码来实现信号的处理。

和上次的Cache Lab中间还隔开了一个P13是关于链接的,也就是书本第7章的相关内容,可惜这里并没有在Lab中有很好的体现,学还是要学的…

课程文件

相关的作业还是在CMU的官网上,相同位置:

Lab Assignments

在Shell Lab一栏中,我们可以查看相关文件,例如:

下载后并解压的文件如图所示:

使用环境

继续使用Arch Linux作为我的日常试验机…

题目

作业要求

目标

根据PDF讲义中的说法,我们需要修改文件夹下的tsh.c文件,并通过编译,完成tsh这个Shell程序。

而我们需要我们自己完成的函数有:

  • eval:解析并执行命令行输入的主进程。
  • builtin_cmd:检测命令是否为内置的命令,在这次作业中需要实现的命令有:quit, fg, bgjobs
  • do_bgfg:实现bgfg这两个内置指令,也就是前台和后台运行的两种情形。
  • waitfg:等待一个前台任务结束。
  • sigchld_handler:处理SIGCHLD信号的信号处理函数。
  • sigint_handler:处理SIGINT(Ctrl-C)信号的信号处理函数。
  • sigtstp_handler:处理SIGTSTP(Ctrl-Z)信号的信号处理函数。

我们稍后对这几个函数慢慢的看过去…

Shell可以使用多个内建方法,例如他们的功能为:

  • jobs:列出正在运行和停止的后台作业。
  • bg <job>:将一个停止的作业转入后台开始运行。
  • fg <job>:将一个停止或者正在运行的后台作业转入前台进行运行。
  • kill <job>:终止一个作业。(这个貌似没要求)
  • quit:退出终端程序

Shell简介

具体的实现规格:

  • 每一行会输出一个 tsh>,然后等待用户输入。
  • 用户的输入包括 name 加上零个或多个参数,这些参数之间用一个或多个空格分隔。如果 name 是内置命令,那么直接执行然后等待下一个命令,否则Shell需要认为name是一个可执行文件的路径,需要新建一个子进程,并在子进程中完成具体的工作。
  • 不需要支持管道或者I/O重定向。
  • 输入 Ctrl-CCtrl-Z 会给当前的前台进程(包括其子进程)发送 SIGINT(SIGTSTP) 信号,如果没有前台任务,那么信号不应该产生任何效果。
  • 如果输入的命令以 & 结尾,那么就要以后台任务的方式执行,否则按照前台执行。
  • 每个作业都有其进程 ID(PID) 和 job ID(JID),都是由 tsh 指定的正整数,JID 以 % 开头(如 %5 表示 JID 为 5,而 5 则表示 PID 为 5)。
  • 我们需要支持的内置命令有
    • quit 指令退出 Shell。
    • jobs 指令列出所有的后台作业。
    • bg job 给后台 job 发送 SIGCONT 信号来继续执行该任务,具体的 job 数值可以是 PID 或 JID。
    • fg job 给前台 job 发送 SIGCONT 信号来继续执行该任务,具体的 job 数值可以是 PID 或 JID。
  • tsh 应该回收所有的僵尸进程,如果任何作业因为接收了没有捕获的信号而终止,tsh 应该识别出这个时间并且打印出 JID 和相关信号的信息。

编写安全信号处理程序的原则

关键的要点在P534提及:

  • 处理程序尽可能的简单。

    避免麻烦的最好方法是保持处理程序尽可能的小和简单。例如,处理程序可能只是简单地设置全局标志并立即返回;所有与接收信号相关的处理程序都由主程序执行,它周期性地检查(并重置)这个标志。

  • 在处理程序中只调用异步信号安全的函数。

    所谓异步安全的函数能够被信号处理程序安全的调用,原因有二:要么它是可重入的(例如只访问局部变量),要么它不能被信号处理程序中断。很多常见的函数都不是安全的,例如printf, sprintf, mallocexit

    P. S.

    例如我们有如下的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int main(){
    Signal(SIGINT, sigint_handler); //install the SIGINT handler
    //....
    while(1){
    printf("Hello\n");
    }
    //...
    }

    void sigint_handler(){
    printf("hi there\n");
    }

    printf每次需要获取输出缓冲区的锁才能对终端进行输出,如果我们恰好在printf获得锁后将其通过一个信号打断,在信号处理程序中,如果我们继续调用printf方法,就会发生死锁,信号处理程序等待原程序释放锁,然而那是不可能的,因此就会一直等下去…

    这也就暗示了,要么我们控制printf无法被信号打断,要么我们就将其设置为可重入的来解决死锁的问题。

    P. S.

    另外这次的Lab中,在信号处理函数中大量使用了printf等方法,不知道是否是无心之举,还是纯粹是为了简化程序…

    事实上在信号处理程序中产生输出的唯一方法是使用write函数。而特别的,调用printf或者sprintf是不安全的,为了绕开这个限制,CS:APP特意开发了一些安全的函数,称为SIO(安全的I/O包),可以用来在信号处理程序中打印简单的消息。

    1
    2
    3
    4
    5
    #include "csapp.h"

    ssize_t sio_putl(long v);
    ssize_t sio_puts(char s[]);
    void sio_error(char s[]);

    相关的示例程序在Code Examples

  • 保存和恢复errno

    许多Linux异步安全的函数都会在出错返回时设置errno。在处理程序中调用这样的函数可能会干扰主程序中其他依赖于errno的部分。解决办法是在进入处理程序时把errno保存在一个局部变量中,在处理函数返回前恢复它。注意,只有在处理程序需要返回时才有此必要。如果处理程序调用_exit终止该进程,那么就不需要这样做了。

  • 阻塞所有的信号,保护对共享全局数据结构的访问。

    如果处理程序和主程序或其他处理程序共享一个全局数据结构,那么在访问(读或者写)该数据结构时,你的处理程序和主程序应该暂时阻塞所有的信号。这条规则的原因是从主程序访问一个数据结构d通常需要一系列指令,如果指令序列被访问d的处理程序中断,那么处理程序可能会发现d的状态不一致,得到不可预知的结果。在访问d时暂时阻塞信号保证了程序不会中断该指令。

  • volatile声明全局变量。

    volatile限定符强迫编译器每次在代码中引用g时,从内存中读取g的值。一般来说,和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。

  • sig_atomic_t声明标志。

    在常见的处理程序设计中,处理程序会写全局标志来记录收到了信号。主程序周期性的读取这个标志,相应信号,再清除该标志。对于通过这种方式来共享的标志,C提供一种整型数据类型sig_atomic_t,对它的读和写保证会是原子的,因为可以用一条指令来实现它们:

    1
    volatile sig_atomic_t flag;

    我们可以通过这样的方法安全的读写,而不需要暂时的阻塞信号。

完成程序

错误包装函数

通过错误处理包装函数,我们可以进一步的简化代码。对于一个基本的函数,我们定义一个具有相同参数的包装函数,但是第一个字母大写。

这些代码其实也被CS:APP附在了csapp.c的代码中,除了自己写也可以直接去上面提及的Code Examples里面找。

这些代码用于调用基本函数,检查错误,如果有任何问题则终止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/******************************
* Error handling wrapper function
******************************/

pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0) {
unix_error("Fork error");
}
return pid;
}

void Kill(pid_t pid, int sig) {
if (kill(pid, sig) < 0) {
unix_error("Kill error");
}
return;
}

pid_t Wait(int *status) {
pid_t pid;
if ((pid = wait(status)) < 0) {
unix_error("Wait error");
}
return pid;
}

pid_t Waitpid(pid_t pid, int *status, int options) {
pid_t p;
if ((p = waitpid(pid, status, options)) < 0) {
unix_error("Waitpid error");
}
return p;
}

void Sigemptyset(sigset_t *mask) {
if (sigemptyset(mask) < 0) {
unix_error("Sigemptyset error");
}
return;
}

void Sigfillset(sigset_t *mask) {
if (sigfillset(mask) < 0) {
unix_error("Sigfillset error");
}
return;
}

void Sigaddset(sigset_t *mask, int sign) {
if (sigaddset(mask, sign) < 0) {
unix_error("Sigaddset error");
}
return;
}

void Sigprocmask(int how, sigset_t *mask, sigset_t *oldmask) {
if (sigprocmask(how, mask, oldmask) < 0) {
unix_error("Sigprocmask error");
}
return;
}

void Setpgid(pid_t pid, pid_t gpid) {
if (setpgid(pid, gpid) < 0) {
unix_error("Setpgid error");
}
return;
}

void Execve(const char *filename, char *const argv[], char *const envp[]) {
if (execve(filename, argv, envp) < 0) {
printf("%s: Command not found.\n", filename);
exit(0);
}
}
/******************************
* End Error handling wrapper function
******************************/

waitfg实现

首先看一下注释中的要求:

1
2
3
/*
* waitfg - Block until process pid is no longer the foreground process
*/

当我们在执行一个前台作业时,shell会选择卡在那里直到作业终止或者停止,暂时不接受接下来的输入,就像这样:

waitfg函数实现的就是这样的功能,我们选择在前台作业结束前在此位置等待。

我们可以选择自旋的做法,使用fgjob不断的比较在前台的作业PID和传入PID。如何确定当前在前台运行的作业PID是什么呢?tsh.c中内置了fgpid方法:

1
2
3
4
5
6
7
8
/* 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;
}

而如果当前没有前台任务,则会返回0

因此可以有:

1
2
3
4
5
6
7
void waitfg(pid_t pid){
//....
while(fgpid(jobs) == pid){ //自旋
//若被SIGCHILD信号打断,将fgpid(jobs)修改,则可退出循环
}
//....
}

但是这样… 未免太消耗处理器资源了,我们不断的进行循环,每次还会进行条件的判断。

参考书中P546的做法,我们不仿使用全局变量fgchld_flag来表示前台作业的状态,表示是否有前台作业尚未完成。根据刚才的信号处理函数的书写指南,当我们对这样的变量进行操作时,应当使用volatile来确保其可见,使用sig_atomic_t类型确保其操作原子:

1
volatile sig_atomic_t fgchld_flag = 1; //一开始赋值1,表示没有前台作业

这样的话,我们在SIGCHLD信号的处理函数中只要顺带对fgchld_flag进行修改为1表示无前台作业,让waitfg发现fgchld_flag发生了改变,也就达成了告知其前台作业已经结束的目的了。

前台作业我们可以不同于选择自旋的做法,我们使用sigsuspend进行暂停来节省处理器资源。

关于为何使用sigsuspend而非pause,在书中和视频中已经讲的很清楚了,为了防止出现Data race现象,如果在while循环刚检测完fgchld_flag时决定进入循环时突然收到打断的SIGCHLD直接修改了fgchld_flag值,就没能达成通知的目的,程序会一直卡死在pause无法醒来。相比之下suspend函数等价于如下代码的原子(不可中断)版本:

1
2
3
sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

我们确保只有在puase()的时候才会接受SIGCHLD信号。

所以我们要这样写:

1
2
3
4
5
6
sigset_t mask_empty;
sigemptyset(&mask_empty);
fgchld_flag = 0; //将标志置0
while(!fgchld_flag){
sigsuspend(&mask_empty);
}

所以最后的程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid) {
sigset_t mask_empty;
sigemptyset(&mask_empty); //设置无屏蔽的掩码
fgchld_flag = 0; //将标志位置1,表示当前有前台作业
while (!fgchld_flag) {
sigsuspend(&mask_empty); //只有每次pause的时候才会开启无屏蔽掩码
}
return;
}

sigint_handler实现

先从简单的来~ 例如如何实现SIGINT信号的处理,我们看一下tsh.c中的注释:

1
2
3
4
5
/*
* 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.
*/

当内核向shell发送一个SIGINT信号时,我们将其捕获,并将其发送给前台工作。

tsh.c中内置的fgpid方法刚才已经讲过:

1
2
3
4
5
6
7
8
/* 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;
}

而如果当前没有前台任务,则会返回0。按照Shell简介,如果没有前台任务,那么SIGINT信号不应该产生任何效果。

此外,由于当我们调用fgpid时,需要访问全局数据结构jobs

1
2
3
4
5
6
7
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 */

因此,按照之前的安全信号处理原则,最好在此期间对所有信号进行阻塞,所以我们有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 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) {
sigset_t mask_all, prev;
Sigfillset(&mask_all);
Sigprocmask(SIG_SETMASK, &mask_all, &prev); //屏蔽所有信号,将原信号掩码记录在prev
pid_t pid = fgpid(jobs); //获取前台作业pid
Sigprocmask(SIG_SETMASK, &prev, NULL); //恢复原信号掩码
if (pid == 0) { //如果没有,不产生任何变化
return;
}
Kill(-pid, sig); //否则对当前前台任务及其子进程发送信号(进程组)
return;
}

P. S.

kill函数并不完全代表“杀死某个进程”,不要被他的名字迷惑了,它的功能描述为用于向任何进程组或进程发送信号。我们除了发送SIGKILL(9)以外,还有很多选择。

sigtstp_handler实现

我们先看注释要求:

1
2
3
4
5
/*
* 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.
*/

输入Ctrl-Z 会给当前的前台进程(包括其子进程)发送 SIGTSTP 信号,如果没有前台任务,那么信号不应该产生任何效果。

emmmm,看起来和上面这个没差,只是发送的信号不一样,照抄下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 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) {
sigset_t mask_all, prev;
Sigfillset(&mask_all);
Sigprocmask(SIG_SETMASK, &mask_all, &prev); //屏蔽所有信号,将原信号掩码记录在prev
pid_t pid = fgpid(jobs); //获取前台作业pid
Sigprocmask(SIG_SETMASK, &prev, NULL); //恢复原信号掩码
if (pid == 0) { //如果没有,不产生任何变化
return;
}
Kill(-pid, sig); //否则对当前前台任务及其子进程发送信号
return;
}

sigchld_handler实现

接下来这个可能就会有点难度了,在这里我们需要处理捕获SIGCHLD信号时的行为。

P. S.

注意这里有一个误区需要纠正,SIGCHLD不是只有子进程终止时这一种情形。SIGCHLD信号产生的条件有:

  • 子进程终止时会向父进程发送SIGCHLD信号,告知父进程回收自己,但该信号的默认处理动作为忽略,因此父进程仍然不会去回收子进程,需要捕捉处理实现子进程的回收;
  • 子进程接收到SIGSTOP信号停止时;
  • 子进程处在停止态,接受到SIGCONT后唤醒时。

同样,waitpid功能也完全不是等待进程终止,而更强调是进程的状态发生改变,例如正常退出exit,被信号终止,被信号暂停等等,因此我们可以使用WIFEXITED, WIFSTOPPED, WIFCONTINUED等手段去确认。而且waitpid也不是一定要阻塞,我们通过在options中加入WNOHANG就可以避免阻塞,找不到就直接返回-1。更多详细细节可以看:waitpid(3) - Linux man page

我们先看一下注释下的要求:

1
2
3
4
5
6
7
/*
* 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.
*/

题目明确有如下几点需求:

  • 不等待其他正在运行的进程终止,即不阻塞。考虑在waitpidoptions参数中加入WNOHANG
  • 我们处理的子进程包含两种情形,一种是终止(称为僵尸进程),一种是进程收到SIGSTOP或者SIGTSTP信号而暂停。由于是子进程,因此waitpid中的pid参数设置为-1表示等待子进程,此外还需在options参数中加入WUNTRACED,表示当子进程暂停时也返回其PID。

此外调用waitpid时,可能会对errno进行修改,例如没有子进程时,就会修改为ECHILD。因此我们需要对errno进行保存和恢复,参照刚才的安全信号程序处理原则。

当子进程终止或者暂停时,我们需要对作业列表进行修改,选择删除作业或者将作业状态改为暂停的操作,我们会对jobs作业列表进行访问,和刚才一样记得屏蔽所有信号事后恢复。

例如从作业表中删除当前进程,我们可以使用deletejob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 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;
}

假如我们要修改作业表中的状态status或者打印相关的日志,我们首先还需根据子进程的PID定位到其在作业表中对应的job实例,我们可以使用getjobpid

1
2
3
4
5
6
7
8
9
/* 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;
}

另外别忘了,刚才写的waitfg还卡在那里呢,我们打断了waitfg之后需要把全局变量fgchld_flag改成1,表示前台作业完成达到通知的目的,以此取消阻塞。

因此我们获得以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* 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; //保存errno
sigset_t mask_all, prev;
Sigfillset(&mask_all);
pid_t pid;
int status;

while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) { //加入等待子进程被暂停的选项,并取消阻塞
Sigprocmask(SIG_SETMASK, &mask_all, &prev); //由于jobs全局变量的访问,信号阻塞
struct job_t *job = getjobpid(jobs, pid); //根据pid获得job实例
if (job->state == FG) { //如果是前台进程,修改标志,通知waitfg
fgchld_flag = 1;
}
if (WIFEXITED(status)) { //如果正常退出,直接从作业表中删除当前子进程
deletejob(jobs, pid);
} else if (WIFSIGNALED(status)) { //如果是收到信号终止,打印消息
printf("Job [%d] (%d) terminated by signal %d\n", job->jid, pid,
WTERMSIG(status));
deletejob(jobs, pid);
} else if (
WIFSTOPPED(status)) { //如果是被暂停,在作业表中将作业状态改为暂停,并打印消息
job->state = ST;
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid,
WSTOPSIG(status));
}
Sigprocmask(SIG_SETMASK, &prev, NULL); //信号恢复
}
errno = oldErrno; //恢复errno
return;
}

builtin_command实现

在这里我们需要实现题目中所给出的四个内建命令,分别是jobs, quit, bg, fg

注释要求如下:

1
2
3
4
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/

我们直接将参数传入即可,然后根据内容按情况讨论,如果是内建命令就返回1并执行,否则返回0

相关代码可参考书P525,但是不是很全…

至于相关的指令,quit发生时,我们直接使用exit退出Shell;

jobs列出时,主进程会调用listjobs函数,将作业表jobs中的所有内容打印出来(访问了全局变量jobs,建议信号阻塞一下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 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);
}
}
}

fg或者bg开头时,表示这个作业需要我们(转到)前台/后台运行,转到do_bgfg,这个函数我们还没完成,等下写。

此外&被单独列为了内建指令,P525中也有提及,我们的操作是直接忽略并返回。

所以我们有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 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")) { //退出shell
exit(0);
} else if (!strcmp(argv[0], "jobs")) {
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask); //访问jobs,屏蔽信号
listjobs(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL); //恢复信号
return 1;
} else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) { //转入前台/后台运行
do_bgfg(argv);
return 1;
} else if (!strcmp(argv[0], "&")) {
return 1;
}
return 0; /* not a builtin command */
}

do_bgfg实现

在这里我们实现fgbg这两个内建命令的实现,它们的功能分别是将对应的PID/JID转入前台/后台运行。

注释要求如下:

1
2
3
/*
* do_bgfg - Execute the builtin bg and fg commands
*/

由于fg或者bg的指令传入的参数可能是PID也可能是JID,我们需要根据参数的特征进行分别,也就是开头是否带%带上的就是JID,否则就是PID。当然我们还要考虑异常情况,例如参数PID/JID均不匹配,没有找到PID/JID对应的作业实例等情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void do_bgfg(char **argv) {
struct job_t *job;
char *param;
int jid;
pid_t pid;

param = argv[1];
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 访问全局变量jobs,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
if (param == NULL) { //缺少参数
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

if (param[0] == '%') { // JID处理
jid = atoi(&param[1]);
job = getjobjid(jobs, jid);
if (job == NULL) { //异常处理
printf("%s: No such job\n", param);
return;
} else {
pid = job->pid;
}
} else if (isDigit(param)) { //PID处理
pid = atoi(&param[0]);
job = getjobpid(jobs, pid);
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
} else {
jid = job->jid;
}
} else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//.......
//........
return;
}

int isDigit(char *str) {
while (str) {
if (!isdigit(str)) return 0;
str++;
}
return 1;
}

接下来,我们将指定的进程(组)通过SIGCONT运行起来。调用kill函数即可。

如果是前台进程,我们需要将其state设置为FG,如果是后台进程就是就是BG。如果是前台进程,我们还需要将Shell主进程挂起,等待到前台进程运行结束方可解除。

程序写作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv) {
struct job_t *job;
char *param;
int jid;
pid_t pid;

param = argv[1];
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 访问全局变量jobs,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
if (param == NULL) { //缺少参数
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

if (param[0] == '%') { // JID处理
jid = atoi(&param[1]);
job = getjobjid(jobs, jid);
if (job == NULL) { //异常处理
printf("%s: No such job\n", param);
return;
} else {
pid = job->pid;
}
} else if (isDigit(param)) { //PID处理
pid = atoi(&param[0]);
job = getjobpid(jobs, pid);
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
} else {
jid = job->jid;
}
} else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}

Kill(-pid, SIGCONT); //发送SIGCONT信号
if (!strcmp(argv[0], "bg")) { //如果后台,设置状态,打印
job->state = BG;
printf("[%d] (%d) %s", jid, pid, job->cmdline);
} else if (!strcmp(argv[0], "fg")) { //如果前台,设置状态,挂起
job->state = FG;
waitfg(job->pid);
}
sigprocmask(SIG_SETMASK, &prev_mask, NULL); //恢复掩码

return;
}

eval实现

eval函数为shell的主处理函数,在书本P525处有类似的参考。

我们以此为基础进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
* 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]; /* Argument list execv() */
char buf[MAXLINE]; /* Holds modified commandline */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */

strcpy(buf, cmdline);
bg = parseline(buf, argv); //根据是否&结尾,确定前台/后台
if (argv[0] == NULL) {
return; /* Ignore empty lines */
}

if (!builtin_cmd(argv)) { //如果非内置指令
sigset_t mask_all, mask_one, prev_one;

Sigfillset(&mask_all);
Sigemptyset(&mask_one);
Sigaddset(&mask_one, SIGCHLD);
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
/* 防止在操作中子进程就结束打断 Block SIGCHLD */
if ((pid = Fork()) == 0) { /* Child runs user job */
Sigprocmask(SIG_SETMASK, &prev_one, NULL);
/* fork出来的子进程应当解除掩码限制恢复原状 Unblock SIGCHLD */
setpgid(0, 0); //令进程组号等于进程号
Execve(argv[0], argv, environ); //执行命令路径
}
//访问jobs全局变量,信号全部屏蔽
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
addjob(jobs, pid, bg ? BG : FG,
cmdline); /* Add the child to the job list */


/* Parent waits for foreground job to terminate */
if (!bg) { //前台则挂起
Sigprocmask(SIG_BLOCK, &mask_one, NULL); //需要设置屏蔽SIGCHLD,配合waitfg
waitfg(pid);
} else {
Sigprocmask(SIG_SETMASK, &mask_all, NULL); //访问全局变量jobs,屏蔽所有信号
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
Sigprocmask(SIG_SETMASK, &prev_one, NULL); //恢复信号
}

return;
}

在中间处理过程中,小心注意屏蔽SIGCHLD信号,如果SIGCHLDwaitfg(pid)前发生,极有可能提前更改了标志,而waitfg就会卡在那里…

如果在addjob前没有屏蔽SIGCHLD信号,就有可能提前进入到SIGCHLD处理程序中执行deletejob… 在addjob前使用deletejob这显然也是不正常的…

测试

首先我们在文件夹下通过make完成编译。

接下来共有16个测试,试图分析一下正确性:

trace01

这个一开始就是完成的,程序中有如下片段:

1
2
3
4
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

我们也可以使用Ctrl-D触发。

trace02

这里就是测试内置的quit命令是否正常。

trace03

这里测试了调用一个前台作业是否成功,命令为/bin/echo tsh> quit

trace04

这里测试了后台程序是否能够正常使用,我们看到最后myspin在运行了1s后正常退出了。

trace05

这里测试jobs内置指令是否正常,我们看到连续后台运行了两个程序,在jobs中作业表都很好的体现了出来。

trace06

这里测试了前台程序对SIGINT的反应,我们看到./myspin 4SIGINT终止了,还打印出了相关日志。

trace07

这里测试尝试只对前台程序发送SIGINT,我们看到后台程序./myspin 4 &没有收到影响,而前台程序./myspin 5被终止了。

trace08

这里测试尝试只对前台程序发送SIGTSTP信号,我们看到后台程序./myspin 4 &没有收到影响,而前台程序./myspin 5被暂停了。

trace09

这里测试bg内置指令,我们看到被暂停的./myspin 5通过bg %2运行了起来。

trace10

这里测试fg内置指令,工作正常。

trace11

这里我们测试向前台进程组的每一个进程都发送SIGINT,我们看到./mysplit 4终止后,没有子进程残留。

trace12

这里测试了向前台进程组的每一个进程都发送SIGTSTP,我们看到./mysplit 4相关子进程都已经处于暂停的状态。

trace13

这里测试了重启进程组中的每一个进程,我们看到./mysplit 4首先被暂停,在进程表中有显示,随后被fg调起到前台运行,最后正常退出。

trace14

这里主要测试了异常的处理,我们输入各种不正确的输入,程序均予以了处理。

trace15

拼合的测试,目测没有问题…

trace16

这里测试了shell能否处理来自其他进程而非终端的SIGINTSIGTSTP信号。

我们看到程序/mystop 2运行了2s后,自己暂停了自己;程序./myint 2运行了2s后,自己终止了自己。

自此,所有的程序测试都已完成。

总结

最后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
/*
* tsh - A tiny shell program with job control
*
* <Put your name and login ID here>
*/
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.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 */
volatile sig_atomic_t fgchld_flag = 1;

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);

pid_t Fork(void);
void Kill(pid_t pid, int sig);
pid_t Wait(int *status);
pid_t Waitpid(pid_t pid, int *status, int options);
void Sigfillset(sigset_t *mask);
void Sigemptyset(sigset_t *mask);
void Sigaddset(sigset_t *mask, int sign);
void Sigprocmask(int how, sigset_t *mask, sigset_t *oldmask);
void Setpgid(pid_t pid, pid_t gpid);
void Execve(const char *filename, char *const argv[], char *const envp[]);
int isDigit(char *str);
/*
* 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) {
char *argv[MAXARGS]; /* Argument list execv() */
char buf[MAXLINE]; /* Holds modified commandline */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */

strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL) {
return; /* Ignore empty lines */
}

if (!builtin_cmd(argv)) {
sigset_t mask_all, mask_one, prev_one;

Sigfillset(&mask_all);
Sigemptyset(&mask_one);
Sigaddset(&mask_one, SIGCHLD);
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
if ((pid = Fork()) == 0) { /* Child runs user job */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
setpgid(0, 0); //令进程组号等于进程号
Execve(argv[0], argv, environ);
}
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
addjob(jobs, pid, bg ? BG : FG,
cmdline); /* Add the child to the job list */
/* Parent waits for foreground job to terminate */
if (!bg) {
Sigprocmask(SIG_BLOCK, &mask_one, NULL);
waitfg(pid);
} else {
Sigprocmask(SIG_SETMASK, &mask_all, NULL);
// printf("%d %s", pid, cmdline);
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
Sigprocmask(SIG_SETMASK, &prev_one, NULL);
}

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")) {
exit(0);
} else if (!strcmp(argv[0], "jobs")) {
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
listjobs(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
return 1;
} else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
} else if (!strcmp(argv[0], "&")) {
return 1;
}
return 0; /* not a builtin command */
}

/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv) {
struct job_t *job;
char *param;
int jid;
pid_t pid;

param = argv[1];
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 访问全局变量jobs,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
if (param == NULL) { //缺少参数
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

if (param[0] == '%') { // JID处理
jid = atoi(&param[1]);
job = getjobjid(jobs, jid);
if (job == NULL) { //异常处理
printf("%s: No such job\n", param);
return;
} else {
pid = job->pid;
}
} else if (isDigit(param)) { //PID处理
pid = atoi(&param[0]);
job = getjobpid(jobs, pid);
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
} else {
jid = job->jid;
}
} else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}

Kill(-pid, SIGCONT);
if (!strcmp(argv[0], "bg")) {
job->state = BG;
printf("[%d] (%d) %s", jid, pid, job->cmdline);
} else if (!strcmp(argv[0], "fg")) {
job->state = FG;
waitfg(job->pid);
}
sigprocmask(SIG_SETMASK, &prev_mask, NULL);

return;
}

/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid) {
sigset_t mask_empty;
sigemptyset(&mask_empty);
fgchld_flag = 0;
while (!fgchld_flag) {
sigsuspend(&mask_empty);
}
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 oldErrno = errno; //保存errno
sigset_t mask_all, prev;
Sigfillset(&mask_all);
pid_t pid;
int status;

while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) >
0) { //加入等待子进程被暂停的选项,并取消阻塞
Sigprocmask(SIG_SETMASK, &mask_all,
&prev); //由于jobs全局变量的访问,信号阻塞
struct job_t *job = getjobpid(jobs, pid); //根据pid获得job实例
if (job->state == FG) { //如果是前台进程,修改表示,通知waitfg
fgchld_flag = 1;
}
if (WIFEXITED(status)) { //如果正常退出,直接从作业表中删除当前子进程
deletejob(jobs, pid);
} else if (WIFSIGNALED(status)) { //如果是收到信号终止,打印消息
printf("Job [%d] (%d) terminated by signal %d\n", job->jid, pid,
WTERMSIG(status));
deletejob(jobs, pid);
} else if (
WIFSTOPPED(
status)) { //如果是被暂停,在作业表中将作业状态改为暂停,并打印消息
job->state = ST;
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid,
WSTOPSIG(status));
}
Sigprocmask(SIG_SETMASK, &prev, NULL); //信号恢复
}
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) {
sigset_t mask_all, prev;
Sigfillset(&mask_all);
Sigprocmask(SIG_SETMASK, &mask_all, &prev);
pid_t pid = fgpid(jobs);
Sigprocmask(SIG_SETMASK, &prev, NULL);
if (pid == 0) {
return;
}
Kill(-pid, sig);
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) {
sigset_t mask_all, prev;
Sigfillset(&mask_all);
Sigprocmask(SIG_SETMASK, &mask_all,
&prev); //屏蔽所有信号,将原信号掩码记录在prev
pid_t pid = fgpid(jobs); //获取前台作业pid
Sigprocmask(SIG_SETMASK, &prev, NULL); //恢复原信号掩码
if (pid == 0) { //如果没有,不产生任何变化
return;
}
Kill(-pid, sig); //否则对当前前台任务及其子进程发送信号
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
******************************/

/******************************
* Error handling wrapper function
******************************/

pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0) {
unix_error("Fork error");
}
return pid;
}

void Kill(pid_t pid, int sig) {
if (kill(pid, sig) < 0) {
unix_error("Kill error");
}
return;
}

pid_t Wait(int *status) {
pid_t pid;
if ((pid = wait(status)) < 0) {
unix_error("Wait error");
}
return pid;
}

pid_t Waitpid(pid_t pid, int *status, int options) {
pid_t p;
if ((p = waitpid(pid, status, options)) < 0) {
unix_error("Waitpid error");
}
return p;
}

void Sigemptyset(sigset_t *mask) {
if (sigemptyset(mask) < 0) {
unix_error("Sigemptyset error");
}
return;
}

void Sigfillset(sigset_t *mask) {
if (sigfillset(mask) < 0) {
unix_error("Sigfillset error");
}
return;
}

void Sigaddset(sigset_t *mask, int sign) {
if (sigaddset(mask, sign) < 0) {
unix_error("Sigaddset error");
}
return;
}

void Sigprocmask(int how, sigset_t *mask, sigset_t *oldmask) {
if (sigprocmask(how, mask, oldmask) < 0) {
unix_error("Sigprocmask error");
}
return;
}

void Setpgid(pid_t pid, pid_t gpid) {
if (setpgid(pid, gpid) < 0) {
unix_error("Setpgid error");
}
return;
}

void Execve(const char *filename, char *const argv[], char *const envp[]) {
if (execve(filename, argv, envp) < 0) {
printf("%s: Command not found.\n", filename);
exit(0);
}
}
/******************************
* End Error handling wrapper function
******************************/

/***********************
* 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);
}

int isDigit(char *str) {
while (str) {
if (!isdigit(str)) return 0;
str++;
}
return 1;
}

感觉不管是看书还是听课,这一章都过的迷迷糊糊的,但是通过完成Shell Lab,很快的帮助我扫除了很多的困惑和误解,果然上手做一做才是真的。