CSAPP: Shell Lab

标签: shell csapp 进程控制 信号管理 c系统编程
7817人阅读 评论(0) 收藏 举报
分类:

介绍

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;
}
查看评论

shell lab 实现详解

这次的CSAPP的实验是要自己实现一个shell(外壳),即自己实现一个命令行,要自己实现一个简单的我们在linux上常用的shell,想想就让人兴奋呢。 这次的实验环境,已经给我们搭好了程序的基本...
  • github_33873969
  • github_33873969
  • 2017-09-08 17:15:43
  • 1755

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

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

【CSAPP】Shell Lab 外壳实验

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

ICS shell lab总结

ICS shell lab总结
  • PKU_ZZY
  • PKU_ZZY
  • 2016-11-23 20:21:58
  • 1137

ICS lab9 shell lab

  • 2015年01月25日 20:50
  • 243KB
  • 下载

CMU 深入理解计算机操作系统 shell lab

文章定位:写一下这个lab有的坑,防止下次继续踩坑。若想自己解坑,read every word carefully in wrtie-up。 问题1: 前台运行的程序,要不要加入工作队列(...
  • donggua_fu
  • donggua_fu
  • 2018-01-10 19:27:59
  • 115

CSAPP(深入理解计算机系统) 实验——实现shell

/* * tsh - A tiny shell program with job control * rommel @copyright */ #include #include #inc...
  • rommel1
  • rommel1
  • 2012-03-21 19:50:18
  • 7988

SJTU->SE->ICS->LAB9 Tiny Shell

实在是不想再继续看大雾了,这讲的都是啥。于是就作死地试着把win7换成了win8.1,问题是真多- -在写这个日志的时候,我的输入法每打一个字,整个chrome就会不响应几秒钟,我整个人都不好了。于是...
  • u011450272
  • u011450272
  • 2013-12-17 12:00:13
  • 1453

ics csapp lab tiny shell

  • 2013年12月16日 23:19
  • 118KB
  • 下载

CSAPP: shell lab 解答

  • 2013年05月07日 17:42
  • 290KB
  • 下载
    个人资料
    持之以恒
    等级:
    访问量: 9万+
    积分: 1105
    排名: 4万+