CSAPP LAB7 shell-lab

实验题目:shell-lab

实验目的:

在本次实验中,我们需要构建一个简单的类Unix/Linux Shell。基于已经提供的“微Shell”框架tsh.c,完成部分函数和信号处理函数的编写工作。使用sdriver.pl可以评估你所完成的shell的相关功能。

实验环境:

Ubuntu12.04

实验内容及操作步骤:

  • 题目解读

在本次实验中,我们需要做的就是填充如下几个函数的内容:

  • eval: 解析和解释命令行的主要例程。[70行]
  • builtin cmd:识别和解释内置命令:quit、fg、bg和jobs。[25行]
  • do bgfg:实现bg和fg内置命令。[50行]
  • waitfg: 等待前台作业完成。[20行]
  • sigchld handler: 捕获SIGCHILD信号。[80行]
  • sigint handler: 捕获SIGINT (ctrl-c) 信号。[15行]
  • sigtstp handler: 捕获SIGTSTP (ctrl-z) 信号。[15行]

我们的tsh-shell应该有以下功能:

  • 提示应该是字符串“tsh>”。
  • 用户键入的命令行应由名称和零个或多个参数组成,所有参数均由一个或多个空格分隔。如果name是内置命令,那么tsh应该立即处理它,并等待下一个命令行。否则,tsh应该假设name是可执行文件的路径,它在初始子进程的上下文中加载和运行 (在这种情况下,术语job指的是这个初始子进程)。
  • tsh 不需要支持管道 (|) 或 I/O 重定向(< 和 >)。
  • 键入 ctrl-c (ctrl-z) 应该会导致将 SIGINT (SIGTSTP) 信号发送到当前前台作业,以及该作业的任何后代(例如,它派生的任何子进程)。如果没有前台工作,那么信号应该没有效果。
  • 如果命令行以 & 符号结尾,那么 tsh 应该在后台运行该作业。否则,它应该在前台运行作业。
  • 每个作业都可以通过进程 ID (PID) 或作业 ID (JID) 来标识,这是一个由 tsh 分配的正整数。 JID 应该在命令行上用前缀 '%' 表示。例如,“%5”表示 JID 5,“5”表示 PID 5。(我们为您提供了操作作业列表所需的所有例程。)
  • tsh应该支持以下内置命令:
    • quit命令终止shell。
    • job命令列出所有后台运行的工作。
    • bg 命令重新启动<工作>通过发送SIGCONT,然后在后台运行。
    • 参数可以是一个PID或JID。
    • fg 命令重新启动<工作>通过发送SIGCONT,然后在前台运行它。
    • 参数可以是一个PID或JID。
    • tsh 应该收获它所有的僵尸孩子。如果任何作业因为接收到它没有捕获的信号而终止,则 tsh 应该识别此事件并打印一条带有作业 PID 和违规信号描述的消息。

如何检查程序:

每一次修改好代码后都需要用make来重新编译更新我们的系统。

文件提供了16个跟踪文件(trace01-16.txt),通过这些文件结合shell驱动程序,我们来测试shell的正确性。编号较低的文件执行非常简单的测试,编号较高的测试执行更复杂的测试。

一些tips:

  • 阅读教科书中第 8 章(异常控制流)的每一个字。
  • 使用跟踪文件来指导您的 shell 的开发。 trace01.txt 开始,确保您的 shell 产生与参考 shell 相同的输出。 然后继续跟踪文件 trace02.txt,依此类推。
  • waitpidkillforkexecvesetpgid sigprocmask 函数会派上用场。 waitpid WUNTRACED WNOHANG 选项也很有用。
  • 当您实现信号处理程序时,请确保将 SIGINT SIGTSTP 信号发送到整个前台进程组, kill 函数的参数中使用“-pid”而不是“pid” sdriver.pl 程序测试此错误。
  • 实验室的一个棘手部分是决定waitfg sigchld 处理函数之间的工作分配。 我们推荐以下方法:
    • waitfg 中,围绕 sleep 函数使用busy loop
    • sigchld 处理程序中,只调用一次 waitpid
  • 虽然其他解决方案是可能的,例如在 waitfg sigchld 处理程序中调用 waitpid,但这些可能会非常令人困惑。 在处理程序中进行所有收获更简单。
  • eval 中,父级必须在分叉子级之前使用 sigprocmask 阻止 SIGCHLD 信号,然后解除阻塞这些信号,在通过调用 addjob 将子级添加到作业列表后再次使用 sigprocmask。由于子进程继承了父进程的阻塞向量,因此子进程必须确保在执行新程序之前解除阻塞 SIGCHLD 信号。

父级需要以这种方式阻止 SIGCHLD 信号,以避免在父级调用 addjob 之前子级被 sigchld 处理程序收割(并因此从作业列表中删除)的竞争条件。

  • 诸如 morelessvi emacs 之类的程序在终端设置上会做一些奇怪的事情。 不要从你的 shell 运行这些程序。 坚持使用简单的基于文本的程序,例如 /bin/ls/bin/ps /bin/echo
  • 当您从标准Unix shell运行您的shell时,您的shell正在前台进程组中运行。如果您的shell随后创建了一个子进程,则默认情况下,该子进程也将是前台进程组的成员。由于键入ctrl-c会向前台组中的每个进程发送一个SIGINT,因此键入ctrl-c会向您的shell以及您的shell创建的每个进程发送一个SIGINT,这显然是不正确的。

解决方法如下:在fork之后,但在execve之前,子进程应该调用setpgid00),这会将子进程放入一个新的进程组中,其组ID与子进程的PID相同。这将确保前台进程组中只有一个进程,即您的shell。当您键入ctrl-c时,shell应该捕获生成的SIGINT,然后将其转发到相应的前台作业(或者更准确地说,是包含前台作业的进程组)。

准备工作:

先用tar xvf shlab-handout.tar解压压缩包;在tsh.c文件中补充代码;利用make和16个test.txt文件进行测试。

  • 补全函数

1、void eval(char *cmdline)函数

函数功能:

用于评估用户刚输入的命令行。如果用户请求了内置命令(quit,jobs,bg或fg),则立即执行。 否则,fork一个子进程并在子进程的上下文中运行该作业。如果作业在前台运行,等待它终止然后返回。

参数:

形参是用户输入的命令行cmdline 。实参是argv , mask, SIG_UNBLOCK , argv[0] , environ。

代码:

void eval(char *cmdline)   

{  

    /* $begin handout */  

    char *argv[MAXARGS]; /* argv for execve() */  

    int bg;              /* should the job run in bg or fg? */  

    pid_t pid;           /* process id */  

    sigset_t mask;       /* signal mask */  

  

    /* Parse command line */  

    bg = parseline(cmdline, argv);   

    if (argv[0] == NULL)    

        return;   /* ignore empty lines */  

  

    if (!builtin_cmd(argv)) {   /* 判断是否为内置命令 */

  

        /*  

         * This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP 

         * signals until we can add the job to the job list. This 

         * eliminates some nasty races between adding a job to the job 

         * list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.   

         */  

  

        if (sigemptyset(&mask) < 0)//block  mask集合置空  

            unix_error("sigemptyset error");  

        if (sigaddset(&mask, SIGCHLD)) //添加SIGCHLDmask集合  

            unix_error("sigaddset error");  

        if (sigaddset(&mask, SIGINT)) //添加SIGINT进集合  

            unix_error("sigaddset error");  

        if (sigaddset(&mask, SIGTSTP)) //添加SIGTSTP(来自终端的停止信号)进集合  

            unix_error("sigaddset error");  

        if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)//mask更新BLOCK  

            unix_error("sigprocmask error");  

  

        /* Create a child process */  

        if ((pid = fork()) < 0)  

            unix_error("fork error");  

      

        /*  

        * Child  process  

        */  

  

        if (pid == 0) {  

            /* Child unblocks signals */  

            sigprocmask(SIG_UNBLOCK, &mask, NULL);//mask删除SIG_BLOCK  

  

            /* Each new job must get a new process group ID  

               so that the kernel doesn't send ctrl-c and ctrl-z 

               signals to all of the shell's jobs */  

            if (setpgid(0, 0) < 0) //将创建新的进程组,进程组ID为调用进程ID  

                unix_error("setpgid error");   

  

            /* Now load and run the program in the new job */  

            if (execve(argv[0], argv, environ) < 0) {  

                printf("%s: Command not found\n", argv[0]);  

                exit(0);  

            }  

        }  

  

        /*  

        * Parent process 

        */  

  

        /* Parent adds the job, and then unblocks signals so that 

           the signals handlers can run again */  

        addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);  

        sigprocmask(SIG_UNBLOCK, &mask, NULL);//还原Block  

  

        if (!bg)   

            waitfg(pid);//前台进行  

        else  

            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);//后台进行  

    }  

    /* $end handout */  

    return;  

}  

处理流程:

第一步:定义各个变量。使用parseline()函数函数解析命令行,得到命令行参数。如果argv[0]=NULL,则无命令。

第二步:使用builtin_cmd()函数判断命令是否为内置命令,如果不是内置命令,则继续执行。

第三步:设置阻塞集合。先初始化mask为空集合,再将SIGCHLD , SIGINT ,SIGTSTP 信号加入阻塞集合。然后创建子进程。

第四步:阻塞SIGCHLD,防止子进程在父进程之前结束,防止addjob()函数错误地把(不存在的)子进程添加到作业列表中。

第五步:子进程中,先解除对SIG_CHLD的阻塞,再使用setpgid(0,0)创建一个虚拟的进程组,使子进程拥有自己唯一的进程组ID,目的是为了在键盘上键入Ctrl+C (Ctrl+Z)时,我们的后台子进程就不会从内核接收SIGINT (SIGTSTP)信号。该进程组ID表明其不和tsh进程在一个进程组。然后调用execve函数,执行相应的文件。

第六步:将job添加到job list,解除SIG_CHLD阻塞信号。判断进程是否为前台进程,如果是前台进程,调用waitfg()函数,等待前台进程,如果是后台进程,则打印出进程信息。

要点分析:

1.在键盘上键入Ctrl+C (Ctrl+Z)时,我们的后台子进程就不会从内核接收SIGINT (SIGTSTP)信号。

2.在执行addjob之前需要阻塞信号,防止addjob()函数错误地把(不存在的)子进程添加到作业列表中。

2. int builtin_cmd(char **argv)函数

函数功能:

如果用户键入了内置命令,则立即执行。

参数:

形参:传入的参数数组argv 。实参:argv[0] ,“quit” ,"&" ,“jobs” ,“bg” ,“fg”, argv,SIG_BLOCK ,mask ,prev ,NULL。

代码:

int builtin_cmd(char **argv)   

{  

    sigset_t mask, prev;  

    sigfillset(&mask);//阻塞全部信号,为下面lishjobs调用全局变量jobs准备  

    char *cmd = argv[0];//第一个字符  

    if(cmd == NULL) //若为空,就直接返回,返回1表示不用进行if语句内包含的内容因为如果仅仅按下回车会使得strcmp失效  

        return 1;  

    if(!strcmp(cmd, "&"))//若只单单有一个&字符,同上  

        return 1;  

    if(!strcmp(cmd, "quit"))//若退出,则选择退出,并选用安全的退出函数  

        _exit(0);  

    if(!strcmp(cmd, "jobs")){//jobs列出当前的进程  

        sigprocmask(SIG_BLOCK, &mask, &prev);//调用全局变量前需要阻塞全部信号  

        listjobs(jobs);  

        sigprocmask(SIG_SETMASK, &prev, NULL);//恢复原来信号  

        return 1;  

    }  

    if(!strcmp(cmd, "bg") || !strcmp(cmd, "fg")){//交由do_bgfg函数处理  

        do_bgfg(argv);  

        return 1;  

    }  

    return 0;     /* not a builtin command */  

}  

处理流程:

函数需要根据传入的参数数组,判断用户键入的是否为内置命令,采取的办法就是比较argv[0]和内置命令,如果是内置命令,则跳到相应的函数,并且返回1,如果不是,则什么也不做,并且返回0。

其中,如果命令为quit,则直接退出;如果命令是内置的jobs命令,则调用listjobs()函数,打印job列表;如果是fg或是bg两条内置命令,则调用do_bgfg()函数来处理即可。

要点分析:

1.要注意如果用户仅仅按下回车键,那么在解析后argv的第一个变量将是一个空指针。如果用这个空指针去调用strcmp函数会引发segment fault。

2.因为jobs是全局变量,为了防止其被修改,需要阻塞全部信号,过程大致为(后面函数阻塞全部信号的做法与此基本一致):

3. void do_bgfg(char **argv) 函数

函数功能:

执行内置bg和fg命令。

参数:

形参是传入的参数数组argv 。实参是argv[1] , argv[1][0] , jobs , pid , argv[1] , argv[0]。

代码:

void do_bgfg(char **argv)   

{  

    /* $begin handout */  

    struct job_t *jobp=NULL;  

      

    /* Ignore command if no argument */  

    if (argv[1] == NULL) {  

        printf("%s command requires PID or %%jobid argument\n", argv[0]);  

        return;  

    }  

      

    /* Parse the required PID or %JID arg */  

    if (isdigit(argv[1][0])) {  

        pid_t pid = atoi(argv[1]);  

        if (!(jobp = getjobpid(jobs, pid))) {  

            printf("(%d): No such process\n", pid);  

            return;  

        }  

    }  

    else if (argv[1][0] == '%') {  

        int jid = atoi(&argv[1][1]);  

        if (!(jobp = getjobjid(jobs, jid))) {  

            printf("%s: No such job\n", argv[1]);  

            return;  

        }  

    }         

    else {  

        printf("%s: argument must be a PID or %%jobid\n", argv[0]);  

        return;  

    }  

  

    /* bg command */  

    if (!strcmp(argv[0], "bg")) {   

        if (kill(-(jobp->pid), SIGCONT) < 0)  

            unix_error("kill (bg) error");  

        jobp->state = BG;  

        printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);  

    }  

  

    /* fg command */  

    else if (!strcmp(argv[0], "fg")) {   

        if (kill(-(jobp->pid), SIGCONT) < 0)  

            unix_error("kill (fg) error");  

        jobp->state = FG;  

        waitfg(jobp->pid);  

    }  

    else {  

        printf("do_bgfg: Internal error\n");  

        exit(0);  

    }  

    /* $end handout */  

    return;  

}  

处理流程:

第一步:先判断fg或bg后是否有参数,如果没有,则忽略命令。

第二步:如果fg或bg后面只是数字,说明取的是进程号,获取该进程号后,使用getjobpid(jobs, pid)得到job;如果fg或bg后面是%加上数字的形式,说明%后面是任务号(第几个任务),此时获取jid后,可以使用getjobjid(jobs, jid)得到job。

第三步:比较区分argv[0]是“bg”还是“fg”。如果是后台进程,则发送SIGCONT信号给进程组PID的每个进程,并且设置任务的状态为BG,打印任务的jid,pid和命令行;如果是前台进程,则发送SIGCONT信号给进程组PID的每个进程,并且设置任务的状态为FG,调用waitfg(jobp->pid),等待前台进程结束。

要点分析:

1.函数主要是先判断fg后面是%+数字还是只有数字的形式,从而根据进程号pid或是工作组号jid来获取结构体job;然后在根据前台和后台进程的不同,执行相应的操作。

2. isdigit()函数判断是否为数字,不是数字返回0。

3. atoi()函数把字符串转化为整型数。

4. SIGCONT信号对应事件为:继续进程如果该进程停止。

4. void waitfg(pid_t pid) 函数

函数功能:

阻止直到进程pid不再是前台进程。

参数:

形参是前台进程pid。实参是mask,jobs。

代码:

void waitfg(pid_t pid)  

{  

    sigset_t mask;  

    sigemptyset(&mask);//在挂起进程的过程中清空block以便信号能够响应  

    while(pid == fgpid(jobs)) {//前台工作的进程值比较  

        sigsuspend(&mask);//挂起进程  

    }  

    return;  

}  

处理流程:

函数主体是while循环语句,判断传入的pid是否为一个前台进程的pid,如果是,则一直循环,如果不是,则跳出循环。其中while循环内部使用sigsuspend()函数,暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,选择运行一个处理程序或者终止该进程。

要点分析:

1.在while内部,如果使用的只是pause()函数,那么程序必须等待相当长的一段时间才会再次检查循环的终止条件,如果使用向nanosleep这样的高精度休眠函数也是不可接受的,因为没有很好的办法来确定休眠的间隔。但是PPT上示例代码却给出sleep(1),我:???。

2.在while循环语句之前,初始化mask结合为空,在while内部用SIG_SETMASK使block=mask,这样sigsuspend()才不会因为收不到SIGCHLD信号而永远睡眠。

5. void sigchld_handler(int sig) 函数

函数功能:

每当一个子进程终止(成为僵死进程),或者因为接收到SIGSTOPSIGTSTP信号而停止时,内核就向shell发送一个SIGCHLD。处理程序获取所有可用的僵死子进程,但不等待当前正在运行的任何其他子进程终止。

参数:

形参是sig,实参是status , WNOHANG | WUNTRACED , SIG_BLOCK , mask , prev, SIG_SETMASK

代码:

void sigchld_handler(int sig)   

{  

    int olderrno = errno, status;//status 用于检查回收子进程的退出状态  

    sigset_t mask, prev;//用于阻塞全局共享数据结构  

    struct job_t *jobfirst;//方便后续删除  

    sigfillset(&mask);  

    pid_t pid;//记录停止的子进程ID  

    while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){//立即返回,如果等待集合中的子进程都没有被停止或终止,则返回值为0;如果有一个停止或终止,则返回值为该子进程的PID  

        sigprocmask(SIG_BLOCK, &mask, &prev);//删除进程时需要阻塞所有信号——引用了全局共享数据结构  

        jobfirst = getjobpid(jobs, pid);//根据PID寻找进程  

        if(WIFEXITED(status)) {//引起返回的子进程当前是正常停止的  

            deletejob(jobs, pid);  

        }  

        else if(WIFSIGNALED(status)){//因一个未被捕获的信号终止  

            printf("Job [%d] (%d) terminated by signal %d\n", jobfirst->jid, jobfirst->pid, WTERMSIG(status));  

            deletejob(jobs, pid);  

        }  

        else if(WIFSTOPPED(status)){//因为子进程停止而停止  

            jobfirst->state = ST;  

            printf("Job [%d] (%d) stopped by signal %d\n", jobfirst->jid, jobfirst->pid, WSTOPSIG(status));  

        }  

        fflush(stdout);  

        sigprocmask(SIG_SETMASK, &prev, NULL);//恢复Block  

    }  

    errno = olderrno;//还原errno  

    return;  

处理流程:

第一步:把每个信号都添加到mask阻塞集合中,设置olderrno = errno 。

第二步:在while循环中使用waitpid(-1, &status, WNOHANG | WUNTRA

CED)),其中,目的是尽可能回收子进程,其中WNOHANG | WUNTRACED表示立即返回,如果等待集合中没有进程被中止或停止返回0,否则孩子返回进程的pid。

第三步:在循环中阻塞信号,并且使用getjobpid()函数,通过pid找到job 。

第四步:通过waitpid在status中放上的返回子进程的状态信息,判断子进程的退出状态。如果引起返回的子进程当前是停止的,那么WIFSTOPPED(status)就返回真,此时只需要将pid找到的job的状态改为ST,并且按照示例程序输出的信息,将job的jid,pid以及导致子进程停止的信号的编号输出即可。如果子进程是因为一个未被捕获的信号终止的,那么WIFSIGNALED(status)就返回真,此时同样按照示例程序输出的信息,将job的jid,pid以及导致子进程终止的信息的编号输出即可,因为此时进程是中止的的进程,所以还需要deletejob()将发出SIGCHLD信号的将其直接回收。

第五步:清空缓冲区,解除阻塞,恢复errno。

要点分析:

1.while循环来避免信号阻塞的问题,循环中使用waitpid()函数,以尽可能多的回收僵尸进程。但是使用while可能会让waitpid等待后台还在进行的进程结束,但如果使用一次if可能会导致信号累加的问题,例如多个后台程序同时结束的情况。然后PPT上建议使用一次waitpid,示例代码却又将其用在了while循环判断条件。考虑到函数的目的是要获取所有可用的僵死进程,故而采用while循环。

2.调用deletejob()函数时,因为jobs是全局变量,因此需要阻塞信号。

3.通过waitpid在status中放上的返回子进程的状态信息,判断子进程的退出状态。WIFSIGNALED判断子进程是否因为一个未被捕获的信号中止的,WIFSTOPPED判断引起返回地子进程当前是否为停止的。WIFEXITED判断是否是正常返回。

6. void sigint_handler(int sig) 函数和void sigchld_handler(int sig) 函数

函数功能:

前者:当用户在键盘上键入Ctrl+C时,内核向shell发送一个SIGINT。捕获它并将其发送到前台作业。

后者:每当用户在键盘上键入Ctrl+Z时,内核都会向shell发送一个SIGTSTP。捕获它并通过发送一个SIGTSTP来挂起前台作业。

参数:

形参是sig,实参是SIG_BLOCK , mask , prev, SIG_SETMASK

代码:

void sigint_handler(int sig)   

{  

    pid_t pid;  

    sigset_t mask, prev;  

    int olderrno = errno;  

    sigfillset(&mask);  

    sigprocmask(SIG_SETMASK, &mask, &prev);//日常……  

    pid = fgpid(jobs);//获取前台进程组  

    sigprocmask(SIG_SETMASK, &prev, NULL);  

    if(pid != 0)  

        kill(-pid, SIGINT);//发给前台进程组信号  

    errno = olderrno;  

    return;  

}  

void sigtstp_handler(int sig)   

{  

    pid_t pid;  

    sigset_t mask, prev;  

    int olderrno = errno;  

    sigfillset(&mask);  

    sigprocmask(SIG_SETMASK, &mask, &prev);//日常……  

    pid = fgpid(jobs);//获取前台进程组  

    sigprocmask(SIG_SETMASK, &prev, NULL);  

    if(pid != 0)  

        kill(-pid, SIGTSTP);//发给前台进程组信号  

    errno = olderrno;  

    return;  

}

处理流程:

第一步:将每个信号添加至mask阻塞集合,设置olderrno = errno
第二步:获取前台进程组的pgid
第三步:还原原阻塞信号,若有前台进程组,将信号SIGINT发给这个进程组。
第四步:还原errno

要点分析:

jobs为全局共享数据结构,需要进行信号阻塞后对它进行调用,然后需还原信号阻塞。发送信号的时候需要对整个进程组进行。

  • 测试

tshtshref进行对比,相同则说明结果正确。

 

 

 

 

 

 

 

 

 

 

——————————————————————————————————————————

 

 

——————————————————————————————————————————

 

 

——————————————————————————————————————————

 

 

——————————————————————————————————————————

 

 

——————————————————————————————————————————

 

 

——————————————————————————————————————————

 

——————————————————————————————————————————

测试结果显示正确。

四、收获与体会

在本次实验中,我直到了shell接口的一些用法,对于信号处理相关的知识也更熟练了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值