CSAPP---------------shell lab

这次的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       (关于这几个命令是什么意思,可以看一下指导书)
  1. 如果是前台进程,那么就要注意了,首先是需要fork一个子进程给这个前台进程去运行,然后需要把此job加入到终端的job队列中,此时需要注意,当进入到子进程的时候,就需要设置setpgid(0,0);因为如果之后想发送sig给前台进程,由于它是由tsh这个shell创建的,所以属于同一个进程组,而你的shell是由unix shell创建的,所以都属于前台进程,所以当你发送sigint,默认情况是发送给每一个前台进程,我们这里需要保证发送信号到的那个进程是前台进程,但这里有时创建的进程是后台进程
  2. 还有就是shell需要把此job加入到job队列中,但是父进程有可能在加入job队列之前就会回收子进程,那么加入的进程就会不存在,那么就会存在问题,于是就需要阻塞sigchild信号,使得在加入job队列之后再回收子进程
  3. 当你使用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));
                }

            }
        }
        
    }
     
}

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值