0%

CSAPP:shlab-report

CSAPP-LAB4-SHELL-LAB-REPORT

计科2002班

202001130329

杨铭

实验目的

通过这个实验能够更加深刻理解进程控制和信号控制的相关概念。本次实验我们要写一个自己的类linux/unix的shell程序,它能够支持工作的控制。

实验平台准备

  • Ubuntu-32 VMfare虚拟机
  • typora编写实验报告(经授课老师许可)

Hand Out 介绍

首先解压实验文件,然后做如下工作:

  • 使用命令tar xvf shlab-handout.tar解压文件
  • 使用命令make来编译和链接测试程序
  • 输入(你的队员)没有队员,把你的大名写在tsh.c程序的顶部注释中

注意到你的tsh.c(tiny shell)文件,你会看到它包含了一个简单Unix的shell的函数框架。为了简化实验,实际上题目已经帮我们实现了一些没那么有趣的函数。我们的任务就是完成剩下的空的函数。

需要实现

目标函数 说明
eval 主例程,用以分析和解释命令行(原型在课本8.4节)
builtin_cmd 执行bg和fg内置命令
waitfg 等待前台作业执行
sigchld_handler 响应处理SIGCHILD信号
sigint_handler 响应处理SIGINT(ctrl-c)信号
sigtstp_handler 响应处理SIGTP(ctrl-z)信号

每一次修改tsh.c,都需要输入make来重编译它。运行你的shell,在命令行输入tsh如下

1
2
unix> ./tsh
tsh> [type commands to your shell here]

普通的Unix Shells概述

一个shell是一个对于用户层面的交互程序,它能够对用户输入的命令行进行解析。一个shell通常会重复输出一个标识符(通常是’prompt‘),然后等待一个命令行输入或者stdin标准输入。一旦它取得了命令行之后就会用它的内容进行一系列的解析工作。

命令行是一个有序的ASCII字符序列,它们使用空格相隔。命令行的第一个单词通常是一个shell的内部命令或者一个可执行文件的路径名称。剩下的单词是命令行的参数。如果第一个单词是一个命令行的内部命令,shell会立即在当前进程(也就是shell)立即执行。否则的话,它是一个可执行文件的路径。在这个情况之下shell通过fork一个子进程,然后在子进程的上下文中加载并运行程序。由于解析一个命令行而创建的子进程又被称作是job(作业)。总的来说,一个job是由若干子进程组成的,这些子进程通过Unix管道进行连接。

如果一个命令行以“&”符号结尾,那么这个job就会在background运行该程序。也就是在后台运行这个程序,这意味着shell不会等待这个子进程结束后才打印下一个prompt来解析下一条命令行。因此在任何时候,最多只有一个任务能够在前台运行。然而在后台可以运行任意数目的作业。

例如,输入命令行

1
tsh> jobs

会导致shell执行一个内置的作业命令。输入命令如下

1
tsh> /bin/ls -l -d

来在前台运行一个ls程序。按照惯例,shell确保当程序开始执行的时候,它的主例程

1
int main(int argc, char *argv[])

这里的argcargv拥有了如下值:

  • argc == 3,
  • argv[0] == “/bin/ls”,
  • argv[1] == “-l”,
  • argv[2] == “-d”.

或者另一方面,我们输入命令行如下

1
tsh> /bin/ls -l -d &

就会使得ls程序在后台运行。

Unix shells支持job control,它能够允许用户对将作业在前台和后台之间移动,同时也能够改变进程的状态(running, stopped, or terminated)。输入ctrl-c会导致产生一个SIGINT信号,它被发送到前台的每个作业。类似的,如果你输入一个ctrl-z会导致一个SIGTSTP信号产生,并被发送给每一个前台的工作。对于SIGTSTP的默认行为是设置一个进程为stopped状态,它会保持停止状态直到直到它收到SIGCONT信号后才被唤醒。Unix shells同样提供不同的内置命令来支持这种工作的控制。例如

  • jobs:列出正在运行的以及停止的的后台作业
  • bg <job>:改变一个停止的后台作业为一个后台运行态作业
  • fg <job>:改变一个停止或运行的后台作业为一个运行的前台作业
  • kill <job>:终止一个作业

tsh的特点

我们的tsh应该具备以下特点

  • 标志符prompt应该是字符串“tsh>”
  • 用户输入的字符串应该由一个可执行的程序或者内嵌的指令开头(name),然后紧跟着0个或者更若干个参数。它们由一个或者更更多的空格隔开。如果name是一个内置指令,tsh直接处理执行,然后等待下一条指令。否则tsh就会把这个name当成是一个可执行文件的路径,然后初始化一个子进程,然后在这个子进程的上下文中加载执行这个程序(在这种情况下,我们所说的作业通常指这个子进程)
  • tsh不需要支持管道符(|)或者I/O的重定向(<和>)
  • 输入ctrl-c(ctrl-z)应该导致SIGINT(SIGTSTP)信号产生然后被发送到当前的前台作业那里去,这些信号同样会被发送到这些前台进程的后代进程那里去。(例如它所派生的子进程)。如果没有任何前台进程,那么这个信号将不会有任何的效果
  • 如果这个命令行是以&结尾的,那么tsh应该让这个作业在后台运行。
  • 每一个作业能够唯一的ID(PID)标识或者我们说这是一个作业ID(JID),它是一个tsh所分配的标识符。JIDs应该在命令行上用一个前缀’%‘来表示。例如:“%5”表示的是JID为5。(实验中提供了所有作业列表所需要的例程)
  • tsh应该支持以下内置命令
    • quit命令用来终止shell
    • jobs用来列出所有后台作业
    • bg <job>通过向<job>发送SIGCONT来重启它,然后让它运行在后台中,<job>参数可以用PID或者JID
    • fg <job>命令通过发送SIGCONT信号给进程<job>,然后把它运行在前台中,<job>参数可以用PID或者JID
  • tsh应该回收它的所有僵尸孩子。如果任何作业因为接收到未捕获的信号而终止,则tsh应识别此事件并打印带有作业 PID 的消息和违规信号的描述

检查工作的正确性

实验提供了一些工具来帮助我们检查工作是否正确。

Reference solution.tshref是一个Linux下的可执行文件为我们的shell提供了一个参考解决办法。运行这个程序来解决问题,看看我们的tsh执行的怎么样。你的shell应该提交输出和参考解决办法完全相同。

Shell driver.sdriver.pl程序将我们的tsh作为子进程执行,按照跟踪文件的指令向其发送命令和信号,并捕获显示shell的输出。使用-h

我们有16个trace文件trace{01-16}.txt,用来和shell driver一起使用,用来测试我们的shell的正确性。那些数字小的trace文件做一些简单的测试,大的数字做的是更加复杂的测试。

可以简单通过

1
2
unix> make test01		#执行test01
unix> make rtest01 #用reference solution(tshref)执行test01

看输出结果是否一致即可。

实验步骤

为了保证实验的顺利进行,首先阅读课本第八章的内容。

根据实验指导书,这边建议根据trace文件来进行实验,trace给出你要做的事情的提示。

main函数

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
/*
* 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 */
// 处理参数
/*
* -h 帮助文件
* -v 发送额外的诊断信息
* -p 不打印prompt
*/
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 */
// 打印一个prompt符
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
// 从标准获得命令行
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");

// EOF: 当输入ctrl-d的时候表示标准输入(文件)结束,此时直接退出
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 */
}

从上面的代码我们可以看出,main函数的主要工作就是从标准输出中读出命令行,然后把它交给eval来处理。

参考CSAPP官方网站上给出阉割版本的shell.c,我们可以看到eval主要工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
* eval - evaluate a command line
*/
void eval(char *cmdline)
{
...
// 通过把argv传递给buildin_command让它判断是不是内置命令并执行
// 如果不是返回零
if (!builtin_command(argv)) {
...
}

/*
* buildin_cmd
*/
int builtin_cmd(char **argv)
{
...

我们理清楚主要执行流程如下

image-20220525154122346

test01

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest01
./sdriver.pl -t trace01.txt -s ./tshref -a "-p"
#
# trace01.txt - Properly terminate on EOF.
#

我们参考第一个test,对tshref输出了如上结果。这个对于标准输入结束(EOF)的处理。实际上就是对ctrl-d的处理。我们观察到上面main函数中已经有包含对EOF的处理

1
2
3
4
5
// EOF: 当输入ctrl-d的时候表示标准输入(文件)结束,此时直接退出
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

因此我们不许要做什么,可以看到测试结果和参考一致。

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test01
./sdriver.pl -t trace01.txt -s ./tsh -a "-p"
#
# trace01.txt - Properly terminate on EOF.
#

test02

这个一个明显需要做些什么。(不可能又帮你写好)

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest02
./sdriver.pl -t trace02.txt -s ./tshref -a "-p"
#
# trace02.txt - Process builtin quit command.
#

这个测试要求我们完成对tsh的一条内嵌指令(quit)的处理。它完成的任务就是退出tsh

1
2
3
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ ./tsh
tsh> quit
tsh>

我们可以看到如果什么都不做,是不能够退出的。

现在对这个quit指令的位置有两种思考方向。根据上面main的执行流程,我们说有两个后续处理的方向,一个就是放在eval里面处理,一个就是放在buildin_cmd里面。后者很好理解,这个quit本来就是内嵌指令,所以可以放在后者里面。

1
int builtin_cmd(char **argv);

可以看到buildin_cmd虽然是对内置指令的处理,这样一条quit非常简单,只要读到这条指令,直接exit(0),就是它的全部逻辑了。

首先用到parseline来解析命令行,并建立argv,我们可以看到parseline函数的说明如下:

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

这个函数主要主要功能就是解析命令行字符串,然后建立argv参数数组,用来给后面execve使用等。

所以代码实现起来就是如下

1
2
3
4
5
6
7
8
9
10
11
12
void eval(char *cmdline) 
{
// 参数字符串数组
char *argv[MAXARGS]; /* argv for execve() */
int bg; /* should the job run in bg or fg? */

/* parse command line */
// 解析cmdline获得argv
bg = parseline(cmdline, argv);
if (!builtin_cmd(argv)) {
...
}
1
2
3
4
5
6
7
8
9
/* 
* 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); /* terminate shell */
...

然后我们make编译以下,查看测试情况,

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test02
./sdriver.pl -t trace02.txt -s ./tsh -a "-p"
#
# trace02.txt - Process builtin quit command.
#

和参考一样,通关~

test03 & test04

测试3和测试4的内容放在一起讲好了,因为

1
2
3
4
5
6
7
8
9
10
11
12
13
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest03
./sdriver.pl -t trace03.txt -s ./tshref -a "-p"
#
# trace03.txt - Run a foreground job.
#
tsh> quit
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest04
./sdriver.pl -t trace04.txt -s ./tshref -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[1] (9950) ./myspin 1 &

可以看到参考程序中,对于测试3,它测试了前台运行quit,测试4在后台运行了myspinmyspin是自旋等待了1秒。

1
2
3
4
5
6
// in `parseline`

/* should the job run in the background? */
if ((bg = (*argv[argc-1] == '&')) != 0) {
argv[--argc] = NULL;
}

可以看到在parseline通过上面的步骤解析判断了是否需要讲进程运行在后台。主要是通过检索命令行末尾的字符是否是‘&’

前台工作需要等待工作执行完毕就好了。不论是前台还是后台程序,都需要加入到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
26
27
28
/* global variables */
int nextjid = 1; /* next job ID to allocate */
...
/* 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) // 无效pid
return 0;

for (i = 0; i < MAXJOBS; i++) {
/* 如果jobs数组里面有pid等于0等于0的表项,表示这个位置可以用,把这个作业放进去 */
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;
}

addjob的工作就是为PID为pid的子进程分配一个jid,然后把jid保存在进程结构中,然后根据全局变量JID来给这个子进程为tsh编写这个子进程的JID。通过是为了防止JID越界的情况,需要额外对的nextjid进行判断。我们对此更加加深了对jid的理解。jid就是现在一共有几个job在tsh下面运行。由于jobs数组是有限的,所以jid超过了MAXJOBS的话是需要重置的。如果发生这种事情的话,只有当一个作业结束,jobs才能让出位置来,这是后才可以再分配工作。值得一提的是,根据课本shell.c的代码,我们可以在这里加一个,如果verbose有效的话,打印详细信息(加入的作业是啥)

我们还需要等待前台运行的子进程运行完,所以我们还要解决waitfg函数的问题。这个函数是需要我们自己编写的。主要功能就是阻塞当前进程也就是tsh,直到我们pid命名的子进程结束。

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

上面这个函数就是循环查看当前前台工作的作业的pid是多少。知道了这个我们就可以写waitfg函数了。当pid对应的进程还是前台进程的时候就一直循环等待。指导书里面说可以用循环sleep(0)进行等待。

1
2
3
4
5
void waitfg(pid_t pid)
{
while (pid == fgpid(jobs))
sleep(0); // 这里是指主动让出CPU的意思
}

具备上面的基础,我们可以写出代码如下:

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
// in `eval`
...

if (!builtin_cmd(argv)) {
if ((pid = fork()) == 0) { /* child */

/* Background jobs should ignore SIGINT (ctrl-c) */
/* and SIGTSTP (ctrl-z) */
/*
if (bg) {
// 课本对SIG_IGN有详细解释
// 如果信号的handler是SIG_IGN,会忽略这个信号
Signal(SIGINT, SIG_IGN);
Signal(SIGTSTP, SIG_IGN);
}
*/

// 前台执行
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
fflush(stdout);
exit(0); // 如果没有这个可执行程序,那么直接终止这个子进程
}
}
/* parent waits for foreground job to terminate or stop */
addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
if (!bg) // 前台运行则等待子进程
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}

可以看到上面的函数fork出一个子进程,然后execve执行某个可执行程序。对于父进程,父进程把这个进程添加到jobs队列中去。然后如果这是一个前台程序,就调用waitfg等待子进程结束。如果子进程结束了。通过SIGCHLD信号给父进程发信息,唤醒父进程回收自己。

父进程回收处理程序如下:

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
/* 
* 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)
{
pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
* 这个选项开启能够检查已终止和被停止的子进程
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
// 这里不管是不是正常exit或者ctrl-c或者ctrl-z退出的
// 子进程都结束了,都要回收资源
deletejob(jobs, pid);
if (verbose)
printf("sigchld_handler: job %d deleted\n", pid);
}
if (verbose)
printf("sigchld_handler: exiting\n");
}

像上面这样写看起来是没有什么问题了。但是课本519中提示了一种苛刻同步问题。如果子进程在父进程将自己加入到jobs之前就执行完毕,然后exit退出,就会触发父进程的回收sigchld_handler程序,实际上,这样的删除显然没有任何意义。后续又将这个工作addjob,这也是没有什么意义的。

因此我们还要对上面的eval进一步修改为:

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
// in `eval`
...
sigset_t mask;

if (!builtin_cmd(argv)) {
Sigprocmask(SIG_BLOCK, &mask, &prev); /* 阻塞SIGCHLD */

if ((pid = fork()) == 0) { /* Child runs user job */
/* 给fork出来的子进程设置一个独立的组,
* 子进程是这个组的组长,组id为子进程的pid */
setpgid(0, 0);

/* 对于子进程并不需要阻塞SIGCHLD的信号 */
Sigprocmask(SIG_UNBLOCK, &prev, NULL); // unblock SIGCHLD

/*
* 前台执行
* 如果没有这个可执行程序,那么直接终止这个子进程
*/
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
_exit(1);
}
}

addjob(jobs, pid, bg ? BG : FG, cmdline);
/* 已经加到jobs了,解除阻塞 */
Sigprocmask(SIG_SETMASK, &prev, NULL);


/* Parent waits for foreground job to terminate */
if (!bg) // 前台运行则等待子进程
waitfg(pid);
else // background
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}

测试以下我们的代码的正确性

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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest03
./sdriver.pl -t trace03.txt -s ./tshref -a "-p"
#
# trace03.txt - Run a foreground job.
#
tsh> quit
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test03
./sdriver.pl -t trace03.txt -s ./tsh -a "-p"
#
# trace03.txt - Run a foreground job.
#
tsh> quit


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest04
./sdriver.pl -t trace04.txt -s ./tshref -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[1] (10488) ./myspin 1 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test04
./sdriver.pl -t trace04.txt -s ./tsh -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[1] (10494) ./myspin 1 &

测试程序都相同了,成功~

test05

1
2
3
4
5
6
7
8
9
10
11
12
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest05
./sdriver.pl -t trace05.txt -s ./tshref -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (10504) ./myspin 2 &
tsh> ./myspin 3 &
[2] (10506) ./myspin 3 &
tsh> jobs
[1] (10504) Running ./myspin 2 &
[2] (10506) Running ./myspin 3 &

这里的任务是完成内嵌指令jobs的工作。它能够列出作业列表中的所有作业。

这个很简单,起始只要用一个listjobs就可已完成这个工作了,这个函数是实验已经帮我们实现了的。

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

然后在builtin_cmd中加入jobs命令的入口就好。

1
2
3
4
5
6
7
8
9
int builtin_cmd(char **argv) 
{
...
// jobs command
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest05
./sdriver.pl -t trace05.txt -s ./tshref -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (10613) ./myspin 2 &
tsh> ./myspin 3 &
[2] (10615) ./myspin 3 &
tsh> jobs
[1] (10613) Running ./myspin 2 &
[2] (10615) Running ./myspin 3 &

puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test05
./sdriver.pl -t trace05.txt -s ./tsh -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (10622) ./myspin 2 &
tsh> ./myspin 3 &
[2] (10624) ./myspin 3 &
tsh> jobs
[1] (10622) Running ./myspin 2 &
[2] (10624) Running ./myspin 3 &

和参考一致,表示正确。这里你可能会被英文的实验指导书迷惑了,实际上列出的是工作列表的所有工作,而不是后台的所有工作。通关~

test06 & test07 & test08

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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest06
./sdriver.pl -t trace06.txt -s ./tshref -a "-p"
#
# trace06.txt - Forward SIGINT to foreground job.
#
tsh> ./myspin 4
Job [1] (10840) terminated by signal 2
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest07
./sdriver.pl -t trace07.txt -s ./tshref -a "-p"
#
# trace07.txt - Forward SIGINT only to foreground job.
#
tsh> ./myspin 4 &
[1] (10846) ./myspin 4 &
tsh> ./myspin 5
Job [2] (10848) terminated by signal 2
tsh> jobs
[1] (10846) Running ./myspin 4 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest08
./sdriver.pl -t trace08.txt -s ./tshref -a "-p"
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
tsh> ./myspin 4 &
[1] (10856) ./myspin 4 &
tsh> ./myspin 5
Job [2] (10858) stopped by signal 20
tsh> jobs
[1] (10856) Running ./myspin 4 &
[2] (10858) Stopped ./myspin 5

测试678要完成的任务是对于SIGINT和SIGTSTP的处理。对于上面的测试,测试6启动一个自旋程序到前台并且发出SIGINT终止它。测试7启动两个自旋,一个在前台一个在后台,然后发出SIGINT,检测到只有前台进程被终止了。测试8类似。

image-20220526003548830

我们可以从课本上这幅图看到shell会给每一个子进程分配一个和PID一样的组id,这些shell的子进程的孩子都在这些子进程的组内。比如上图中的前台作业就是shell产生的子进程,但是它和shell不是一个组的,他自己独立成组,组id等于自己的pid,然后它的孩子都属于自己这个组。

首先需要完成SIGINT的处理,给前台进程的组的所有进程发送SIGINT信号(这里就是参数sig)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void sigint_handler(int sig)
{
// 这一步的保存和下一步的恢复,是由于在下面的kill系统调用中可能会覆盖errno的值
// 导致现在的errno丢失
// 而errno在sigchild_handler中还有使用到,为了避免产生这种错误需要在这里保存,在后面恢复
int olderrno = errno;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
/*
* int kill(pid_t pid,int signo)
* 功能: 向进程或进程组发送一个信号 (成功返回 0; 否则,返回 -1 )
*/
kill(-fg_pid, sig);
// -fg_pid表示向进程组号为pid的组中的每个进程发sig信号
// 在这里就是向前台进程以及它的每一个子进程(子进程都在自己的父进程的pid为组id的组下)
errno = olderrno;
}

相同的原理,可以写出sigtstp_handler函数

1
2
3
4
5
6
7
8
9
10
void sigtstp_handler(int sig)
{
// 为了异步信号安全防止errno被覆盖
int olderrno = errno;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
// kill the group in the foreground
kill(-fg_pid, sig);
errno = olderrno;
}

上面的两个函数就是信号处理过程。sigchld_handler里收到子进程终止或停止的消息后给出对应的输出然后改变其状态,对于终止的进程就在jobs里将其删除,对于停止的进程则设置其state为ST。

image-20220526081536379

主要流程如上图。

然后就是对sigchld_handler的修改。我们需要针对上面的代码进一步进行修改。

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
/*
* 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 old_errno = errno;

pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
/* 这里实验指导书说不能用while,但是不用会出现错误。他说的while和我的判断条件不一样 */
// 子进程正常退出
if (WIFEXITED(status)) {
deletejob(jobs, pid);
}
// 子进程因为ctrl-c退出
if (WIFSIGNALED(status)) { // terminated by ctrl-c
printf("Job [%d] (%d) terminated by signal %2d\n",
pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}
if (WIFSTOPPED(status)) { // stopped by ctrl-z
printf("Job [%d] (%d) stopped by signal %2d\n",
pid2jid(pid), pid, WSTOPSIG(status));
// 修改子进程状态为ST
getjobpid(jobs, pid)->state = ST;
}
}
errno = old_errno;
if (verbose)
printf("sigchld_handler: exiting\n");
}

上面的程序,在子进程发送SIGCHLD时处于不同状态的子进程,已经有了不同的处理。

但是还存在一个问题,就是printf是异步不安全的。可能出现死锁现象。

死锁(Deadlock):当你存在多个逻辑流在等待永远不会发生的场景时就会出现死锁。比如在信号处理程序中使用异步信号不安全的printf函数就可能会出现死锁现象。在主程序中执行了printf函数,则该函数会请求某些资源的一个锁,当该printf函数请求这个锁时它被某个信号处理程序中断了,而在信号处理程序中也要执行一个printf函数,这个printf也试图请求那个锁,但是由于主程序中的printf函数持有那个锁,所以信号处理程序中的printf得不到那个锁,所以这个printf就在等待那个锁被释放的锁,但是主程序只有在信号处理程序返回时才可能释放那个锁,所以这里就造成了死锁。

这里是线程安全但是不可重入的异步安全问题,使用锁是不能够解决的。

所以我们再主线程也就是main里面已经使用过printf了,就要避免在信号处理程序中在使用printf的出现。

我提供下面两种解决手段。

  • 一种想法是在这里使用printf的时候屏蔽或者说阻塞SIGCHLD信号,这样主线程在使用printf就不会被中断。但是带来的问题就是这个中断处理信号会被忽略。可能出现处理遗漏的情况。
  • 还有一种处理手段是使用异步信号安全的可重入函数,如下

CSAPP课本实例程序提供了一种线程安全且可重入的函数sio系列函数,使用它,不调用printf

1
2
3
4
5
6
7
8
/* private functions */
static void sio_reverse(char s[]);
static void sio_ltoa(long v, char s[], int b);
static size_t sio_strlen(char s[]);
/* public functions */
int sio_puts(char s[]);
int sio_putl(long v);
void sio_error(char s[]);

查看Linux手册可以知道write是异步信号安全的,使用write就可以实现异步安全的输出了。

1
2
3
4
int sio_puts(char s[]) /* Put string */
{
return write(STDOUT_FILENO, s, strlen(s));
}

我对上面的sio族进行封装形成Sio系列。

我们把sigchld_handler修改以下如下

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
/*
* 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 old_errno = errno;

pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
// 子进程正常退出
if (WIFEXITED(status)) {
deletejob(jobs, pid);
}
// 子进程因为ctrl-c退出
if (WIFSIGNALED(status)) { // terminated by ctrl-c
/* printf("Job [%d] (%d) terminated by signal %2d\n",
pid2jid(pid), pid, WTERMSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") terminated by signal ");
Sio_putl(WTERMSIG(status));
Sio_puts("\n");
}
deletejob(jobs, pid);
}
if (WIFSTOPPED(status)) { // stopped by ctrl-z
/* printf("Job [%d] (%d) stopped by signal %2d\n",
pid2jid(pid), pid, WSTOPSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") stopped by signal ");
Sio_putl(WSTOPSIG(status));
Sio_puts("\n");
}
// 修改子进程状态为ST
getjobpid(jobs, pid)->state = ST;
}
}
errno = old_errno;
if (verbose) {
/* printf("sigchld_handler: exiting\n"); */ {
Sio_puts("sigchld_handler: exiting\n");
}
}
}

这样就不会在处理信号程序中使用异步信号不安全的printf了。

然后,shell程序本身是所有子进程的父进程,按照fork函数简单实现,会被分配在一个组中。如果收到了SIGINT,发送给子进程的组,那也会发送给自己(子进程就在自己的组中),那么自己又会收到SIGINT,然后又会发送给自己的所有子进程,然后自己又收到…就陷入循环中无法继续,或者崩溃退出。所以shell不可以和它的子进程在同一个组内,就是要给每个子进程单独的组,组的id就是子进程的pid。我们应该把这个操作放在fork之后,修改它的组id,然后执行execve

修改eval这一部分代码如下

1
2
3
4
5
if ((pid = fork()) == 0) {  /* child */
/* 给fork出来的子进程设置一个独立的组,
* 子进程是这个组的组长,组id为子进程的pid */
setpgid(0, 0);
...

setpgid函数在实验英文指导书有介绍,用于设置进程组id

表头文件 #include

定义函数 int setpgid(pid_t pid,pid_t pgid);

功能:设置pid所在的进程组的进程组id设置成pgid

setpgid(0, 0)的含义:

  • 第一个参数pid是0,则使用当前进程的pid
  • 第二个参数pgid是0,则使用当前进程pid作为pgid

测试结果

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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest06
./sdriver.pl -t trace06.txt -s ./tshref -a "-p"
#
# trace06.txt - Forward SIGINT to foreground job.
#
tsh> ./myspin 4
Job [1] (3427) terminated by signal 2
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test06
./sdriver.pl -t trace06.txt -s ./tsh -a "-p"
#
# trace06.txt - Forward SIGINT to foreground job.
#
tsh> ./myspin 4
Job [1] (3433) terminated by signal 2


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest07
./sdriver.pl -t trace07.txt -s ./tshref -a "-p"
#
# trace07.txt - Forward SIGINT only to foreground job.
#
tsh> ./myspin 4 &
[1] (3440) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3442) terminated by signal 2
tsh> jobs
[1] (3440) Running ./myspin 4 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test07
./sdriver.pl -t trace07.txt -s ./tsh -a "-p"
#
# trace07.txt - Forward SIGINT only to foreground job.
#
tsh> ./myspin 4 &
[1] (3449) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3451) terminated by signal 2
tsh> jobs
[1] (3449) Running ./myspin 4 &


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest08
./sdriver.pl -t trace08.txt -s ./tshref -a "-p"
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
tsh> ./myspin 4 &
[1] (3459) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3461) stopped by signal 20
tsh> jobs
[1] (3459) Running ./myspin 4 &
[2] (3461) Stopped ./myspin 5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test08
./sdriver.pl -t trace08.txt -s ./tsh -a "-p"
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
tsh> ./myspin 4 &
[1] (3469) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3471) terminated by signal 20
tsh> jobs
[1] (3469) Running ./myspin 4 &
[2] (3471) Stopped ./myspin 5

可以看到与参考输出一致,通关~

test09 & test10

第九个和第十个放在一起写。请往下看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest09
./sdriver.pl -t trace09.txt -s ./tshref -a "-p"
#
# trace09.txt - Process bg builtin command
#
tsh> ./myspin 4 &
[1] (3480) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3482) stopped by signal 20
tsh> jobs
[1] (3480) Running ./myspin 4 &
[2] (3482) Stopped ./myspin 5
tsh> bg %2
[2] (3482) ./myspin 5
tsh> jobs
[1] (3480) Running ./myspin 4 &
[2] (3482) Running ./myspin 5

我们先看看第九个测试的参考输出。我们可以看到我们需要构建的是bg内置命令的相关功能。

bg <job>通过向<job>发送SIGCONT来重启它,然后让它运行在后台中,<job>参数可以用PID或者JID

  • 如果参数是PID则直接输入pid即可
  • 如果参数是JID则输入“%jid”

我们看到上面的测试。首先是开启一个后台自旋和一个前台自旋的程序。然后发起ctrl-z终止前台的进程。然后jobs可以看到两个进程以及状态。后台的进程仍然在正常运行,但是前台的进程由于SIGTSTP信号而进入stopped状态。然后使用bg %2指令,把第二个子进程(停止的“./myspin 5”)唤醒,然后放到后台执行。输入jobs指令可以看到两个都在执行,但是后者是后来被切换到后台的,所以没有它的cmdline还没有更新还是显示原来的“./myspin 5”。

再看看rtest10,第十个测试

1
2
3
4
5
6
7
8
9
10
11
12
13
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest10
./sdriver.pl -t trace10.txt -s ./tshref -a "-p"
#
# trace10.txt - Process fg builtin command.
#
tsh> ./myspin 4 &
[1] (3510) ./myspin 4 &
tsh> fg %1
Job [1] (3510) stopped by signal 20
tsh> jobs
[1] (3510) Stopped ./myspin 4 &
tsh> fg %1
tsh> jobs

它针对的是fg这个内置命令的处理。

fg <job>命令通过发送SIGCONT信号给进程<job>,然后把它运行在前台中,<job>参数可以用PID或者JID

  • 如果参数是PID则直接输入pid即可
  • 如果参数是JID则输入“%jid”

可以看到上面的测试样例,一开始,是后台运行一个“./myspin 4”,然后把它放到前台运行,使用“fg %1”命令,切换到前台执行,shell等待它执行完毕。然后jobs查看看到有一个停止的工作,然后把它放到前台并开始运行。最后jobs查看,由于已经运行完了,所以没有输出。

对于这两个功能我们要实现的是do_fgbg函数

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
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char** argv)
{
struct job_t* job;
char* id = argv[1];

// 判断输入的是jid还是pid
if (id[0] == '%') { /* jid */
//去掉'%'开始读jid,根据jid返回这个子进程的结构指针
int jid = atoi(id + 1);
job = getjobjid(jobs, jid);
}
else { /* pid */
int pid = atoi(id);
job = getjobpid(jobs, pid);
}

/*
* kill不单是杀掉进程,还有发送信号的功能
* 这里唤醒job所在的组中的所有进程
* 就是唤醒这个stopped的子进程,以及它派生的孙子进程
*/
kill(-(job->pid), SIGCONT);

if (!strcmp(argv[0], "fg")) { // fg command
job->state = FG;
// 等待该前台作业终止
waitfg(job->pid);
}
else { // bg command
job->state = BG;
/* 切换到bg后打印作业信息 */
printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
}
}

然后我们在builtin_cmd中加入函数入口。

1
2
3
4
5
6
7
// in `builtin_cmd`

// bg or fg command
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 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
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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest09
./sdriver.pl -t trace09.txt -s ./tshref -a "-p"
#
# trace09.txt - Process bg builtin command
#
tsh> ./myspin 4 &
[1] (3627) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3629) stopped by signal 20
tsh> jobs
[1] (3627) Running ./myspin 4 &
[2] (3629) Stopped ./myspin 5
tsh> bg %2
[2] (3629) ./myspin 5
tsh> jobs
[1] (3627) Running ./myspin 4 &
[2] (3629) Running ./myspin 5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test09
./sdriver.pl -t trace09.txt -s ./tsh -a "-p"
#
# trace09.txt - Process bg builtin command
#
tsh> ./myspin 4 &
[1] (3638) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3640) terminated by signal 20
tsh> jobs
[1] (3638) Running ./myspin 4 &
[2] (3640) Stopped ./myspin 5
tsh> bg %2
[2] (3640) ./myspin 5
tsh> jobs
[1] (3638) Running ./myspin 4 &
[2] (3640) Running ./myspin 5


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest10
./sdriver.pl -t trace10.txt -s ./tshref -a "-p"
#
# trace10.txt - Process fg builtin command.
#
tsh> ./myspin 4 &
[1] (7350) ./myspin 4 &
tsh> fg %1
Job [1] (7350) stopped by signal 20
tsh> jobs
[1] (7350) Stopped ./myspin 4 &
tsh> fg %1
tsh> jobs
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test10
./sdriver.pl -t trace10.txt -s ./tsh -a "-p"
#
# trace10.txt - Process fg builtin command.
#
tsh> ./myspin 4 &
[1] (7361) ./myspin 4 &
tsh> fg %1
Job [1] (7361) stopped by signal 20
tsh> jobs
[1] (7361) Stopped ./myspin 4 &
tsh> fg %1
tsh> jobs

与参考一致,通关~

test11 & test12 & test13

先看看test11

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest11
./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#

要求我们接收到SIGINT的时候把它发送给前台进程所处的进程组的所有进程。这个在我们前面的代码中的sigint_handler就已经有所处理了。

然后看看test12

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest12
./sdriver.pl -t trace12.txt -s ./tshref -a "-p"
#
# trace12.txt - Forward SIGTSTP to every process in foreground process group
#

和测试11类似,我们接受到SIGTSTP的时候要把信号发给前台进程所处的进程组的所有进程。这个在sigtstp_handler就已经处理了。

再看看test13

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest13
./sdriver.pl -t trace13.txt -s ./tshref -a "-p"
#
# trace13.txt - Restart every stopped process in process group
#

这个测试检测的是能够重启一个进程(原来是stopped状态),这个功能在对于内置指令fg以及bg的处理上已经实现了。

对于Linux下的ps命令a参数表示显示所有的进程

状态码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Here are the different values that the s, stat and state output specifiers (header "STAT" or "S") will display to describe the state of a process:

D uninterruptible sleep (usually IO)
R running or runnable (on run queue)
S interruptible sleep (waiting for an event to complete)
T stopped by job control signal
t stopped by debugger during the tracing
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z defunct ("zombie") process, terminated but not reaped by its parent

For BSD formats and when the stat keyword is used, additional characters may be displayed:
< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+ is in the foreground process group

我们直接对比测试结果

  • test11
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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest11
./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
tsh> ./mysplit 4
Job [1] (7419) terminated by signal 2
tsh> /bin/ps a
PID TTY STAT TIME COMMAND
1190 tty4 Ss+ 0:00 /sbin/getty -8 38400 tty4
1197 tty5 Ss+ 0:00 /sbin/getty -8 38400 tty5
1207 tty2 Ss+ 0:00 /sbin/getty -8 38400 tty2
1209 tty3 Ss+ 0:00 /sbin/getty -8 38400 tty3
1230 tty6 Ss+ 0:00 /sbin/getty -8 38400 tty6
1352 tty7 Ss+ 23:36 /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none
1785 tty1 Ss+ 0:00 /sbin/getty -8 38400 tty1
6865 pts/0 Ss 0:00 bash
6971 pts/1 Ss+ 0:00 /bin/bash
7255 pts/0 R 39:18 ./tsh -p
7256 pts/0 Z 0:00 [echo] <defunct>
7414 pts/0 S+ 0:00 make rtest11
7415 pts/0 S+ 0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
7416 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tshref -a -p
7417 pts/0 S+ 0:00 ./tshref -p
7422 pts/0 R 0:00 /bin/ps a
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test11
./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
tsh> ./mysplit 4
Job [1] (7433) terminated by signal 2
tsh> /bin/ps a
PID TTY STAT TIME COMMAND
1190 tty4 Ss+ 0:00 /sbin/getty -8 38400 tty4
1197 tty5 Ss+ 0:00 /sbin/getty -8 38400 tty5
1207 tty2 Ss+ 0:00 /sbin/getty -8 38400 tty2
1209 tty3 Ss+ 0:00 /sbin/getty -8 38400 tty3
1230 tty6 Ss+ 0:00 /sbin/getty -8 38400 tty6
1352 tty7 Ss+ 23:37 /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none
1785 tty1 Ss+ 0:00 /sbin/getty -8 38400 tty1
6865 pts/0 Ss 0:00 bash
6971 pts/1 Ss+ 0:00 /bin/bash
7255 pts/0 R 39:26 ./tsh -p
7256 pts/0 Z 0:00 [echo] <defunct>
7428 pts/0 S+ 0:00 make test11
7429 pts/0 S+ 0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
7430 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tsh -a -p
7431 pts/0 R+ 0:02 ./tsh -p
7436 pts/0 R 0:00 /bin/ps a
  • test12test13略(答案太长了)

test14

这个测试需要对fgbg的输入参数进行一些错误处理。例如没有参数或参数非数值或所选任务或进程不存在等。修改do_bgfg函数如。(/ ADD PART /带有这个标签的就是增加的功能)

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
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char** argv)
{
struct job_t* job;
char* id = argv[1];

/* ADD PART bg和fg后边不跟任何参数不执行直接返回 */
// no argument for bg/fg
if (id == NULL)
{
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

// 判断输入的是jid还是pid
if (id[0] == '%') { /* jid */
/* ADD PART 检查输入的jid是不是数字 */
if (!checkNum(id + 1)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//去掉'%'开始读jid,根据jid返回这个子进程的结构指针
int jid = atoi(id + 1);
job = getjobjid(jobs, jid);
/* ADD PART 找不到输入的这个作业 */
if (job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
}
else { /* pid */
/* ADD PART 检查输入的pid是不是数字 */
if (!checkNum(id)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
int pid = atoi(id);
job = getjobpid(jobs, pid);
/* ADD PART 找不到这个作业 */
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
}
}

/*
* kill不单是杀掉进程,还有发送信号的功能
* 这里唤醒job所在的组中的所有进程
* 就是唤醒这个stopped的子进程,以及它派生的孙子进程
*/
kill(-(job->pid), SIGCONT);

if (!strcmp(argv[0], "fg")) { // fg command
job->state = FG;
// 等待该前台作业终止
waitfg(job->pid);
}
else { // bg command
job->state = BG;
/* 切换到bg后打印作业信息 */
printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
}
}

查看参考输出

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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest14
./sdriver.pl -t trace14.txt -s ./tshref -a "-p"
#
# trace14.txt - Simple error handling
#
tsh> ./bogus
./bogus: Command not found
tsh> ./myspin 4 &
[1] (7521) ./myspin 4 &
tsh> fg
fg command requires PID or %jobid argument
tsh> bg
bg command requires PID or %jobid argument
tsh> fg a
fg: argument must be a PID or %jobid
tsh> bg a
bg: argument must be a PID or %jobid
tsh> fg 9999999
(9999999): No such process
tsh> bg 9999999
(9999999): No such process
tsh> fg %2
%2: No such job
tsh> fg %1
Job [1] (7521) stopped by signal 20
tsh> bg %2
%2: No such job
tsh> bg %1
[1] (7521) ./myspin 4 &
tsh> jobs
[1] (7521) Running ./myspin 4 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test14
./sdriver.pl -t trace14.txt -s ./tsh -a "-p"
#
# trace14.txt - Simple error handling
#
tsh> ./bogus
tsh> ./myspin 4 &
[1] (7540) ./myspin 4 &
tsh> fg
fg command requires PID or %jobid argument
tsh> bg
bg command requires PID or %jobid argument
tsh> fg a
fg: argument must be a PID or %jobid
tsh> bg a
bg: argument must be a PID or %jobid
tsh> fg 9999999
(9999999): No such process
tsh> bg 9999999
(9999999): No such process
tsh> fg %2
%2: No such job
tsh> fg %1
Job [1] (7540) stopped by signal 20
tsh> bg %2
%2: No such job
tsh> bg %1
[1] (7540) ./myspin 4 &
tsh> jobs
[1] (7540) Running ./myspin 4 &

对于输入的不合法的pid不是数字(例如44行)或者没有没有参数(42行)或者单纯没有这个pid(48行),都会进行处理并返回而不是崩溃。这个功能的晚上增强了shell的鲁棒性。

和参考基本一致,通关~

test15 & test16

我们看看测试的要求

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest15
./sdriver.pl -t trace15.txt -s ./tshref -a "-p"
#
# trace15.txt - Putting it all together
#

测试15是要我们把所有的方法放在一起。这个都不要做,本来就在一个文件tsh.c下写的。。。

1
2
3
4
5
6
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest16
./sdriver.pl -t trace16.txt -s ./tshref -a "-p"
#
# trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT
# signals that come from other processes instead of the terminal.
#

这个测试要我们测试shell收到SIGINT或者SIGTSTP的来源不是中断而是其他进程是否仍然能够正常工作。这个也不需要做什么。原因有二

  • 我们前面的make testn的时候,就是把我们的tsh作为shell driver的子进程,然后驱动给我们的tsh发送测试指令。其中就有SIGINT和SIGTSTP,我们能过够通过前面的,肯定是对的。
  • 我们的tsh至始至终实现对的都是收到信号,然后对自己的孩子怎么处理。至于如何接受外部信号,是从中断收到还是其他的进程收到,我们实验过程中虽然没有关注到。但是我们在从unix的shell输入“./tsh”之后进入到tsh
    • 这个过程是内核捕捉到SIGINT等信号发送给我们的unix shell,然后unix shell会把这些信号有发送给tsh,然后tsh又把它发送给子进程们。
    • 关系如下图。

image-20220526221432408

还有一个小知识点:参考exit与_exit的区别,可以知道在fork出的child中要用_exit来退出,否则exit会调用用atexit注册的函数并刷新父进程的缓冲区。一般来说在一个main函数中只调用一次exit或return。

1
2
3
4
5
6
7
8
9
10
// in `eval`

/*
* 前台执行
* 如果没有这个可执行程序,那么直接终止这个子进程
*/
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
_exit(1);
}

tsh.c文件综合

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
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
/*
* tsh - A tiny shell program with job control
*
* <Puitar>
*/
#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);

// wrapper functions get from csapp.h
void Sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
void Sigemptyset(sigset_t* set);
void Sigaddset(sigset_t* set, int signum);

// Sio package declaration from csapp.h
/* Sio (Signal-safe I/O) routines */
ssize_t sio_puts(char s[]);
ssize_t sio_putl(long v);
void sio_error(char s[]);

/* Sio wrappers */
ssize_t Sio_puts(char s[]);
ssize_t Sio_putl(long v);
void Sio_error(char s[]);


/*
* 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]; /* argv for execve() */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */
sigset_t mask, prev;

bg = parseline(cmdline, argv);

if (argv[0] == NULL)
return; /* Ignore empty lines */

// 这里参考课本520的方法
/*
* sigemptyset是设置一个信号集
* sigaddset是向mask中添加SIGCHLD信号
* sigprocmask是保存现在的信号集到prev然后根据第一个参数设置信号集为mask
* 第二个参数设置成当前阻塞信号集合
*/
Sigemptyset(&mask);
Sigaddset(&mask, SIGCHLD);


if (!builtin_cmd(argv)) {
Sigprocmask(SIG_BLOCK, &mask, &prev); /* 阻塞SIGCHLD */

if ((pid = fork()) == 0) { /* Child runs user job */
/* 给fork出来的子进程设置一个独立的组,
* 子进程是这个组的组长,组id为子进程的pid */
setpgid(0, 0);

/* 对于子进程并不需要阻塞SIGCHLD的信号 */
Sigprocmask(SIG_UNBLOCK, &prev, NULL); // unblock SIGCHLD

/*
* 前台执行
* 如果没有这个可执行程序,那么直接终止这个子进程
*/
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
_exit(1);
}
}

addjob(jobs, pid, bg ? BG : FG, cmdline);
/* 已经加到jobs了,解除阻塞 */
Sigprocmask(SIG_SETMASK, &prev, NULL);


/* Parent waits for foreground job to terminate */
if (!bg) // 前台运行则等待子进程
waitfg(pid);
else // background
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}

/*
* 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)
{
// quit command
if (!strcmp(argv[0], "quit"))
exit(0);

// jobs command
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
}

// bg or fg command
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
}

// ignore singleton & (不处理单独的 '&')
if (!strcmp(argv[0], "&"))
return 1;

// not a build-in command
return 0;
}

// check if arg is a string of nums
int checkNum(char* arg) {
int len = strlen(arg);
int i;
for (i = 0; i < len; ++i) {
if (!isdigit(arg[i])) // not num
return 0;
}
return 1;
}

/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char** argv)
{
struct job_t* job;
char* id = argv[1];

// no argument for bg/fg
if (id == NULL)
{
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

// 判断输入的是jid还是pid
if (id[0] == '%') { /* jid */
if (!checkNum(id + 1)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//去掉'%'开始读jid,根据jid返回这个子进程的结构指针
int jid = atoi(id + 1);
job = getjobjid(jobs, jid);
if (job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
}
else { /* pid */
if (!checkNum(id)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
int pid = atoi(id);
job = getjobpid(jobs, pid);
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
}
}

/*
* kill不单是杀掉进程,还有发送信号的功能
* 这里唤醒job所在的组中的所有进程
* 就是唤醒这个stopped的子进程,以及它派生的孙子进程
*/
kill(-(job->pid), SIGCONT);

if (!strcmp(argv[0], "fg")) { // fg command
job->state = FG;
// 等待该前台作业终止
waitfg(job->pid);
}
else { // bg command
job->state = BG;
/* 切换到bg后打印作业信息 */
printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
}
}

/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
while (pid == fgpid(jobs))
sleep(0); // 这里是主动让出CPU
}

/*****************
* 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 old_errno = errno;

pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
// 子进程正常退出
if (WIFEXITED(status)) {
deletejob(jobs, pid);
}
// 子进程因为ctrl-c退出
if (WIFSIGNALED(status)) { // terminated by ctrl-c
/* printf("Job [%d] (%d) terminated by signal %2d\n",
pid2jid(pid), pid, WTERMSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") terminated by signal ");
Sio_putl(WTERMSIG(status));
Sio_puts("\n");
}
deletejob(jobs, pid);
}
if (WIFSTOPPED(status)) { // stopped by ctrl-z
/* printf("Job [%d] (%d) stopped by signal %2d\n",
pid2jid(pid), pid, WSTOPSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") stopped by signal ");
Sio_putl(WSTOPSIG(status));
Sio_puts("\n");
}
// 修改子进程状态为ST
getjobpid(jobs, pid)->state = ST;
}
}
errno = old_errno;
if (verbose) {
/* printf("sigchld_handler: exiting\n"); */ {
Sio_puts("sigchld_handler: exiting\n");
}
}
}

/*
* 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;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
// kill the group in the foreground
/*
* int kill(pid_t pid,int signo)
* 功能: 向进程或进程组发送一个信号 (成功返回 0; 否则,返回 -1 )
*/
kill(-fg_pid, sig);
// -fg_pid表示向进程组号为pid的组中的每个进程发sig信号
// 在这里就是向前台进程以及它的每一个子进程(子进程都在自己的父进程的pid为组id的组下)
errno = olderrno;
}

/*
* 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)
{
// 为了异步信号安全防止errno被覆盖
int olderrno = errno;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
// kill the group in the foreground
kill(-fg_pid, sig);
errno = olderrno;
}

/*********************
* 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++) {
/* 如果jobs数组里面有pid等于0等于0的表项,表示这个位置可以用,把这个作业放进去 */
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);
}


// The following function are from csapp.c

/*************************************************************
* Wrappers for blocking signals
*************************************************************/

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

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

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

/*************************************************************
* The Sio (Signal-safe I/O) package - simple reentrant output
* functions that are safe for signal handlers.
*************************************************************/

/* Private sio functions */

/* $begin sioprivate */
/* sio_reverse - Reverse a string (from K&R) */
static void sio_reverse(char s[])
{
int c, i, j;

for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}

/* sio_ltoa - Convert long to base b string (from K&R) */
static void sio_ltoa(long v, char s[], int b)
{
int c, i = 0;
int neg = v < 0;

if (neg)
v = -v;

do {
s[i++] = ((c = (v % b)) < 10) ? c + '0' : c - 10 + 'a';
} while ((v /= b) > 0);

if (neg)
s[i++] = '-';

s[i] = '\0';
sio_reverse(s);
}

/* sio_strlen - Return length of string (from K&R) */
static size_t sio_strlen(char s[])
{
int i = 0;

while (s[i] != '\0')
++i;
return i;
}
/* $end sioprivate */

/* Public Sio functions */
/* $begin siopublic */

ssize_t sio_puts(char s[]) /* Put string */
{
return write(STDOUT_FILENO, s, sio_strlen(s)); //line:csapp:siostrlen
}

ssize_t sio_putl(long v) /* Put long */
{
char s[128];

sio_ltoa(v, s, 10); /* Based on K&R itoa() */ //line:csapp:sioltoa
return sio_puts(s);
}

void sio_error(char s[]) /* Put error message and exit */
{
sio_puts(s);
_exit(1); //line:csapp:sioexit
}
/* $end siopublic */

/*******************************
* Wrappers for the SIO routines
******************************/
ssize_t Sio_putl(long v)
{
ssize_t n;

if ((n = sio_putl(v)) < 0)
sio_error("Sio_putl error");
return n;
}

ssize_t Sio_puts(char s[])
{
ssize_t n;

if ((n = sio_puts(s)) < 0)
sio_error("Sio_puts error");
return n;
}

void Sio_error(char s[])
{
sio_error(s);
}

实验总结

通过这次实验,才算是比较彻底理解了,信号的相关知识。

  • 包括信号的发送和捕捉
  • 异步的思考方式
  • 信号的阻塞
  • 拓展:异步安全问题
  • 信号处理程序的编写和使用
  • signal函数和kill以及相关的函数的使用等

这个是本课程最后一个实验的,综合难度并不是最大的。但是是知识体系最为复杂的。总的来说非常喜欢这门课的实验。做完之后能够感觉到和知识的紧密连接。感谢陪伴~