这次的shell-lab还是挺练习关于信号的使用,gdb调试带有信号的程序,以及当一个信号输入到终端时,接下来的逻辑问题
做这个实验的时候我很多都参考了这位老哥的实验,刚开始并不理解逻辑问题,于是在一次次的测试过程中慢慢理解整个的逻辑问题,得出结论,要想做好这个实验,就必须先把深入理解计算机系统的第8章异常控制流的大部分都看懂,里面涉及了很多这次shell的逻辑,所以做这个实验的时候,一定要把第8章细读一遍,然后就是实验指导书
材料:
实验指导书:http://csapp.cs.cmu.edu/3e/shlab.pdf
实验下载地址:http://csapp.cs.cmu.edu/3e/labs.html
这次实验需要完成以下这几个函数:
首先是主函数里模拟的就是shell终端;
- 当有信号来临时,那么它就需要对其进行捕捉,然后进行处理,所以在main函数的开始需要几个signal handle 函数
- 然后就是一个while循环,由于终端是一直在那里等待你输入命令去执行,所以需要一个一直循环的过程
- 当fgets收到终端输入的命令cmdline,接下来就有一个eval(cmdline)函数进行处理
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);
}
eval函数:
这个eval函数,主要是对接收到的cmdline进行处理,会有下面几种情况:
- 前台进程
- 后台进程
- 内建命令:fg,bg,quit,jobs (关于这几个命令是什么意思,可以看一下指导书)
- 如果是前台进程,那么就要注意了,首先是需要fork一个子进程给这个前台进程去运行,然后需要把此job加入到终端的job队列中,此时需要注意,当进入到子进程的时候,就需要设置setpgid(0,0);因为如果之后想发送sig给前台进程,由于它是由tsh这个shell创建的,所以属于同一个进程组,而你的shell是由unix shell创建的,所以都属于前台进程,所以当你发送sigint,默认情况是发送给每一个前台进程,我们这里需要保证发送信号到的那个进程是前台进程,但这里有时创建的进程是后台进程
- 还有就是shell需要把此job加入到job队列中,但是父进程有可能在加入job队列之前就会回收子进程,那么加入的进程就会不存在,那么就会存在问题,于是就需要阻塞sigchild信号,使得在加入job队列之后再回收子进程
- 当你使用unix shell的时候,就会发现当你执行一个前台进程的时候,它会等待你执行完这个进程才会返回到shell输入的格式,所以这里就需要处理sigchild信号,使得如果是前台进程,那么就要等待该进程回收,再进入到下一次while循环
void eval(char *cmdline)
{
char *argv[MAXARGS]; //argument list execve()
char buf[MAXLINE]; //modified command line
int bg; //decides whether the job runs in bg or fg
pid_t pid; //process id
int status;
strcpy(buf,cmdline);
bg = parseline(buf,argv);
if(bg)
status=2;
else
status=1;
if(argv[0]==NULL){ //empty line case
return;
}
/* Install the block signal sets */
sigset_t mask_all,mask_one,prev_one;
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one,SIGCHLD);
if(!builtin_cmd(argv)){ //non-builtin command case
//阻塞sigchild,为了防止在add_job之前delete_job
sigprocmask(SIG_BLOCK,&mask_one,&prev_one);
if((pid = fork())==0){ //Child runs user job
setpgid(0,0); //修改进程组
sigprocmask(SIG_SETMASK,&prev_one,NULL);
if(execve(argv[0],argv,environ)<0){
printf("Command Invalid\n"); //when input an invalid command
exit(0);
}
exit(0);
}
//阻塞一切,为了加入此job
sigprocmask(SIG_BLOCK,&mask_all,NULL);
addjob(jobs,pid,status,cmdline);
sigprocmask(SIG_SETMASK,&mask_one,NULL); //阻塞sigchild
//Parent waits for foreground job to terminate
if(!bg){ //child process
//是前台进程才会修改那个原子p_id
waitfg(pid); //前台进程要等回收结束才会让下一个命令输入
}else{
printf("[%d] %s",pid,cmdline);
//后台进程不需要等,只打印命令,然后前台继续执行
}
sigprocmask(SIG_SETMASK,&prev_one,NULL);
}
return;
}
sigchild函数
这个sigchild很重要,之前只是简单看了一下那位老哥的思路,于是自己凭着自己的理解去实现,但是在逐个调试的时候发现了很多问题,然后就开始细想,理解那位老哥为啥要这么写,以及当一个信号来临时,shell应该怎么处理,所以,不要先急着写,而需要先搞清楚这里面的逻辑,
- 这里之前我有个疑问,就是我再测试的时候发现,我每次发送一个sigint信号,结果却来了好几个,我百思不得其解,于是写了个小程序测试了一下,它应该是接连发送很多个,直到收到为止,那其他的就丢弃了,(当然我的解释可能不正确)
- 这里把回收函数waitpid写在这里很关键,就是不管你发送什么信号,它都能捕捉到,然后进行下一步处理
- 这里的终端shell管理的job队列就很重要了,因为你子进程是setpid(0,0),自己和自己一组,于是你只能通过job队列找到前台进程,然后判断发送过来的进程是不是前台进程,然后根据信号进行分别处理
void sigchld_handler(int sig)
{
int old_errno=errno;
sigset_t mask_all,prev_all;
struct job_t *jb;
pid_t pid;
int status;
sigfillset(&mask_all);
//设置不阻塞
while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)
{
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
if(pid == fgpid(jobs)){
p_id = 1;
}
jb=getjobpid(jobs,pid);
if(WIFSTOPPED(status)){
//子进程停止引起的waitpid函数返回
jb->state = ST;
printf("Job [%d] (%d) stop by signal %d\n", jb->jid, jb->pid, WSTOPSIG(status));
}else {
if(WIFSIGNALED(status)){
//子进程终止引起的返回,这里主要是sigint的信号过来的
printf("Job [%d] (%d) terminated by signal %d\n", jb->jid, jb->pid, WTERMSIG(status));
}
//只有除了sigstop之外的信号,有sigint和正常的sigchild都需要删除job
deletejob(jobs,pid);
}
//不能在这里删除job,因为sigstop的信号也会进来,虽然我也不知道为啥
//deletejob(jobs,pid); //此时这个这个子进程被回收了
//可以让shell终端开始下一次命令的输入了
sigprocmask(SIG_SETMASK,&prev_all,NULL);
}
errno=old_errno;
}
这个全局变量很重要,书中有详细介绍,等待前台进程结束就靠它了(p546)
volatile sig_atomic_t p_id;
waitfg函数
当我们fork子进程的时候,肯定是先运行父进程,这个父进程就是一个shell,当运行的是前台进程,那么肯定是要等待前台进程结束的这个时候waitfg就很重要(书中也有介绍)
这里的waitfg会一直等待p_id=1,才会结束循环,否则sigsuspend会pause等待,当有信号来就会醒来,然后继续循环,当p_id=1,表明前台进程或者后台进程被终止,那么此时waitfg就会结束,shell就会进行下一次命令
void waitfg(pid_t pid)
{
sigset_t mask;
sigemptyset(&mask);
p_id=0;
//若未错误,应该是没有阻塞sigchild的信号集
while (p_id==0){
sigsuspend(&mask); //此时挂起该进程,然后等收到sigchild信号时,再恢复原先的信号集
}
//这一步是为了等回收刚刚处理完毕的前台进程,然后释放出前台shell,让用户输入下一个命令
return;
}
do_bgfg函数
- 当输入的是后台命令,那么就会执行此函数
- 首先判断输入的是jobid还是processid,然后执行相应的逻辑就行了,注释里都有解释
void do_bgfg(char **argv)
{
char *Jid;
int id=0;
struct job_t *jb;
pid_t jpid;
sigset_t mask_all,prev_,mask_one;
sigfillset(&mask_all);
sigaddset(&mask_one,SIGCHLD);
if(!argv[1]){
printf("please input Jid or Pid\n");
return;
}
Jid=argv[1];
sigprocmask(SIG_BLOCK,&mask_all,&prev_);
if(*Jid=='%'){
id=atoi(++Jid);
printf("%d",id);
jb=getjobjid(jobs,id); //输入的id是jobid
if(!jb)
{
printf("No this process!\n");
sigprocmask(SIG_SETMASK,&prev_,NULL);
return;
}
jpid=jb->pid;
//除了是ST之外,是BG,则啥也不干
}
else {
jpid=atoi(Jid);
jb=getjobpid(jobs,jpid);
if(!jb)
{
printf("No this process!\n");
sigprocmask(SIG_SETMASK,&prev_,NULL);
return;
}
}
if(!strcmp(argv[0],"bg")){// Change a stopped background job to a running background job.
switch (jb->state)
{
case BG:
/* 啥也不干 */
break;
case ST:
//接下来是给停止的这个信号发送继续的信号,阻塞信号集,防止此时退出终端
jb->state=BG;
kill(-jpid,SIGCONT);
printf("[%d] (%d) %s", jb->jid, jb->pid, jb->cmdline);
break;
case UNDEF:
case FG:
unix_error("bg 出现undef或者FG的进程\n");
break;
default:
break;
}
}
else if(!strcmp(argv[0],"fg")){ //Change a stopped or running background job to a running in the foreground
switch (jb->state)
{
case FG:
case UNDEF:
unix_error("fg 出现undef或者FG的进程\n");
break;
default :
sigprocmask(SIG_BLOCK,&mask_all,&prev_);
if(jb->state==ST) //if stopped ,change to run
kill(-jpid,SIGCONT);
jb->state=FG;
sigprocmask(SIG_SETMASK,&mask_one,NULL);
waitfg(jpid); //此时是前台进程就必须等待此进程被回收结束
break;
}
}
sigprocmask(SIG_SETMASK,&prev_,NULL);
return;
}
sigint_handle函数
这个函数就是执行发送SIGINT信号给前台进程
void sigint_handler(int sig)
{
//杀死前台进程,此时必须向前台进程发送死亡信号,并且需要删除job
int olderrno = errno;
sigset_t mask_all,prev_all;
pid_t pid;
struct job_t *jb;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
pid=fgpid(jobs);
if(pid){
//jb=pid2jid(pid);
//deletejob(jobs,pid); //此时这个这个子进程被回收了
//当给这个进程发sigint信号时,那么就相当于杀死这个进程
//然后父进程就会收到signal child 然后回收子进程
//在child handle 函数中,有delete job的工作
kill(-pid,SIGINT);
}
else{
printf("终止该进程前已经死了\n");
}
sigprocmask(SIG_SETMASK,&prev_all,NULL);
errno = olderrno;
return;
}
sigstop_handle函数
代码详情都在注释里了
void sigtstp_handler(int sig)
{
int olderrno = errno;
sigset_t mask_all,prev_all;
pid_t pid;
struct job_t *curr_job;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
pid=fgpid(jobs);
if(pid){
//curr_job=getjobpid(jobs,pid);
//curr_job->state=ST;
kill(-pid,SIGTSTP);
//这一步很关键,此时waitfg还在等待前台进程回收,但是此时已经有个stop信号过来了
//此时前台进程被终止,但是shell会接受新的命令
//因此要让waitfg结束
//printf("Job [%d] (%d) stopped by signal 20\n", curr_job->jid, curr_job->pid);
}
sigprocmask(SIG_SETMASK,&prev_all,NULL);
errno = olderrno;
return;
}
大体上,写一个简单的shell就是这个样子,不过还需要进行调试
这里当我刚开始测试的时候,发现程序莫名的停止了,于是我就使用gdb进行调试,这里记录一下
gdb调试+信号
当你在gdb下运行程序,输入信号时,gdb会捕捉该信号,并不会发送给你的程序,于是就要设置
handle signal nostop pass //nostop表示不要停止,pass就会不要捕捉
当你的程序突然中断 了,那么可以使用bt,查看程序都调用了那些函数,其实也就是查看栈层
总结:
这里还是挺锻炼对信号的运用,以及整个信号运行时的逻辑问题,需要在做之前考虑清楚;后期我打算能不能加上管道
完整的代码我放在github上:https://github.com/Yonhoo/CSAPP-shell-lab
这里是测试sigstop的程序,表明发送sigstop,waitpid也会收到;
#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>
volatile sig_atomic_t p_id;
void sig_child(int sig)
{
printf("i'm a child signal\n");
}
void sig_stop(int sig)
{
kill(-p_id,SIGSTOP);
printf("i'm a stop signal\n");
}
void sig_int(int sig){
kill(-p_id,SIGINT);
printf("i'm a int signal\n");
}
int main(int argc,char *argv[])
{
pid_t pid;
if((pid=fork())==0)
{
setpgid(0,0);
printf("c %d\n",getpid());
while (1)
{
printf("i'm a child\n");
sleep(1);
}
//execve("./myspin","./myspin 1",NULL);
}
else if(pid>0)
{
signal(SIGCHLD,sig_child);
signal(SIGTSTP,sig_stop);
signal(SIGINT,sig_int);
p_id=pid;
printf("p %d\n",getpid());
int status;
while((pid=waitpid(pid,&status,NULL))>0)
{
if(WIFSTOPPED(status)){
//进程停止引起的waitpid函数返回
printf(" (%d) stop by signal %d\n",pid, WSTOPSIG(status));
}else {
if(WIFSIGNALED(status)){
//子进程终止引起的返回,这里主要是sigint的信号过来的
printf(" (%d) terminated by signal %d\n", pid, WTERMSIG(status));
}
}
}
}
}