CSAPP: Shell Lab

原创 2016年07月16日 21:38:27

介绍

shell Lab的主要目的是为了让我们熟悉进程控制和信号。

实验准备

下载shell Lab实验包:原实验包github链接,本文中撰写的tsh.c全部代码github链接

【实验内容】
是对tsh.c中没有填写的函数进行填写,使得该shell能处理前后台运行程序、能够处理ctrl+z、ctrl+c等信号。
需要实现的函数主要有一下五个:

eval: 主要功能是解析cmdline,并且运行. [70 lines]
builtin cmd: 辨识和解析出bulidin命令: quit, fg, bg, and jobs. [25lines]
do bgfg: 实现bg和fg命令. [50 lines] 
waitfg: 实现等待前台程序运行结束. [20 lines]
sigchld handler: 响应SIGCHLD. 80 lines]
sigint handler: 响应 SIGINT (ctrl-c) 信号. [15 lines] 
sigtstp handler: 响应 SIGTSTP (ctrl-z) 幸好. [15 lines]

【实验结果的检验】
通过运行./tshref这个已经实现的shell将它的输出结果与我们自己实现的shell的结果进行比较

【注意】
有必要阅读《深入理解计算机系统 第二版》第8章异常控制流的所有内容。对于以下内容有比较好的了解

  • 实验中重要的函数:
void sigemptyset(sigset_t *mask);
void Sigaddset(sigset_t *mask,int sign);
void Sigprocmask(int how,sigset_t *mask,sigset_t *oldmask);
pid_t Fork(void);
void Execve(char *filename,char *argv[],char *envp[]);
void Setpgid(pid_t pid,pid_t gpid);
void Kill(pid_t pid,int sig);
  • 实验中最重要的eval()函数的原型可以在P503找到。

  • 实验中期望运用GDB来调试程序,然而当初次调试时发现里面并不包含符号表等为调试提供方便的内容。可以通过修改makefile来改变这一情况,修改makefile文件中的CFLAGS字段,添加-g 参数(为函数编译时添加必要的调试信息)。其中本来就存在的-02参数代表程序需要优化的级别,对于优化过的程序我们调试起来可能有些困惑,所以推荐移除,当然不移除问题不大。

在linux 64位机器上执行.tshref程序会有如下可能输出
unix > ./tshref: No such file or directory
file ./tshref文件可以看到是32位程序
通过sudo apt-get install ia32-libs解决该问题


实验

tsh.c的完整代码在github tsh.c链接

下面实现用用到的系统函数首字母为大写如Fock(),是我自己定义的错误分装函数,提高代码的简洁性。

/*error-handling wrapper funtion -by yzf*/
void Sigemptyset(sigset_t *mask);
void Sigaddset(sigset_t *mask,int sign);
void Sigprocmask(int how,sigset_t *mask,sigset_t *oldmask);
pid_t Fork(void);
void Setpgid(pid_t pid,pid_t gpid);
void Kill(pid_t pid,int sig);

1. eval()函数

该函数的主要功能是对用户输入的参数进行解析并运行计算。如果用户输入内建的命令行(quit,bg,fg,jobs)那么立即执行。
否则,fork一个新的子进程并且将该任务在子进程的上下文中运行。如果该任务是前台任务那么需要等到它运行结束才返回。

  1. 注意每个子进程必须用户自己独一无二的进程组id,通过在fork()之后的子进程中Setpgid(0,0)实现,这样当我们向前台程序发送ctrl+c 或ctrl+z命令时,才不会影响到后台程序。如果没有这一步,则所有的子进程与当前的tsh shell进程为同一个进程组,发送信号时,前后台的子进程均会收到。
  2. 在fork()新进程前后要阻塞SIGCHLD信号,防止出现竞争(race)这种经典的同步错误,如果不阻塞会出现子进程先结束从jobs中删除,然后再执行到主进程addjob的竞争问题。相关解释和方法见CSAPP P519页。

下面是eval()函数的实现

void eval(char *cmdline)
{
    char *argv[MAXLINE];    /*argument list of execve()*/
    char buf[MAXLINE];      /*hold modified commend line*/
    int bg;                 /*should the job run in bg or fg?*/
    pid_t pid;
    sigset_t mask;          /*mask for signal*/

    stpcpy(buf,cmdline);
    bg = parseline(buf,argv);

    if(argv[0]==NULL){
        return;     /*ignore empty line*/
    }

    if(!builtin_cmd(argv)){                         /*not a build in cmd*/
        Sigemptyset(&mask);
        Sigaddset(&mask,SIGCHLD);
        Sigprocmask(SIG_BLOCK,&mask,NULL);           /*block the SIGCHLD signal*/

        if((pid = Fork())==0)
        {
            Sigprocmask(SIG_UNBLOCK,&mask,NULL);     /*unblock the SIGCHLD signal in child*/
            Setpgid(0,0);                            /*puts the child in a new process group*/

            if(execve(argv[0],argv,environ)<0){
                printf("%s: Command not found\n",argv[0]);
                exit(0);
            }
        }

        addjob(jobs, pid, bg?BG:FG,cmdline);        /*add job into jobs*/
        Sigprocmask(SIG_UNBLOCK,&mask,NULL);        /*unblock the SIGCHLD signal in parent*/

        bg ? printf("[%d] (%d) %s", pid2jid(pid), pid,cmdline):waitfg(pid); /*do in background or foreground*/
    }
    return;
}

2. builtin_cmd

该函数主要用来判断cmd是否是内建指令,如果是则立即执行,不是则返回。对于单独的&指令直接无视。

int builtin_cmd(char **argv)
{
    if(!strcmp(argv[0],"quit"))
        exit(0);
    if(!strcmp(argv[0],"&"))
        return 1;
    if(!strcmp(argv[0],"bg")||!strcmp(argv[0],"fg"){
        do_bgfg(argv);
        return 1;
    }
    if(!strcmp(argv[0],"jobs")){
        listjobs(jobs);
        return 1;
    }

    return 0;     /* not a builtin command */
}

3.do_bgfg函数

主要执行bg和fg指令功能
1. 函数中输出的相关提示指令需要trace09.txt 和trace10.txt在./tshref shell中执行来获得
2. 输入时%num 代表任务id,num代表进程id

void do_bgfg(char **argv)
{
    pid_t pid;
    struct job_t *job;
    char *id = argv[1];

    if(id==NULL){       /*bg or fg has the argument?*/
        printf("%s command requires PID or %%jobid argument\n",argv[0]);
        return;
    }

    if(id[0]=='%'){     /*the argument is a job id*/
        int jid = atoi(&id[1]);
        job = getjobjid(jobs,jid);
        if(job==NULL){
            printf("%%%d: No such job\n",jid);
            return;
        }
    }else if(isdigit(id[0])){               /*the argument is a pid is a digit number?*/
        pid = atoi(id);
        job = getjobpid(jobs,pid);
        if(job==NULL){
            printf("(%d): No such process\n",pid);
            return ;
        }
    }else{
        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }

    Kill(-(job->pid),SIGCONT); /*send the SIGCONT to the pid*/

    if(!strcmp(argv[0],"bg")){ /*set job state ,do it in bg or fg*/
        job->state = BG;
        printf("[%d] (%d) %s", job->jid, job->pid,job->cmdline);
    }else{
        job->state = FG;
        waitfg(job->pid);
    }
    return;
}

4.waitfg

在eval函数中调用,用来等待前台子进程的完成。
在注释中可以看到最好不要用waitpid(pid,NULL,0);,根据shell lab的writeup中的提示,我们可以看到推荐不要同时在SIGCHLD和waitfg函数中使用waitpid(),因为这样会让人迷惑,在同一个程序的两个地方均会回收僵死进程。
当然这样做也是可行的,在执行waitfg的waitpid()时通过gdb调试,可以看到子进程结束的SIGCHLD信号会被sigchld_handler中waitpid()处理,处理结束后会返回到waitfg()的waitpid()函数继续判断,程序亦然符合我们预期的执行。
但是,根据我们还是根据writeup中推荐的方法,当子进程结束时发出SIGCHLD信号后,由sigchld_handler()处理并回收僵尸进程并从jobs中删除该前台进程。我们在程序中运用sleep函数来等待jobs列表中是否还存在前台进程,如果不存在则返回。

One of the tricky parts of the assignment is deciding on the allocation of work between the waitfg and sigchld handler functions. We recommend the following approach:
- In waitfg,use a busy loop around the sleep function.
- In sigchldhandler,use exactly one callto waitpid.

While other solutions are possible, such as calling waitpid in both waitfg and sigchld handler, these can be very confusing. It is simpler to do all reaping in the handler.

/*
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    while(pid == fgpid(jobs)){
        sleep(0);
    }
    return;
//    waitpid(pid,NULL,0);    /*this is wrong answer ,see the num5 hints*/
}

5.sigchld_handler

该函数是SIGCHLD信号的响应函数。
该函数中运用waitpid()函数并且用WNOHANG|WUNTRACED参数,该参数的作用是判断当前进程中是否存在已经停止或者终止的进程,如果存在则返回pid,不存在则立即返回
通过另外一个&status参数,我们可以判断返回的进程是由于什么原因停止或暂停的。

  • WIFEXITED(status):
    如果进程是正常返回即为true,什么是正常返回呢?就是通过调用exit()或者return返回的
  • WIFSIGNALED(status):
    如果进程因为捕获一个信号而终止的,则返回true
  • WTERMSIG(status):
    当WIFSIGNALED(status)为真时,设置该值,返回导致当前状态的信号编号
  • WIFSTOPPED(status):
    如果返回的进程当前是被停止,则为true
  • WSTOPSIG(status):
    返回引起进程停止的信号
void sigchld_handler(int sig)
{
    pid_t pid;
    int status;
    while((pid = waitpid(-1,&status,WNOHANG|WUNTRACED))>0){
        if(WIFEXITED(status)){  /*process is exited in normal way*/
            deletejob(jobs,pid);
        }
        if(WIFSIGNALED(status)){/*process is terminated by a signal*/
            printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
            deletejob(jobs,pid);
        }
        if(WIFSTOPPED(status)){/*process is stop because of a signal*/
            printf("Job [%d] (%d) stopped by signal %d\n",pid2jid(pid),pid,WSTOPSIG(status));
            struct job_t *job = getjobpid(jobs,pid);
            if(job !=NULL )job->state = ST;
        }
    }
    if(errno != ECHILD)
        unix_error("waitpid error");
    return;
}

6.sigint_handler

该函数用来捕获响应ctrl-c操作,并且将该信号发送为所有前台程序

void sigint_handler(int sig)
{
    pid_t pid = fgpid(jobs);
    if(pid != 0){
        Kill(-pid,SIGINT);
        /*let the sigchld_handler to delete the job in jobs?*/
    }
    return;
}

7.sigtstp_handler

该函数用来捕获响应ctrl-z操作,并且将该信号发送为所有前台程序

void sigtstp_handler(int sig)
{

    pid_t pid = fgpid(jobs);

    if(pid!=0 ){
        struct job_t *job = getjobpid(jobs,pid);
        if(job->state == ST){  /*already stop the job ,do‘t do it again*/
            return;
        }else{
            Kill(-pid,SIGTSTP);
        }
    }
    return;
}
版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

csapp lab2 bomb 二进制炸弹《深入理解计算机系统》

bomb炸弹实验 首先对bomb这个文件进行反汇编,得到一个1000+的汇编程序,看的头大。 phase_1: 0000000000400ef0 : 400ef0: 48 83 ec 08...
  • fang92
  • fang92
  • 2015年06月05日 17:13
  • 2585

CSAPP: Proxy lab

CSAPP proxy lab

CSAPP: shell lab 解答

  • 2013年05月07日 17:42
  • 290KB
  • 下载

【CSAPP】Shell Lab 外壳实验

这个实验的目的是为了更加熟悉进程控制和信号处理。从给出到说明文档得知,实验主要是按照tshref.out文件的说明,一步一步往tsh.c添加相应的功能。同时还有tshref文件作为我们要达到的目标。 ...

csapp实验,一个简单的shell. Lab Assignment L5: Writing Your Own Unix Shell

实验指导书 http://csapp.cs.cmu.edu/3e/shlab.pdf 该知道的在实验指导书都有了,以下是感觉这个实验重要的地方 清楚前台和后台的概念,这是shell创造的概念,有外部命...

CSAPP lab3 实验指导说明

  • 2014年09月15日 16:56
  • 778KB
  • 下载

关于CSAPP lab3中压栈问题引发的思考

之前有个问题也没特别注意,今天回来看邮件发现有同学和我讨论关于函数调用压栈的问题。 废话少说,直接上对比测试图: 图一:CSAPP lab3的getbuf反汇编结果截图 图二: 我测...

CSAPP 六个重要实验 lab2 实验材料

  • 2014年09月10日 21:19
  • 40KB
  • 下载

CSAPP lab5 实验材料

  • 2014年10月10日 01:50
  • 330KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:CSAPP: Shell Lab
举报原因:
原因补充:

(最多只允许输入30个字)