【深入理解计算机系统】ShellLab 实验
Unix Shell概述
Shell 是一个交互式命令行解释器,它等待用户输入命令,然后执行该命令。
命令行是由空格分隔的 ASCLL 码单词序列,第一个单词是内置命令的名称或者可执行文件的路径,剩下的单词则是命令行参数。如果第一个单词是内置命令,shell 立即执行该内置命令,否则,这个单词被认为是一个可执行文件的路径,此时 shell 创建一个子进程并在该子进程的上下文中运行该可执行文件。
解释单个命令行而创建的子进程统称为作业。通常,一个作业可以由多个由Unix管道连接的子进程组成。
如果命令行最后一个参数为 &
,那么该作业在后台运行,即执行该命令后,用户可以立即键入新的指令,否则 shell 将等待该作业执行完,才可以键入新的命令。
在任何时候只能有一个作业在前台,后台可以有任意个作业。
Unix shell 支持作业控制,允许用户将某个作业在前台以及后台中切换以及更改作业中进程的状态(运行,停止,终止),键入(ctrl+c) 会给前台进程组中的每个进程发送一个 SIGINT
信号,其默认行为是终止进程。类似,键入 (ctrl+z) 会给前台进程组中每个进程发送 SIGTSTP
信号,默认操作是停止进程,直到其收到 SIGCONT
信号。Unix shell 有几个内置命令支持作业控制:
- jobs:列出运行中以及停止的后台作业
- bg <job>:使停止的后台作业运行在后台
- fg <job>:使停止或运行中的后台作业运行在前台
- kill <job>: 终止一个作业
tsh 说明书
tsh shell 应该有以下特征:
- 提示符为:
tsh>
。 - 用户输入的命令行应该包含一个name和零个或多个参数,所有参数之间用一个或多个空格分隔。如果name是一个内置命令,那么tsh应该立即处理它,并等待下一个命令行。否则,tsh应该假定name是可执行文件的路径,它在初始子进程的上下文中加载并运行该文件。
- tsh 不用支持重定向和管道
- 输入
ctrl+c (ctrl+z)
时,应该发送SIGINT(SIGTSTP)
到前台进程组中每一个进程,(如果没有前台进程,这两个信号不起作用) - 如果命令以
&
结尾,tsh 应该将它们放在后台运行,否则放在前台运行(并等待其结束。 - 每一个 job 都有一个正整数 PID 以及 Job ID(JID),JID 通过'%'前缀标识,例如 %5 是 JID 为 5 的 job,5 则表示 PID 为 5 的进程。
- tsh 应该支持如下内置命令:
- quit 命令退出 shell
- jobs 命令列出所有 job
- bg <job> 通过发送
SIGCONT
信号重启一个 job,并在后台运行,<job> 参数可以是 JID 或者 PID。 - fg <job> 通过发送
SIGCONT
信号使得 <job> 运行在前台,<job> 参数可以是 JID 或者 PID。 - tsh应该回收所有僵尸子进程,如果一个 job 是因为收到了一个它没有捕获的信号而终止的,那么tsh应该输出这个工作的PID和这个信号的相关描述。
提示
- 实现信号处理函数时,确保向整个前台进程组发送
SIGINT
和SIGTSTP
。 - 在
waitfg
函数中,使用循环以及sleep
函数;在sigchld_handler
只使用一次waitpid
。 - 在
eval
中,父进程一定要用sigprocmask
在fork
函数调用前阻塞SIGCHLD
信号,然后在将子进程加入 job list 后解除阻塞。由于子进程继承了阻塞,要在子进程的开头解除阻塞。(书中解释了原因) - 当运行 shell 程序的时候,shell 是运行在前台进程组的,如果 shell 创建子进程,不管其是前台还是后天,子进程都会在这个进程组中,当键入
ctrl+c
时,会向前台进程组中的每个进程发送SIGINT
信号,会将 tsh 中的后台进程终止掉,在execve
之前使用setpgid(0,0)
改变子进程的进程组,使得只有 tsh 在前台进程组中,当 tsh 收到SIGINT
信号后,其再发送SIGINT
信号给 tsh 中的前台进程!
实现
本代码没有满足 安全的信号处理 的几个条件
待实现的几个函数:
void eval(char *cmdline); //处理用户命令
int builtin_cmd(char **argv, char *cmdline); //判断是否为内置命令,如果是,执行命令,返回 1;否则返回 0
void do_bgfg(char **argv, char *cmdline);//实现 bg 以及 fg 命令
void waitfg(pid_t pid);//等待前台进程
//信号处理程序
void sigchld_handler(int sig);//处理进程的状态变化
void sigtstp_handler(int sig);//向前台进程组发送 SIGTSTP 信号
void sigint_handler(int sig); //向前台进程组发送 SIGINT 信号
eval 函数
首先调用 parseline
解析命令行,解析完判断是否是空行。然后进入 builtin_cmd
函数,如果是内置命令,直接执行,并返回 1,否则返回 0,创建子进程,运行可执行文件,添加到任务列表。
注意改变子进程的进程组以及阻塞、解除阻塞 SIGCHLD
信号。
void eval(char *cmdline)
{
char *argv[MAXARGS];
sigset_t mask, premask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
int flag = parseline(cmdline, argv);
if (argv[0] == NULL)//一定要判断空行,不然会段错误
{
return;
}
if (!builtin_cmd(argv))
{
if (access(argv[0], F_OK)) /* 判断文件是否存在 */
{
printf("%s: Command not found\n", argv[0]);
return;
}
sigprocmask(SIG_BLOCK, &mask, &premask);//阻塞 sigchld 信号
pid_t pid;
if ((pid = fork()) == 0)//新建子进程运行可执行文件
{
sigprocmask(SIG_SETMASK, &premask, NULL);//子进程中解除信号
setpgid(0, 0); //修改子进程进程组
if (execve(argv[0], argv, environ) < 0)
{
printf("%s: Failed to execve\n", argv[0]);
exit(1);
}
}
else
{
if (!flag)//前台运行
{
fg_run = 1; //标识此时有前台进程在运行,用于 wiatfg 函数中
addjob(jobs, pid, FG, cmdline);
sigprocmask(SIG_SETMASK, &premask, NULL);//添加完 job 后解除 SIGCHLD 信号
waitfg(pid); //等待前台进程运行结束
}
else
{//同上
addjob(jobs, pid, BG, cmdline);
sigprocmask(SIG_SETMASK, &premask, NULL);
printf("[%d] (%d) %s", maxjid(jobs), pid, cmdline);
}
}
}
return;
}
builtin_cmd 函数
判断是否是内置命令,并执行内置命令
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "quit"))
{
exit(0);
}
else if (!strcmp(argv[0], "jobs"))
{
listjobs(jobs);
return 1;
}
else if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg"))
{
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
do_bgfg 函数
执行这个函数时,待处理的进程有两种状态:
-
停止
-
后台运行
处理思路:先将进程从停止运行变为后台运行,如果命令为 fg ,那么再将进程从后台运行变为前台运行。
void do_bgfg(char **argv)
{
if (argv[1] == NULL)
{
if (argv[0][0] == 'b')
printf("bg command requires PID or %%jobid argument\n");
else
printf("fg command requires PID or %%jobid argument\n");
return;
}
struct job_t *p;
if (argv[1][0] == '%')
{
int jid = atoi(argv[1] + 1);//参数不是数字
if (!jid)
{
if (argv[0][0] == 'b')
{
printf("bg: argument must be a PID or %%jobid\n");
}
else
{
printf("fg: argument must be a PID or %%jobid\n");
}
return;
}
p = getjobjid(jobs, jid);
}
else
{
pid_t pid = atoi(argv[1]);
if (!pid)//参数不是数字
{
if (argv[0][0] == 'b')
{
printf("bg: argument must be a PID or %%jobid\n");
}
else
{
printf("fg: argument must be a PID or %%jobid\n");
}
return;
}
p = getjobpid(jobs, pid);
}
if (p == NULL)//没有找到相应的进程或 job
{
if (argv[1][0] == '%')
printf("%s: No such job\n", argv[1]);
else
printf("(%s): No such process\n", argv[1]);
return;
}
if (p->state == ST)//先将进程从停止转变为后台运行
{
if (argv[0][0] == 'b')
{
printf("[%d] (%d) %s", p->jid, p->pid, p->cmdline);
}
p->state = BG;
kill(-p->pid, SIGCONT);
}
if (argv[0][0] == 'f')//如果命令为 fg,再将进程从后台转变为前台
{
p->state = FG;
fg_run = 1;
waitfg(fgpid(jobs));
}
return;
}
waitfg 函数
fg_run
为全局变量,表示前台程序此时的状态。如果为 1,正在运行,为 0,则终止或停止。
void waitfg(pid_t pid)
{
while (fg_run)
{
sleep(1);
}
return;
}
sigchld_handler 函数
值得注意的是:
当 子进程终止 或 子进程停止 或 子进程从停止状态到执行状态 都会向父进程发送 SIGCHLD 信号。
之前在这个函数中我写的是 waitpid(-1,status,0)
,此时如果使用 bg 命令,由于进程从停止状态切换到执行状态时发送了 SIGCHLD
信号,就会执行信号处理程序,但此时并没有进程终止掉,直接阻塞在了 waitpid
上。找了好几个小时 bug,最后还是在参考的博客上发现的原因。
可以通过 WNOHANG | WUNTRACED | WCONTINUED
这三个参数的组合使用,实现不阻塞的捕捉到上述三个进程状态的切换。
void sigchld_handler(int sig)
{
pid_t pid;
int status;
if ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED)))
{
if (fg_run == 1 && pid == fgpid(jobs))
{
fg_run = 0;
}
if (WIFEXITED(status))//进程正常终止
{
deletejob(jobs, pid);
}
else if (WIFSIGNALED(status))//进程因未被捕获的信号终止
{
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if (WIFSTOPPED(status))//进程停止
{
struct job_t *p = getjobpid(jobs, pid);
p->state = ST;
printf("Job [%d] (%d) stopped by signal 20\n", pid2jid(pid), pid);
}
/*
还可以加上 WIFCONTINUED (status) 判断进程继续,不过没有必要,进程继续什么也不会做
*/
}
return;
}
sigint_handler 函数
ctrl-c: 发送 SIGINT 信号给前台进程组中的所有进程。
运行 tsh 程序时,tsh 在 Linux Shell 中的前台进程组中,当键入ctrl+c
时,因为在 eval
函数中已经改变了其子进程的进程组,只有 tsh 会收到 SIGINT
信号,而 tsh 的所有子进程(包括前台和后台)都不会收到 SIGINT
信号,所以 tsh 中的 SIGINT
信号处理程序只是向 tsh 中的前台进程组发送 SIGINT
信号!
void sigint_handler(int sig)
{
if (fg_run)
{
kill(-fgpid(jobs), SIGINT);
}
return;
}
sigtstp_handler 函数
ctrl-z: 发送 SIGTSTP 信号给前台进程组中的所有进程。
与 sigint_handler
类似,向 tsh
中的所有前台进程发送 SIGTSTP
信号。
void sigtstp_handler(int sig)
{
if (fg_run)
{
kill(-fgpid(jobs), SIGTSTP);
}
return;
}
检验结果
使用 make
命令编译文件下的代码。
借用参考博客中的 shell 脚本。
#! /bin/bash
for file in $(ls trace*)
do
./sdriver.pl -t $file -s ./tshref > tshref_$file
./sdriver.pl -t $file -s ./tsh > tsh_$file
done
for file in $(ls trace*)
do
diff tsh_$file tshref_$file > diff_$file
done
for file in $(ls diff_trace*)
do
echo $file " :"
cat $file
echo -e "-------------------------------------\n"
done
运行此脚本后,会生成 tsh_trace**.txt
、 tshref_trace**.txt
以及这俩文件的比对结果 diff_trace**.txt
,并在终端输出对应文件的不同。如果程序正确,那么在 diff_trace**.txt
中只有进程号不相同。
diff_trace01.txt :
-------------------------------------
diff_trace02.txt :
-------------------------------------
diff_trace03.txt :
-------------------------------------
diff_trace04.txt :
5c5
< tsh> [1] (2770) ./myspin 1 &
---
> tsh> [1] (2766) ./myspin 1 &
-------------------------------------
diff_trace05.txt :
5c5
< tsh> [1] (2781) ./myspin 2 &
---
> tsh> [1] (2774) ./myspin 2 &
7c7
< tsh> [2] (2783) ./myspin 3 &
---
> tsh> [2] (2776) ./myspin 3 &
9,10c9,10
< tsh> [1] (2781) Running ./myspin 2 &
< [2] (2783) Running ./myspin 3 &
---
> tsh> [1] (2774) Running ./myspin 2 &
> [2] (2776) Running ./myspin 3 &
-------------------------------------
diff_trace06.txt :
5c5
< tsh> Job [1] (2792) terminated by signal 2
---
> tsh> Job [1] (2788) terminated by signal 2
-------------------------------------
diff_trace07.txt :
5c5
< tsh> [1] (2803) ./myspin 4 &
---
> tsh> [1] (2796) ./myspin 4 &
7c7
< tsh> Job [2] (2805) terminated by signal 2
---
> tsh> Job [2] (2798) terminated by signal 2
9c9
< tsh> [1] (2803) Running ./myspin 4 &
---
> tsh> [1] (2796) Running ./myspin 4 &
-------------------------------------
diff_trace08.txt :
5c5
< tsh> [1] (2818) ./myspin 4 &
---
> tsh> [1] (2811) ./myspin 4 &
7c7
< tsh> Job [2] (2820) stopped by signal 20
---
> tsh> Job [2] (2813) stopped by signal 20
9,10c9,10
< tsh> [1] (2818) Running ./myspin 4 &
< [2] (2820) Stopped ./myspin 5
---
> tsh> [1] (2811) Running ./myspin 4 &
> [2] (2813) Stopped ./myspin 5
-------------------------------------
diff_trace09.txt :
5c5
< tsh> [1] (2834) ./myspin 4 &
---
> tsh> [1] (2825) ./myspin 4 &
7c7
< tsh> Job [2] (2836) stopped by signal 20
---
> tsh> Job [2] (2827) stopped by signal 20
9,10c9,10
< tsh> [1] (2834) Running ./myspin 4 &
< [2] (2836) Stopped ./myspin 5
---
> tsh> [1] (2825) Running ./myspin 4 &
> [2] (2827) Stopped ./myspin 5
12c12
< tsh> [2] (2836) ./myspin 5
---
> tsh> [2] (2827) ./myspin 5
14,15c14,15
< tsh> [1] (2834) Running ./myspin 4 &
< [2] (2836) Running ./myspin 5
---
> tsh> [1] (2825) Running ./myspin 4 &
> [2] (2827) Running ./myspin 5
-------------------------------------
diff_trace10.txt :
5c5
< tsh> [1] (2851) ./myspin 4 &
---
> tsh> [1] (2843) ./myspin 4 &
7c7
< tsh> Job [1] (2851) stopped by signal 20
---
> tsh> Job [1] (2843) stopped by signal 20
9c9
< tsh> [1] (2851) Stopped ./myspin 4 &
---
> tsh> [1] (2843) Stopped ./myspin 4 &
-------------------------------------
diff_trace11.txt :
5c5
< tsh> Job [1] (2866) terminated by signal 2
---
> tsh> Job [1] (2859) terminated by signal 2
40,42c40,42
< 2863 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tsh
< 2864 pts/4 S+ 0:00 ./tsh
< 2869 pts/4 R 0:00 /bin/ps a
---
> 2856 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tshref
> 2857 pts/4 S+ 0:00 ./tshref
> 2862 pts/4 R 0:00 /bin/ps a
-------------------------------------
diff_trace12.txt :
5c5
< tsh> Job [1] (2881) stopped by signal 20
---
> tsh> Job [1] (2873) stopped by signal 20
7c7
< tsh> [1] (2881) Stopped ./mysplit 4
---
> tsh> [1] (2873) Stopped ./mysplit 4
42,46c42,46
< 2878 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace12.txt -s ./tsh
< 2879 pts/4 S+ 0:00 ./tsh
< 2881 pts/4 T 0:00 ./mysplit 4
< 2882 pts/4 T 0:00 ./mysplit 4
< 2885 pts/4 R 0:00 /bin/ps a
---
> 2870 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace12.txt -s ./tshref
> 2871 pts/4 S+ 0:00 ./tshref
> 2873 pts/4 T 0:00 ./mysplit 4
> 2874 pts/4 T 0:00 ./mysplit 4
> 2877 pts/4 R 0:00 /bin/ps a
-------------------------------------
diff_trace13.txt :
5c5
< tsh> Job [1] (2900) stopped by signal 20
---
> tsh> Job [1] (2889) stopped by signal 20
7c7
< tsh> [1] (2900) Stopped ./mysplit 4
---
> tsh> [1] (2889) Stopped ./mysplit 4
42,46c42,46
< 2897 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tsh
< 2898 pts/4 S+ 0:00 ./tsh
< 2900 pts/4 T 0:00 ./mysplit 4
< 2901 pts/4 T 0:00 ./mysplit 4
< 2904 pts/4 R 0:00 /bin/ps a
---
> 2886 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tshref
> 2887 pts/4 S+ 0:00 ./tshref
> 2889 pts/4 T 0:00 ./mysplit 4
> 2890 pts/4 T 0:00 ./mysplit 4
> 2893 pts/4 R 0:00 /bin/ps a
82,84c82,84
< 2897 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tsh
< 2898 pts/4 S+ 0:00 ./tsh
< 2907 pts/4 R 0:00 /bin/ps a
---
> 2886 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tshref
> 2887 pts/4 S+ 0:00 ./tshref
> 2896 pts/4 R 0:00 /bin/ps a
-------------------------------------
diff_trace14.txt :
7c7
< tsh> [1] (2929) ./myspin 4 &
---
> tsh> [1] (2913) ./myspin 4 &
23c23
< tsh> Job [1] (2929) stopped by signal 20
---
> tsh> Job [1] (2913) stopped by signal 20
27c27
< tsh> [1] (2929) ./myspin 4 &
---
> tsh> [1] (2913) ./myspin 4 &
29c29
< tsh> [1] (2929) Running ./myspin 4 &
---
> tsh> [1] (2913) Running ./myspin 4 &
-------------------------------------
diff_trace15.txt :
7c7
< tsh> Job [1] (2963) terminated by signal 2
---
> tsh> Job [1] (2946) terminated by signal 2
9c9
< tsh> [1] (2965) ./myspin 3 &
---
> tsh> [1] (2948) ./myspin 3 &
11c11
< tsh> [2] (2967) ./myspin 4 &
---
> tsh> [2] (2950) ./myspin 4 &
13,14c13,14
< tsh> [1] (2965) Running ./myspin 3 &
< [2] (2967) Running ./myspin 4 &
---
> tsh> [1] (2948) Running ./myspin 3 &
> [2] (2950) Running ./myspin 4 &
16c16
< tsh> Job [1] (2965) stopped by signal 20
---
> tsh> Job [1] (2948) stopped by signal 20
18,19c18,19
< tsh> [1] (2965) Stopped ./myspin 3 &
< [2] (2967) Running ./myspin 4 &
---
> tsh> [1] (2948) Stopped ./myspin 3 &
> [2] (2950) Running ./myspin 4 &
23c23
< tsh> [1] (2965) ./myspin 3 &
---
> tsh> [1] (2948) ./myspin 3 &
25,26c25,26
< tsh> [1] (2965) Running ./myspin 3 &
< [2] (2967) Running ./myspin 4 &
---
> tsh> [1] (2948) Running ./myspin 3 &
> [2] (2950) Running ./myspin 4 &
-------------------------------------
diff_trace16.txt :
6c6
< tsh> Job [1] (2986) stopped by signal 20
---
> tsh> Job [1] (2979) stopped by signal 20
8c8
< tsh> [1] (2986) Stopped ./mystop 2
---
> tsh> [1] (2979) Stopped ./mystop 2
10c10
< tsh> Job [2] (2989) terminated by signal 2
---
> tsh> Job [2] (2982) terminated by signal 2
-------------------------------------
完整程序
https://paste.ubuntu.com/p/GFWMDYG7Tn/
这次让我感受到 make 的便捷以及 Git 的重要性,好不容易写完的程序,莫名其妙没了,害的我又重新搞了一遍。
参考博客
https://www.cnblogs.com/liqiuhao/p/8120617.html