前言:强烈建议先看完csapp第八章再做此实验,完整的tsh.c代码贴在文章末尾了
1.准备知识- 进程的概念、状态以及控制进程的几个函数(fork,waitpid,execve)。
- 信号的概念,会编写正确安全的信号处理程序。
- shell的概念,理解shell程序是如何利用进程管理和信号去执行一个命令行语句。
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) 信号3.实验环境
ubuntu12.04 (32位)环境
4.实验内容及操作步骤通过阅读实验指导书我们知道此实验要求我们完成tsh.c中的七个函数从而实现一个简单的shell,能够处理前后台运行程序、能够处理ctrl+z、ctrl+c等信号。
首先我们来看一下tsh.c具体内容。
首先定义了一些宏
#define MAXLINE 1024 #define MAXARGS 128 #define MAXJOBS 16 #define MAXJID 1<<16
定义了四种进程状态
#define UNDEF 0 #define FG 1 #define BG 2 #define ST 3
然后定义了job_t的任务的类,并且创建了jobs[]数组
struct job_t {
pid_t pid;
int jid;
int state;
char cmdline[MAXLINE];
};
struct job_t jobs[MAXJOBS];
接着是需要我们完成的七个函数定义
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函数进行解析。我们需要做的就是逐步完善这个过程
接下来开始实验:
- 使用make命令编译tsh.c文件(文件有所改变的话需要先使用make clean指令清空)
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函数用于解析和解释命令行。eval首先解析命令行,如果用户请求一个内置命令quit、jobs、bg或fg(即内置命令)那么就立即执行。否则,fork子进程和在子进程的上下文中运行作业。如果作业正在运行前台,等待它终止,然后返回。
函数原型:void eval(char *cmdline),传入的参数为cmdline,即命令行字符串
实现思路:仿照书上的eval函数写法和所需的功能来完成函数
- 首先调用parseline函数解析命令行,如果为空直接返回,接着使用builtin_cmd函数判断是否为内置命令,返回0说明不是内置命令,如果是内置命令直接执行。
- 如果不是内置命令,那么先阻塞信号(具体在第四点分析),再调用fork创建子进程。在子进程中,首先解除阻塞,设置自己的id号,然后调用execve函数来执行job。
- 父进程判断作业是否后台运行,是的话调用addjob函数将子进程job加入job链表中,解除阻塞,然后调用waifg函数等待前台运行完成。如果不在后台工作则打印进程组jid和子进程pid以及命令行字符串。
- 因为子进程继承了他们父进程的阻塞向量,所以在执行新程序之前,子程序必须确保解除对SIGCHLD信号的阻塞。父进程必须使用sigprocmask在它派生子进程之前也就是调用fork函数之前阻塞SIGCHLD信号,之后解除阻塞;在通过调用addjob将子进程添加到作业列表之后,再次使用sigprocmask,解除阻塞。
完整代码:
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 foundn", 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;
}
注意:
- 每个子进程必须有自己独一无二的进程组id,通过在fork()之后的子进程中Setpgid(0,0)实现,这样当向前台程序发送ctrl+c或ctrl+z命令时,才不会影响到后台程序。如果没有这一步,则所有的子进程与当前的tsh shell进程为同一个进程组,发送信号时,前后台的子进程均会收到。
- 在fork()新进程前要阻塞SIGCHLD信号,防止出现竞争,这是经典的同步错误,如果不阻塞会出现子进程先结束从jobs中删除,然后再执行到主进程addjob的竞争问题。
函数功能:识别并执行内置命令: 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;
return 1;
}
do_bgfg函数
函数功能:实现内置命令bg 和 fg
首先要明确的是bg和bg的作用
bg
:将停止的后台作业更改为正在运行的后台作业。通过发送SIGCONT信号重新启动 ,然后在后台运行它。 参数可以是PID,也可以是JID。ST -> BG fg
:将已停止或正在运行的后台作业更改为前台正在运行的作业。通过发送SIGCONT信号重新启 ,然后在前台运行它。 参数可以是PID,也可以是JID。ST -> FG,BG -> FG
函数原型:void do_bgfg(char **argv),参数为argv 参数列表
实现思路:
- 判断argv[]是否带%,若为整数则传入pid,若带%则传入jid。接着调用getjobjid函数来获得对应的job结构体,如果返回为空,说明列表中并不存在jid的job,要输出提示。
- 使用strcmp函数判断是bg命令还是fg命令
- 若是bg,使目标进程重新开始工作,设置状态为BG(后台),打印进程信息
- 若是fg,使目标进程重新开始工作,设置状态为FG(前台),等待进程结束
完整代码:
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// 没有参数的fg/bg应该被丢弃
if(!argv[1]){ //命令行为空
printf("%s command requires PID or %%jobid argumentn", 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 %%jobidn",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobjid(jobs, num)) == NULL){
printf("%%%d: No such jobn", num); //没找到对应的job
return;
}
} else {
if((num = strtol(argv[1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobidn",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobpid(jobs, num)) == NULL){
printf("(%d): No such processn", 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()函数
函数功能:等待一个前台作业结束,或者说是阻塞一个前台的进程直到这个进程变为后台进程
函数原型: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;
}
sigchld_handler函数
函数功能:处理SIGCHILD信号
函数原型:void sigchld_handler(int sig),参数为信号类型
首先了解一下父进程回收子进程的过程:当一个子进程终止或者停止时,内核会发送一个SIGCHLD信号给父进程。因此父进程必须回收子进程,以避免在系统中留下僵死进程。父进程捕获这个SIGCHLD信号,回收一个子进程。一个进程可以通过调用 waitpid 函数来等待它的子进程终止或者停止。如果回收成功,则返回为子进程的 PID, 如果 WNOHANG, 则返回为 0, 如果其他错误,则为 -1。
实现思路:
- 用while循环调用waitpid直到它所有的子进程终止。
- 检查己回收子进程的退出状态
- WIFSTOPPED::引起返回的子进程当前是被停止的
- WIFSIGNALED:子进程是因为一个未被捕获的信号终止
- WIFEXITED:子进程通过调用exit 或者return正常终止
- 然后分别用WSTOPSIG,WTERMSIG,WEXITSTATUS提取以上三个退出状态。注意如果引起返回的子进程当前是被停止的进程,那么要将其状态设置为ST
完整代码:
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if(verbose)
puts("sigchld_handler: entering");
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 %dn", 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) deletedn", 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) deletedn", jid, pid);
}
printf("Job [%d] (%d) terminated by signal %dn", jid, pid, WTERMSIG(status)); //返回导致子进程终止的信号的数量
}
}
if(verbose)
puts("sigchld_handler: exiting");
return;
}
sigint_handler函数
函数功能:捕获SIGINT信号
函数原型:void sigchld_handler(int sig),参数为信号类型
实现思路:
- 调用函数fgpid返回前台进程pid
- 如果当前进程pid不为0,那么调用kill函数发送SIGINT信号给前台进程组
- 在2中调用kill函数如果返回值为-1,如果返回值为-1表示进程不存在。输出error
完整代码:
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) killedn", pid);
}
}
if(verbose)
puts("sigint_handler: exiting");
return;
}
sigtstp_handler函数
函数功能:同sigint_handler差不多,捕获SIGTSTP信号
函数原型:void sigtstp_handler(int sig) ,参数为信号类型
首先了解一下SIGTSTP的作用:SIGTSPT信号默认行为是停止直到下一个 SIGCONT,是来自终端的停止信号,在键盘上输入 CTR+Z会导致一个 SIGTSPT信号被发送到外壳。外壳捕获该信号,然后发送SIGTSPT信号到这个前台进程组中的每个进程。在默认情况下,结果是停止或挂起前台作业。
实现思路:
- 用fgpid(jobs)获取前台进程pid,判断当前是否有前台进程,如果没有直接返回。
- 用kill(-pid,sig)函数发送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) stoppedn", job->jid, pid);
}
}
if(verbose)
puts("sigstp_handler: exiting");
return;
}
注意:使用kill函数,如果 pid 小于零才会发送信号sig 给进程组中的每个进程,因此这里使用-pid。
至此tsh.c文件完成。
接下来我们分析以下测试文件的内容并看下我们的函数能否完成测试
首先了解一下一些测试文件的符号和命令的定义:
符号:
- 空格:分隔指令作用
- &:如果命令以&结尾,表示标该作业在后台运行
- #:直接打印#后一行的文本内容
- %:后接一个整数,表示job的ID号。
命令:
- jobs: 列出正在运行和停止的后台作业
- bg
: 将停止的后台作业更改为正在运行的后台作业 - fg
:将已停止或正在运行的后台作业更改为前台正在运行的作业 - kill : 终止一个作业
用户程序:
- myint程序:函数睡眠,使程序睡眠n秒,运行结束后不会自动退出,并会检测系统错误;
- myspin程序:函数睡眠,使程序睡眠n秒,在睡眠结束后就自动退出,不检测系统错误;
- mysplit程序:函数睡眠,使程序睡眠n秒,创建一个子进程进行睡眠,然后父进程等待子进程正常睡眠n秒后,继续运行;
- mystop程序:让进程暂定n秒,并发送信号。
首先打开 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可以理解为将其后面的内容当作字符串输出
所以第三关的任务是:
- 首先是/bin/echo tsh> quit 意思是打开 bin 目录下的 echo 可执行文件,在 foregound 开启一个子进程运行它(因为末尾没有&符号,如果有,就是在 backgound 运行)
- 运行 echo 这个进程的过程中,通过 tsh>quit 命令,调用 tsh并执行内置命令 quit,退出 echo 这个子进程
- 最后在 tsh 中执行内置命令 quit,退出 tsh 进程,回到我们的终端。
输入make test03和make rtest03查看运行结果
tsh实验现象和tshref一致,结果正确
第四关打开 trace04.txt查看文件内容
# # trace04.txt - Run a background job. # /bin/echo -e tsh> ./myspin 1 46 ./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 46 ./myspin 2 & /bin/echo -e tsh> ./myspin 3 46 ./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 46 ./myspin 4 & /bin/echo -e tsh> ./myspin 5 ./myspin 5 SLEEP 2 INT /bin/echo tsh> jobs jobs
第七关是检测你之前是否是正确完成了所有的函数,有没有投机取巧
输入make test07和make rtest07查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace08.txt查看文件内容
# # trace08.txt - Forward SIGTSTP only to foreground job. # /bin/echo -e tsh> ./myspin 4 46 ./myspin 4 & /bin/echo -e tsh> ./myspin 5 ./myspin 5 SLEEP 2 TSTP /bin/echo tsh> jobs jobs
当接收到了TSTP中断信号(即CTRL_Z),将前台进程挂起,然后输出被挂起的任务
输入make test08和make rtest08查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace09.txt查看文件内容
# # trace09.txt - Process bg builtin command # /bin/echo -e tsh> ./myspin 4 46 ./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
将一个挂起的程序转到后台继续执行
输入make test09和make rtest09查看运行结果
tsh实验现象和tshref一致,结果正确
打开 trace10.txt查看文件内容
# # trace10.txt - Process fg builtin command. # /bin/echo -e tsh> ./myspin 4 46 ./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
fg命令是将一个挂起的程序转到前台继续执行,或者将一个后台执行的程序转到前台执行
输入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
ps –a 显示所有进程
输入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
输入make test12和make rtest12查看运行结果
tsh实验现象和tshref一致,结果正确
打开 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
输入make test13和make rtest13查看运行结果
tsh实验现象和tshref一致,结果正确
第十四关打开 trace14.txt查看文件内容
# # trace14.txt - Simple error handling # /bin/echo tsh> ./bogus ./bogus /bin/echo -e tsh> ./myspin 4 46 ./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
本关主要是测试所有的命令,判断是否正确。
输入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 46 ./myspin 3 & /bin/echo -e tsh> ./myspin 4 46 ./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
输入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
输入make test16和make rtest16查看运行结果
tsh实验现象和tshref一致,结果正确
十六关全部测试正确。
完整的tsh.c代码如下:#include#include #include #include #include #include #include #include #include #define MAXLINE 1024 #define MAXARGS 128 #define MAXJOBS 16 #define MAXJID 1<<16 #define UNDEF 0 #define FG 1 #define BG 2 #define ST 3 extern char **environ; char prompt[] = "tsh> "; int verbose = 0; int nextjid = 1; char sbuf[MAXLINE]; struct job_t { pid_t pid; int jid; int state; char cmdline[MAXLINE]; }; struct job_t jobs[MAXJOBS]; 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); 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); int main(int argc, char **argv) { char c; char cmdline[MAXLINE]; int emit_prompt = 1; dup2(1, 2); while ((c = getopt(argc, argv, "hvp")) != EOF) { switch (c) { case 'h': usage(); break; case 'v': verbose = 1; break; case 'p': emit_prompt = 0; break; default: usage(); } } Signal(SIGINT, sigint_handler); Signal(SIGTSTP, sigtstp_handler); Signal(SIGCHLD, sigchld_handler); Signal(SIGQUIT, sigquit_handler); initjobs(jobs); while (1) { if (emit_prompt) { printf("%s", prompt); fflush(stdout); } if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin)) app_error("fgets error"); if (feof(stdin)) { fflush(stdout); exit(0); } eval(cmdline); fflush(stdout); fflush(stdout); } exit(0); } 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 foundn", 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; } int parseline(const char *cmdline, char **argv) { static char array[MAXLINE]; char *buf = array; char *delim; int argc; int bg; strcpy(buf, cmdline); buf[strlen(buf)-1] = ' '; while (*buf && (*buf == ' ')) buf++; argc = 0; if (*buf == ''') { buf++; delim = strchr(buf, '''); } else { delim = strchr(buf, ' '); } while (delim) { argv[argc++] = buf; *delim = ' '; buf = delim + 1; while (*buf && (*buf == ' ')) buf++; if (*buf == ''') { buf++; delim = strchr(buf, '''); } else { delim = strchr(buf, ' '); } } argv[argc] = NULL; if (argc == 0) return 1; if ((bg = (*argv[argc-1] == '&')) != 0) { argv[--argc] = NULL; } return bg; } 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; return 1; } void do_bgfg(char **argv) { int num; struct job_t *job; // 没有参数的fg/bg应该被丢弃 if(!argv[1]){ //命令行为空 printf("%s command requires PID or %%jobid argumentn", 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 %%jobidn",argv[0]);//失败,打印错误消息 return; } if((job = getjobjid(jobs, num)) == NULL){ printf("%%%d: No such jobn", num); //没找到对应的job return; } } else { if((num = strtol(argv[1], NULL, 10)) <= 0){ printf("%s: argument must be a PID or %%jobidn",argv[0]);//失败,打印错误消息 return; } if((job = getjobpid(jobs, num)) == NULL){ printf("(%d): No such processn", 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) { struct job_t *job = getjobpid(jobs, pid); if(!job) return; // 如果当前子进程的状态没有发生改变,则tsh继续休眠 while(job->state == FG) // 使用sleep的这段代码会比较慢,最好使用sigsuspend sleep(1); return; } void sigchld_handler(int sig) { int status, jid; pid_t pid; struct job_t *job; if(verbose) puts("sigchld_handler: entering"); 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 %dn", 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) deletedn", 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) deletedn", jid, pid); } printf("Job [%d] (%d) terminated by signal %dn", jid, pid, WTERMSIG(status)); //返回导致子进程终止的信号的数量 } } if(verbose) puts("sigchld_handler: exiting"); return; } 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) killedn", pid); } } if(verbose) puts("sigint_handler: exiting"); return; } 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) stoppedn", job->jid, pid); } } if(verbose) puts("sigstp_handler: exiting"); return; } void clearjob(struct job_t *job) { job->pid = 0; job->jid = 0; job->state = UNDEF; job->cmdline[0] = ' '; } void initjobs(struct job_t *jobs) { int i; for (i = 0; i < MAXJOBS; i++) clearjob(&jobs[i]); } 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; } 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 %sn", jobs[i].jid, jobs[i].pid, jobs[i].cmdline); } return 1; } } printf("Tried to create too many jobsn"); return 0; } 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; } 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; } 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; } 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; } 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; } 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); } } } void usage(void) { printf("Usage: shell [-hvp]n"); printf(" -h print this messagen"); printf(" -v print additional diagnostic informationn"); printf(" -p do not emit a command promptn"); exit(1); } void unix_error(char *msg) { fprintf(stdout, "%s: %sn", msg, strerror(errno)); exit(1); } void app_error(char *msg) { fprintf(stdout, "%sn", msg); exit(1); } handler_t *Signal(int signum, handler_t *handler) { struct sigaction action, old_action; action.sa_handler = handler; sigemptyset(&action.sa_mask); action.sa_flags = SA_RESTART; if (sigaction(signum, &action, &old_action) < 0) unix_error("Signal error"); return (old_action.sa_handler); } void sigquit_handler(int sig) { printf("Terminating after receipt of SIGQUIT signaln"); exit(1); }



