前言:强烈建议先看完csapp第八章再做此实验,完整的tsh.c代码贴在文章末尾了
shell lab主要目的是为了熟悉进程控制和信号。具体来说需要比对16个test和rtest文件的输出,实现七个函数:
void eval(char *cmdline):分析命令,并派生子进程执行 主要功能是解析cmdline并运行
int builtin_cmd(char **argv):解析和执行bulidin命令,包括 quit, fg, bg, and jobs
void do_bgfg(char **argv) 执行bg和fg命令
void waitfg(pid_t pid):实现阻塞等待前台程序运行结束
void sigchld_handler(int sig):SIGCHID信号处理函数
void sigint_handler(int sig):信号处理函数,响应 SIGINT (ctrl-c) 信号
void sigtstp_handler(int sig):信号处理函数,响应 SIGTSTP (ctrl-z) 信号
ubuntu12.04 (32位)环境
通过阅读实验指导书我们知道此实验要求我们完成tsh.c中的七个函数从而实现一个简单的shell,能够处理前后台运行程序、能够处理ctrl+z、ctrl+c等信号。
首先我们来看一下tsh.c具体内容。
首先定义了一些宏
/* 定义了一些宏 */
#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 */
定义了四种进程状态
/* 工作状态 */
#define UNDEF 0 /* undefined */
#define FG 1 /* 前台状态 */
#define BG 2 /* 后台状态 */
#define ST 3 /* 挂起状态 */
然后定义了job_t的任务的类,并且创建了jobs[]数组
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 */
接着是需要我们完成的七个函数定义
void eval(char *cmdline):分析命令,并派生子进程执行 主要功能是解析cmdline并运行
int builtin_cmd(char **argv):解析和执行bulidin命令,包括 quit, fg, bg, and jobs
void do_bgfg(char **argv) 执行bg和fg命令
void waitfg(pid_t pid):实现阻塞等待前台程序运行结束
void sigchld_handler(int sig):SIGCHID信号处理函数
void sigint_handler(int sig):信号处理函数,响应 SIGINT (ctrl-c) 信号
void sigtstp_handler(int sig):信号处理函数,响应 SIGTSTP (ctrl-z) 信号
下面就是一些辅助的函数
int parseline(const char *cmdline, char **argv); //获取参数列表,返回是否为后台运行命令
void sigquit_handler(int sig); //处理SIGQUIT信号
void clearjob(struct job_t *job); //清除job结构体
void initjobs(struct job_t *jobs); //初始化任务jobs[]
int maxjid(struct job_t *jobs); //返回jobs链表中最大的jid号。
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline); //向jobs[]添加一个任务
int deletejob(struct job_t *jobs, pid_t pid); //在jobs[]中删除pid的job
pid_t fgpid(struct job_t *jobs); //返回当前前台运行job的pid号
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); //根据pid找到对应的job
struct job_t *getjobjid(struct job_t *jobs, int jid); //根据jid找到对应的job
int pid2jid(pid_t pid); //根据pid找到jid
void listjobs(struct job_t *jobs); //打印jobs
接着就是mian函数,作用是在文件中逐行获取命令,并且判断是不是文件结束(EOF),将命令cmdline送入eval函数进行解析。我们需要做的就是逐步完善这个过程
接下来开始实验:
bo@bo:~/shlab-handout$ make
gcc -Wall -O2 tsh.c -o tsh
gcc -Wall -O2 myspin.c -o myspin
gcc -Wall -O2 mysplit.c -o mysplit
gcc -Wall -O2 mystop.c -o mystop
gcc -Wall -O2 myint.c -o myint
使用make testXX指令比较traceXX.txt文件在编写的shell和reference shell的运行结果;或者也可以使用”./sdriver.pl -t traceXX.txt -s ./tsh -a “-p”
如果在文件名前面加上r,则是执行标准的tshref,或者将tsh变为tshref。通过比对标准tshref和自制tsh的执行结果结果,可以观察tsh的功能是否正确。如果tsh的执行结果和tshref结果一致,说明结果是正确的
接下来我们开始补充函数
函数功能:eval函数用于解析和解释命令行。eval首先解析命令行,如果用户请求一个内置命令quit、jobs、bg或fg(即内置命令)那么就立即执行。否则,fork子进程和在子进程的上下文中运行作业。如果作业正在运行前台,等待它终止,然后返回。
函数原型:void eval(char *cmdline)
,传入的参数为cmdline,即命令行字符串
实现思路:仿照书上的eval函数写法和所需的功能来完成函数
完整代码:
void eval(char *cmdline)
{
char* argv[MAXARGS]; //execve()函数的参数
int state = UNDEF; //工作状态,FG或BG
sigset_t set;
pid_t pid; //进程id
// 处理输入的数据
if(parseline(cmdline, argv) == 1) //解析命令行,返回给argv数组
state = BG;
else
state = FG;
if(argv[0] == NULL) //命令行为空直接返回
return;
// 如果不是内置命令
if(!builtin_cmd(argv))
{
if(sigemptyset(&set) < 0)
unix_error("sigemptyset error");
if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)
unix_error("sigaddset error");
//在它派生子进程之前阻塞SIGCHLD信号,防止竞争
if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if((pid = fork()) < 0) //fork创建子进程失败
unix_error("fork error");
else if(pid == 0) //fork创建子进程
{
// 子进程的控制流开始
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) //解除阻塞
unix_error("sigprocmask error");
if(setpgid(0, 0) < 0) //设置子进程id
unix_error("setpgid error");
if(execve(argv[0], argv, environ) < 0){
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// 将当前进程添加进job中,无论是前台进程还是后台进程
addjob(jobs, pid, state, cmdline);
// 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// 判断子进程类型并做处理
if(state == FG)
waitfg(pid); //前台作业等待
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); //将进程id映射到job id
}
return;
}
注意:
函数功能:识别并执行内置命令: quit, fg, bg, 和 jobs。
函数原型:int builtin_cmd(char **argv)
,参数为argv 参数列表
实现思路:
当命令行参数为quit时,直接终止shell
当命令行参数为jobs时,调用listjobs函数,显示job列表
当命令行参数为bg或fg时,调用do_bgfg函数,执行内置的bg和fg命令
不是内置命令时返回0
完整代码:
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0], "quit")) //如果命令是quit,退出
exit(0);
else if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) //如果是bg或者fg命令,执行do_fgbg函数
do_bgfg(argv);
else if(!strcmp(argv[0], "jobs")) //如果命令是jobs,列出正在运行和停止的后台作业
listjobs(jobs);
else
return 0; /* not a builtin command */
return 1;
}
函数功能:实现内置命令bg 和 fg
首先要明确的是bg和bg的作用
bg
:将停止的后台作业更改为正在运行的后台作业。通过发送SIGCONT信号重新启动,然后在后台运行它。
参数可以是PID,也可以是JID。ST -> BG
fg
:将已停止或正在运行的后台作业更改为前台正在运行的作业。通过发送SIGCONT信号重新启,然后在前台运行它。
参数可以是PID,也可以是JID。ST -> FG,BG -> FG
函数原型:void do_bgfg(char **argv)
,参数为argv 参数列表
实现思路:
完整代码:
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// 没有参数的fg/bg应该被丢弃
if(!argv[1]){ //命令行为空
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return ;
}
// 检测fg/bg参数,其中%开头的数字是JobID,纯数字的是PID
if(argv[1][0] == '%'){ //解析jid
if((num = strtol(&argv[1][1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobjid(jobs, num)) == NULL){
printf("%%%d: No such job\n", num); //没找到对应的job
return;
}
} else {
if((num = strtol(argv[1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobpid(jobs, num)) == NULL){
printf("(%d): No such process\n", num); //没找到对应的进程
return;
}
}
if(!strcmp(argv[0], "bg")){
// bg会启动子进程,并将其放置于后台执行
job->state = BG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else if(!strcmp(argv[0], "fg")) {
job->state = FG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
// 当一个进程被设置为前台执行时,当前tsh应该等待该子进程结束
waitfg(job->pid);
} else {
puts("do_bgfg: Internal error");
exit(0);
}
return;
}
函数功能:等待一个前台作业结束,或者说是阻塞一个前台的进程直到这个进程变为后台进程
函数原型:void waitfg(pid_t pid)
,参数为进程ID
实现思路:判断当前的前台的进程组pid是否和当前进程的pid是否相等,如果相等则sleep直到前台进程结束。
完整代码:
void waitfg(pid_t pid)
{
struct job_t *job = getjobpid(jobs, pid);
if(!job) return;
// 如果当前子进程的状态没有发生改变,则tsh继续休眠
while(job->state == FG)
// 使用sleep的这段代码会比较慢,最好使用sigsuspend
sleep(1);
return;
}
函数功能:处理SIGCHILD信号
函数原型:void sigchld_handler(int sig)
,参数为信号类型
首先了解一下父进程回收子进程的过程:当一个子进程终止或者停止时,内核会发送一个SIGCHLD信号给父进程。因此父进程必须回收子进程,以避免在系统中留下僵死进程。父进程捕获这个SIGCHLD信号,回收一个子进程。一个进程可以通过调用 waitpid 函数来等待它的子进程终止或者停止。如果回收成功,则返回为子进程的 PID, 如果 WNOHANG, 则返回为 0, 如果其他错误,则为 -1。
实现思路:
完整代码:
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if(verbose)
puts("sigchld_handler: entering");
/*
以非阻塞方式等待所有子进程
waitpid 参数3:
1. 0 : 执行waitpid时, 只有在子进程 **终止** 时才会返回。
2. WNOHANG : 若子进程仍然在运行,则返回0 。
注意只有设置了这个标志,waitpid才有可能返回0
3. WUNTRACED : 如果子进程由于传递信号而停止,则马上返回。
只有设置了这个标志,waitpid返回时,其WIFSTOPPED(status)才有可能返回true
*/
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// 如果当前这个子进程的job已经删除了,则表示有错误发生
if((job = getjobpid(jobs, pid)) == NULL){
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
//接下来判断三种状态
// 如果这个子进程收到了一个暂停信号(还没退出)
if(WIFSTOPPED(status)){
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
job->state = ST; //状态设为挂起
}
// 如果子进程通过调用 exit 或者一个返回 (return) 正常终止
else if(WIFEXITED(status)){
if(deletejob(jobs, pid))
if(verbose){
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
printf("sigchld_handler: Job [%d] (%d) terminates OK (status %d)\n", jid, pid, WEXITSTATUS(status));
}
}
// 如果子进程是因为一个未被捕获的信号终止的,例如SIGKILL
else {
if(deletejob(jobs, pid)){ //清除进程
if(verbose)
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
}
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status)); //返回导致子进程终止的信号的数量
}
}
if(verbose)
puts("sigchld_handler: exiting");
return;
}
函数功能:捕获SIGINT信号
函数原型:void sigchld_handler(int sig)
,参数为信号类型
实现思路:
完整代码:
void sigint_handler(int sig)
{
if(verbose)
puts("sigint_handler: entering");
pid_t pid = fgpid(jobs);
if(pid){
// 发送SIGINT给前台进程组里的所有进程
// 需要注意的是,前台进程组内的进程除了当前前台进程以外,还包括前台进程的子进程。
// 最多只能存在一个前台进程,但前台进程组内可以存在多个进程
if(kill(-pid, SIGINT) < 0)
unix_error("kill (sigint) error");
if(verbose){
printf("sigint_handler: Job (%d) killed\n", pid);
}
}
if(verbose)
puts("sigint_handler: exiting");
return;
}
函数功能:同sigint_handler差不多,捕获SIGTSTP信号
函数原型:void sigtstp_handler(int sig)
,参数为信号类型
首先了解一下SIGTSTP的作用:SIGTSPT信号默认行为是停止直到下一个 SIGCONT,是来自终端的停止信号,在键盘上输入 CTR+Z会导致一个 SIGTSPT信号被发送到外壳。外壳捕获该信号,然后发送SIGTSPT信号到这个前台进程组中的每个进程。在默认情况下,结果是停止或挂起前台作业。
实现思路:
完整代码:
void sigtstp_handler(int sig)
{
if(verbose)
puts("sigstp_handler: entering");
pid_t pid = fgpid(jobs);
struct job_t *job = getjobpid(jobs, pid);
if(pid){
if(kill(-pid, SIGTSTP) < 0)
unix_error("kill (tstp) error");
if(verbose){
printf("sigstp_handler: Job [%d] (%d) stopped\n", job->jid, pid);
}
}
if(verbose)
puts("sigstp_handler: exiting");
return;
}
注意:使用kill函数,如果 pid 小于零才会发送信号sig 给进程组中的每个进程,因此这里使用-pid。
至此tsh.c文件完成。
接下来我们分析以下测试文件的内容并看下我们的函数能否完成测试
首先了解一下一些测试文件的符号和命令的定义:
符号:
命令:
bg
: 将停止的后台作业更改为正在运行的后台作业fg
:将已停止或正在运行的后台作业更改为前台正在运行的作业用户程序:
首先打开 trace01.txt查看文件内容
#
# trace01.txt - Properly terminate on EOF.
#
CLOSE
WAIT
#可以理解为//,不需要我们解析。第一关调用了linux命令close关闭文件并wait等待,在EOF上正常终止,所以不需要我们做任何事
输入make test01
和make rtest01
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace02.txt查看文件内容
#
# trace02.txt - Process builtin quit command.
#
quit
WAIT
第二关需要我们针对输入的命令quit退出shell进程,我们需要解析cmdline(输入的命令),判断是不是“quit”字符串,是就退出。
输入make test02
和make rtest02
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace03.txt查看文件内容
#
# trace03.txt - Run a foreground job.
#
/bin/echo tsh> quit
quit
这里解释一下/bin/echo:
eval函数先通过builtin_cmd查询cmdline是不是内置命令如quit,如果是则当前进程执行命令
如果不是则创建一个子进程,在子进程中调用 execve()函数通过 argv[0]来寻找路径,并在子进程中运行路径中的可执行文件,如果找不到则说明命令为无效命令,输出命令无效,并用 exit(0)结束该子进程
/bin/echo就是打开bin目录下的echo文件,echo可以理解为将其后面的内容当作字符串输出
所以第三关的任务是:
输入make test03
和make rtest03
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace04.txt查看文件内容
#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &
先在前台执行echo命令,等待程序执行完毕回收子进程。&代表是一个后台程序,myspin睡眠1秒,然后停止。因为在后台,所以显示下面一句,如果在前台则无。
输入make test04
和make rtest04
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace05.txt查看文件内容
#
# trace05.txt - Process jobs builtin command.
#
/bin/echo -e tsh> ./myspin 2 \046
./myspin 2 &
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo tsh> jobs
jobs
分别运行了前台echo、后台myspin、前台echo、后台myspin,然后需要实现一个内置命令job,功能是显示目前任务列表中的所有任务的所有属性
输入make test05
和make rtest05
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace06.txt查看文件内容
#
# trace06.txt - Forward SIGINT to foreground job.
#
/bin/echo -e tsh> ./myspin 4
./myspin 4
SLEEP 2
INT
接收到了中断信号SIGINT(即CTRL_C)那么结束前台进程
输入make test06
和make rtest06
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace07.txt查看文件内容
#
# trace07.txt - Forward SIGINT only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
INT
/bin/echo tsh> jobs
jobs
根据注释,我们可以知道第七关测试的是只将SIGINT转发给前台作业。这里的命令行其实根据前面的就很好理解了,就是给出两个作业,一个在前台工作,另一个在后台工作,接下来传递SIGINT指令,然后调用内置指令jobs来查看此时的工作信息,来对比出是不是只将SIGINT转发给前台作业。
输入make test07
和make rtest07
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace08.txt查看文件内容
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
根据注释我们是需要将SIGTSTP转发给前台作业。根据这个信号的作用,也就是该进程会停止直到下一个SIGCONT也就是挂起,让别的程序继续运行。这里也就是运行了后台程序,然后使用jobs来打印出进程的信息。
输入make test08
和make rtest08
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace09.txt查看文件内容
#
# trace09.txt - Process bg builtin command
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> bg %2
bg %2
/bin/echo tsh> jobs
jobs
这里是在第八关的测试文件之上的一个更加完整的测试,这里也就是在停止后,输出进程信息之后,使用bg命令来唤醒进程2,也就是刚才被挂起的程序,接下来继续使用Jobs命令来输出结果。
输入make test09
和make rtest09
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace10.txt查看文件内容
#
# trace10.txt - Process fg builtin command.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
SLEEP 1
/bin/echo tsh> fg %1
fg %1
SLEEP 1
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> jobs
jobs
这里是将后台的进程更改为前台正在运行的程序。测试文中进程1根据&可以知道,进程1是一个后台进程。先使用fg命令将其转化为前台的一个程序,接下来停止进程1,然后打印出进程信息,这时候进程1应该是前台程序同时被挂起了,接下来使用fg命令使其继续运行,使用jobs来打印出进程信息
输入make test10
和make rtest10
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace11.txt查看文件内容
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
INT
/bin/echo tsh> /bin/ps a
/bin/ps a
根据注释我们可以知道这里需要将SIGINT发给前台进程组中的每个进程。ps –a 显示所有进程,这里是有两个进程的,mysplit创建了一个子进程,接下来发送指令SIGINT,所以进程组中的所有进程都应该停止,接下来调用pl来查看该进程组中的每个进程是否都停止了。
输入make test11
和make rtest11
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace12.txt查看文件内容
#
# trace12.txt - Forward SIGTSTP to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> /bin/ps a
/bin/ps a
根据注释可知该测试程序是为了测试将SIGTSTP转发给前台进程组中的每个进程。与上一关相同,只需要相应的进程被挂起即可。
输入make test12
和make rtest12
查看运行结果
打开 trace13.txt查看文件内容
#
# trace13.txt - Restart every stopped process in process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> /bin/ps a
/bin/ps a
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> /bin/ps a
/bin/ps a
根据注释我们可以知道该程序是为了测试重新启动进程组中的每个停止的进程。这里也就是使用fg来唤醒整个工作,中间使用ps -a来查看停止整个工作和唤醒整个工作的区别。
输入make test13
和make rtest13
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace14.txt查看文件内容
#
# trace14.txt - Simple error handling
#
/bin/echo tsh> ./bogus
./bogus
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo tsh> fg
fg
/bin/echo tsh> bg
bg
/bin/echo tsh> fg a
fg a
/bin/echo tsh> bg a
bg a
/bin/echo tsh> fg 9999999
fg 9999999
/bin/echo tsh> bg 9999999
bg 9999999
/bin/echo tsh> fg %2
fg %2
/bin/echo tsh> fg %1
fg %1
SLEEP 2
TSTP
/bin/echo tsh> bg %2
bg %2
/bin/echo tsh> bg %1
bg %1
/bin/echo tsh> jobs
jobs
根据注释可以知道这个文件是为了测试简单的错误处理。这里的测试文件,也就是测试fg和bg后面的参数,我们知道fg和bg后面需要一个JID或者是PID,其中JID是加上%的整型数。其余参数都应该报错,或是没有参数也应该报错。接下来测试的功能,都在上面的关卡测试过了
输入make test14
和make rtest14
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace15.txt查看文件内容
#
# trace15.txt - Putting it all together
#
/bin/echo tsh> ./bogus
./bogus
/bin/echo tsh> ./myspin 10
./myspin 10
SLEEP 2
INT
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> bg %3
bg %3
/bin/echo tsh> bg %1
bg %1
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> quit
quit
根据注释这个测试文件测试的是把它们放在一起。本来还纳闷放在一起是什么意思呢?仔细阅读测试文件,可以知道他是测试了上述所有命令,如jobs,fg,bg,quit。
输入make test15
和make rtest15
查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace16.txt查看文件内容
#
# trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT
# signals that come from other processes instead of the terminal.
#
/bin/echo tsh> ./mystop 2
./mystop 2
SLEEP 3
/bin/echo tsh> jobs
jobs
/bin/echo tsh> ./myint 2
./myint 2
这个测试文件的具体含义就是,用户程序向job 2传送了中止信号,所以最后会输出进程2被中止的信息。同时,mystop需要自己停止才能给别的进程发送信号,所以中间也会出现进程1被中止的信息
输入make test16
和make rtest16
查看运行结果
tsh实验现象和tshref一致,结果正确
十六关全部测试正确。
/*
* tsh - A tiny shell program with job control
*
*
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* fdddd */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/
/* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
struct job_t { /* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
/* End global variables */
/* Function prototypes */
/* Here are the functions that you will implement */
void eval(char *cmdline);
int builtin_cmd(char **argv);
void do_bgfg(char **argv);
void waitfg(pid_t pid);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv);
void sigquit_handler(int sig);
void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs);
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *jobs, pid_t pid);
pid_t fgpid(struct job_t *jobs);
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);
struct job_t *getjobjid(struct job_t *jobs, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t *jobs);
void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
/*
* main - The shell's main routine
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);
/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
/* Install the signal handlers */
/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);
/* Initialize the job list */
initjobs(jobs);
/* Execute the shell's read/eval loop */
while (1) {
/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}
exit(0); /* control never reaches here */
}
/*
* eval - Evaluate the command line that the user has just typed in
*
* If the user has requested a built-in command (quit, jobs, bg or fg)
* then execute it immediately. Otherwise, fork a child process and
* run the job in the context of the child. If the job is running in
* the foreground, wait for it to terminate and then return. Note:
* each child process must have a unique process group ID so that our
* background children don't receive SIGINT (SIGTSTP) from the kernel
* when we type ctrl-c (ctrl-z) at the keyboard.
*/
void eval(char *cmdline)
{
char* argv[MAXARGS]; //execve()函数的参数
int state = UNDEF; //工作状态,FG或BG
sigset_t set;
pid_t pid; //进程id
// 处理输入的数据
if(parseline(cmdline, argv) == 1) //解析命令行,返回给argv数组
state = BG;
else
state = FG;
if(argv[0] == NULL) //命令行为空直接返回
return;
// 如果不是内置命令
if(!builtin_cmd(argv))
{
if(sigemptyset(&set) < 0)
unix_error("sigemptyset error");
if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)
unix_error("sigaddset error");
//在它派生子进程之前阻塞SIGCHLD信号,防止竞争
if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if((pid = fork()) < 0) //fork创建子进程失败
unix_error("fork error");
else if(pid == 0) //fork创建子进程
{
// 子进程的控制流开始
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) //解除阻塞
unix_error("sigprocmask error");
if(setpgid(0, 0) < 0) //设置子进程id
unix_error("setpgid error");
if(execve(argv[0], argv, environ) < 0){
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// 将当前进程添加进job中,无论是前台进程还是后台进程
addjob(jobs, pid, state, cmdline);
// 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// 判断子进程类型并做处理
if(state == FG)
waitfg(pid); //前台作业等待
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); //将进程id映射到job id
}
return;
}
/*
* parseline - Parse the command line and build the argv array.
*
* Characters enclosed in single quotes are treated as a single
* argument. Return true if the user has requested a BG job, false if
* the user has requested a FG job.
*/
int parseline(const char *cmdline, char **argv)
{
static char array[MAXLINE]; /* holds local copy of command line */
char *buf = array; /* ptr that traverses command line */
char *delim; /* points to first space delimiter */
int argc; /* number of args */
int bg; /* background job? */
strcpy(buf, cmdline);
buf[strlen(buf)-1] = ' '; /* replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* ignore leading spaces */
buf++;
/* Build the argv list */
argc = 0;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
while (delim) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* ignore spaces */
buf++;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
}
argv[argc] = NULL;
if (argc == 0) /* ignore blank line */
return 1;
/* should the job run in the background? */
if ((bg = (*argv[argc-1] == '&')) != 0) {
argv[--argc] = NULL;
}
return bg;
}
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0], "quit")) //如果命令是quit,退出
exit(0);
else if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) //如果是bg或者fg命令,执行do_fgbg函数
do_bgfg(argv);
else if(!strcmp(argv[0], "jobs")) //如果命令是jobs,列出正在运行和停止的后台作业
listjobs(jobs);
else
return 0; /* not a builtin command */
return 1;
}
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// 没有参数的fg/bg应该被丢弃
if(!argv[1]){ //命令行为空
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return ;
}
// 检测fg/bg参数,其中%开头的数字是JobID,纯数字的是PID
if(argv[1][0] == '%'){ //解析jid
if((num = strtol(&argv[1][1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobjid(jobs, num)) == NULL){
printf("%%%d: No such job\n", num); //没找到对应的job
return;
}
} else {
if((num = strtol(argv[1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobpid(jobs, num)) == NULL){
printf("(%d): No such process\n", num); //没找到对应的进程
return;
}
}
if(!strcmp(argv[0], "bg")){
// bg会启动子进程,并将其放置于后台执行
job->state = BG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else if(!strcmp(argv[0], "fg")) {
job->state = FG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
// 当一个进程被设置为前台执行时,当前tsh应该等待该子进程结束
waitfg(job->pid);
} else {
puts("do_bgfg: Internal error");
exit(0);
}
return;
}
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
struct job_t *job = getjobpid(jobs, pid);
if(!job) return;
// 如果当前子进程的状态没有发生改变,则tsh继续休眠
while(job->state == FG)
// 使用sleep的这段代码会比较慢,最好使用sigsuspend
sleep(1);
return;
}
/*****************
* Signal handlers
*****************/
/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if(verbose)
puts("sigchld_handler: entering");
/*
以非阻塞方式等待所有子进程
waitpid 参数3:
1. 0 : 执行waitpid时, 只有在子进程 **终止** 时才会返回。
2. WNOHANG : 若子进程仍然在运行,则返回0 。
注意只有设置了这个标志,waitpid才有可能返回0
3. WUNTRACED : 如果子进程由于传递信号而停止,则马上返回。
只有设置了这个标志,waitpid返回时,其WIFSTOPPED(status)才有可能返回true
*/
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// 如果当前这个子进程的job已经删除了,则表示有错误发生
if((job = getjobpid(jobs, pid)) == NULL){
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
//接下来判断三种状态
// 如果这个子进程收到了一个暂停信号(还没退出)
if(WIFSTOPPED(status)){
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
job->state = ST; //状态设为挂起
}
// 如果子进程通过调用 exit 或者一个返回 (return) 正常终止
else if(WIFEXITED(status)){
if(deletejob(jobs, pid))
if(verbose){
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
printf("sigchld_handler: Job [%d] (%d) terminates OK (status %d)\n", jid, pid, WEXITSTATUS(status));
}
}
// 如果子进程是因为一个未被捕获的信号终止的,例如SIGKILL
else {
if(deletejob(jobs, pid)){ //清除进程
if(verbose)
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
}
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status)); //返回导致子进程终止的信号的数量
}
}
if(verbose)
puts("sigchld_handler: exiting");
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)
{
if(verbose)
puts("sigint_handler: entering");
pid_t pid = fgpid(jobs);
if(pid){
// 发送SIGINT给前台进程组里的所有进程
// 需要注意的是,前台进程组内的进程除了当前前台进程以外,还包括前台进程的子进程。
// 最多只能存在一个前台进程,但前台进程组内可以存在多个进程
if(kill(-pid, SIGINT) < 0)
unix_error("kill (sigint) error");
if(verbose){
printf("sigint_handler: Job (%d) killed\n", pid);
}
}
if(verbose)
puts("sigint_handler: exiting");
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)
{
if(verbose)
puts("sigstp_handler: entering");
pid_t pid = fgpid(jobs);
struct job_t *job = getjobpid(jobs, pid);
if(pid){
if(kill(-pid, SIGTSTP) < 0)
unix_error("kill (tstp) error");
if(verbose){
printf("sigstp_handler: Job [%d] (%d) stopped\n", job->jid, pid);
}
}
if(verbose)
puts("sigstp_handler: exiting");
return;
}
/*********************
* End signal handlers
*********************/
/***********************************************
* Helper routines that manipulate the job list
**********************************************/
/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job) {
job->pid = 0;
job->jid = 0;
job->state = UNDEF;
job->cmdline[0] = '\0';
}
/* initjobs - Initialize the job list */
void initjobs(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
clearjob(&jobs[i]);
}
/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *jobs)
{
int i, max=0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid > max)
max = jobs[i].jid;
return max;
}
/* addjob - Add a job to the job list */
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == 0) {
jobs[i].pid = pid;
jobs[i].state = state;
jobs[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = 1;
strcpy(jobs[i].cmdline, cmdline);
if(verbose){
printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
}
return 1;
}
}
printf("Tried to create too many jobs\n");
return 0;
}
/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *jobs, pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == pid) {
clearjob(&jobs[i]);
nextjid = maxjid(jobs)+1;
return 1;
}
}
return 0;
}
/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].state == FG)
return jobs[i].pid;
return 0;
}
/* getjobpid - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *jobs, pid_t pid) {
int i;
if (pid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid)
return &jobs[i];
return NULL;
}
/* getjobjid - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *jobs, int jid)
{
int i;
if (jid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid == jid)
return &jobs[i];
return NULL;
}
/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid) {
return jobs[i].jid;
}
return 0;
}
/* listjobs - Print the job list */
void listjobs(struct job_t *jobs)
{
int i;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid != 0) {
printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
switch (jobs[i].state) {
case BG:
printf("Running ");
break;
case FG:
printf("Foreground ");
break;
case ST:
printf("Stopped ");
break;
default:
printf("listjobs: Internal error: job[%d].state=%d ",
i, jobs[i].state);
}
printf("%s", jobs[i].cmdline);
}
}
}
/******************************
* end job list helper routines
******************************/
/***********************
* Other helper routines
***********************/
/*
* usage - print a help message
*/
void usage(void)
{
printf("Usage: shell [-hvp]\n");
printf(" -h print this message\n");
printf(" -v print additional diagnostic information\n");
printf(" -p do not emit a command prompt\n");
exit(1);
}
/*
* unix_error - unix-style error routine
*/
void unix_error(char *msg)
{
fprintf(stdout, "%s: %s\n", msg, strerror(errno));
exit(1);
}
/*
* app_error - application-style error routine
*/
void app_error(char *msg)
{
fprintf(stdout, "%s\n", msg);
exit(1);
}
/*
* Signal - wrapper for the sigaction function
*/
handler_t *Signal(int signum, handler_t *handler)
{
struct sigaction action, old_action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */
if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}
/*
* sigquit_handler - The driver program can gracefully terminate the
* child shell by sending it a SIGQUIT signal.
*/
void sigquit_handler(int sig)
{
printf("Terminating after receipt of SIGQUIT signal\n");
exit(1);
}