CSAPP: Shell Lab实现思路与细节

题目要求

今天,我们要实现一个简易的shell。通过这个实验,也会让我们对shell这个神秘的东西减少一丝陌生。下面进入正题:
shell程序框架已经帮你搭建好了,我们只需完成以下六个函数:

  • eval: 主要对命令行进行分词并执行指令
  • builtin_cmd: 检查是否为内置指令
  • do_bgfg: 执行fg、bg指令
  • waitfg: 等待fg指令执行完毕
  • sigchld_handler: SIGCHLD信号处理函数
  • sigint_handler: SIGINT信号处理函数(ctrl-c)
  • sigtstp_handler: SIGTSTP信号处理函数(ctrl-z)

题目具体的细节要求在指导书中都有,一定要多读指导书和课本,一定要多读指导书和课本!!!

整体思路

整个shell的思路大体上是这样的。

首先我们在命令行中输入指令,eval函数获取这行命令,其首要任务是调用parseline函数对命令进行分割,明确指令和参数,并构造最终会传递给execve的argv向量。

命令行最后一个参数如果是’&'字符,那么该命令为一个后台命令(shell不会等待它完成)。否则需要前台执行该命令(shell会等待它完成)。

当解析参数后,如果命令为quitjobsbgfg四种题目要求的内置命令,则立刻执行,否则shell创建一个子进程。当进程完成后,回收子进程并进行下一轮迭代。

实现细节

指导书中建议通过使用trace01~trace15一步一步构建shell,我认为这是一个很好的思路,下面我也将按照这一思路构建整个程序。程序在最后会全部贴上,大家可以在测试trace过程中将本文该章节作为参考。

代码的运行结果可以通过输入以下命令查看,其*为01~15

make rtest*

trace02.txt和trace03.txt

首先我们需要做的,是把书中最简易的shell程序原封不动的敲进去看看运行结果。

trace02.txttrace03.txt这两个文件实现测试了对内置指令quit的测试,这一部分很简单,和书中相同,调用exit(0)就能直接实现。

trace04.txt

trace04要求运行一个后台工作,这个指令再不修改书中程序时是能够实现的。

#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &

trace05.txt

真正的问题在测试这个文件时才真正展现。通过调用jobs,因为完全没有任务管理,且书中程序并不认为jobs为一个内置命令,会出现严重的错误。

#
# trace05.txt - Process jobs builtin command.
#
/bin/echo -e tsh> ./myspin 2 \046
./myspin 2 &

/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &

/bin/echo tsh> jobs
jobs

那么,我们的思路是这样的,首先在builtin_cmd函数中,加入对jobs指令的检测,接着调用listjobs(jobs);,这在程序中人家已经给你定义好了。

struct job_t jobs[MAXJOBS];     /* The job list */
void listjobs(struct job_t *jobs);

那么如果要调用该函数,job list也必须进行相应的改变,这一实现必须在eval的父进程中实现。思路是每执行一个任务,就在job list中添加一项,每结束一个任务,就将对应pid的任务在list中删除。在访问全局变量以及创建子进程的过程中需要将SIGCHLD信号进行阻塞,否则会发生意想不到的错误。思路与书中642页程序大致相同。

trace06.txt

trace05的工作量其实还蛮大的,如果完成后,下面就要开始编写信号处理函数。trace06.txt是对ctrl-c进行处理,即SIGINT信号。它运行了一个./myspin 4并在两秒后对其中断。那么思路很明显,这部分要让我们编写sigint_handler函数。

#
# trace06.txt - Forward SIGINT to foreground job.
#
/bin/echo -e tsh> ./myspin 4
./myspin 4 

SLEEP 2
INT

其思路就是键盘按下ctrl-c后,进入信号处理函数,然后终止前台正在运行的函数。不过这地方又一个小小的trick,就是在处理函数中,需要获取前台正在运行进程的pid,而访问全局变量时最好对信号量进行阻塞。

trace07.txt

trace07实质上是对前面两个trace的组合,如果之前函数没问题的话,这个部分可以直接通过。jobs会显示当前运行的程序。

trace08.txt

这一条测试是对前台程序进行停止(挂起),即对sigtstp_handler函数进行编写。该函数与sigint_handler几乎一模一样,只是在kill中的参数不同而已。

#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &

/bin/echo -e tsh> ./myspin 5
./myspin 5 

SLEEP 2
TSTP

/bin/echo tsh> jobs
jobs

trace09.txt和trace10.txt

本次作业的又一个难点来了,它是对bgfg指令进行解析并运行。因此,这里需要完成do_fg函数。该函数需要做到以下几点:

  • bgfg命令进行区分
  • pid和jib参数的打印与错误提示

该函数的大致思路是这样的

  • 首先判断输入指令是否正确
  • 获取指令的pid、jid以及当前操作pid号的job结构体指针(这一步异常重要,因为输入 fg/bg %num指令进行操作时,是无法获取当前指令的pid的,因此更改运行状态等会遇到一些小麻烦,解决的一种方法是自己写一个jid2pid以及更改对应status的函数,这里我嫌麻烦直接就获取job的指针直接修改了)
  • 利用switch语句对几种不同status分别进行操作(fg的实质就是激活后台进程在前台等待,bg只是简单的重启进程即可)

这里会遇到一些细小的问题

do_bgfg函数在处理子进程回收上会遇到一个奇怪的问题,当我在mac和linux上运行是,执行fg %1后程序必然会卡住。在网上搜了许久后,终于找到了答案。
在mac端输入man signal后会看到

20    SIGCHLD      discard signal       child status has changed

因此,问题就出在这里,书中的对SIGCHLD记录是:一个子进程停止或终止。而真实情况是当子进程从stop转换为running时,子进程仍然会发送一个SIGCHLD信号!!!这就是导致fg指令后一直卡住的原因。

解决这个问题的方法是创建一个stopped_child_pid的全局变量,专门用来保存停止状态子进程的pid。在sigchld_handler函数中专门对该情况进行处理即可。这里参考了这位大佬的解答

trace11~trace15

如果能过顺利完成前10个测试条例,其余的都是对之前的一些组合,基本都能通过测试。

代码

eval

/* 
 * eval - Evaluate the command line that the user has just typed in
 * 
 * If the user has requested a built-in command (quit, jobs, bg or fg)
 * then execute it immediately. Otherwise, fork a child process and
 * run the job in the context of the child. If the job is running in
 * the foreground, wait for it to terminate and then return.  Note:
 * each child process must have a unique process group ID so that our
 * background children don't receive SIGINT (SIGTSTP) from the kernel
 * when we type ctrl-c (ctrl-z) at the keyboard.  
*/
void eval(char *cmdline)
{
    char *argv[MAXARGS];    /* Arguments list execve() */
    char buf[MAXLINE];      /* Holds modified command line */
    int bg;                 /* Should the job run in bg or fg */
    pid_t pid;              /* Process id */
    sigset_t mask;          /* mask of signal */

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

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

    if (!builtin_cmd(argv)) {       /* not a build line */

        /* Block SIGCHLD signal */
        sigemptyset(&mask);
        sigaddset(&mask, SIGCHLD);
        sigprocmask(SIG_BLOCK, &mask, NULL);

        if ((pid = fork()) == 0) {  /* Child runs user job */

            /* unblock SIGCHLD signal */
            sigprocmask(SIG_UNBLOCK, &mask, NULL);

            /* puts the child in a new process group */
            setpgid(0, 0);

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

        /* Parent waits for foreground job to terminal or print message of background job */
        if (!bg) {
            fg_pid = pid;
            stopped_child_pid = 0;
            /* add job into job list */
            addjob(jobs, pid, FG, cmdline);
            /* unblock SIGCHLD signal */
            sigprocmask(SIG_UNBLOCK, &mask, NULL);
            waitfg(pid);
        } else {
            addjob(jobs, pid, BG, cmdline);
            sigprocmask(SIG_UNBLOCK, &mask, NULL);
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
        }
    }

    return;
}

builtin_cmd

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv)
{
    if (!strcmp(argv[0], "&")) {    /* Ignore singleton */
        return 1;
    }

    if (!strcmp(argv[0], "quit")) { /* quit command */
        exit(0);
    }

    if (!strcmp(argv[0], "jobs")) { /* jobs command */
        listjobs(jobs);
        return 1;
    }

    if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {   /* bg or fg command */
        do_bgfg(argv);
        return 1;
    }

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

do_bgfg

/*
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv)
{
    if (argv[1] == NULL) {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
    char *cmd = argv[0];
    char *param = argv[1];
    struct job_t *job;
    sigset_t mask, prev;
    int pid, jid;

    sigfillset(&mask);

    if (param[0] == '%') {
        jid = atoi(&(param[1]));
    } else {
        pid = atoi(&(param[0]));
        jid = pid2jid(pid);
    }

    sigprocmask(SIG_BLOCK, &mask, &prev);
    job = getjobjid(jobs, jid);

    if (job == NULL) {
        param[0] == '%' ? printf("%s", param) : printf("(%s)", param);
        printf(": No such process\n");
        return;
    }
    /* bg or fg */
    if (!strcmp(cmd, "bg")) {   /* bg command */
        switch (job->state) {
            case ST:
                job->state = BG;
                stopped_child_pid = job->pid;
                kill(-(job->pid), SIGCONT);
                printf("Job [%d] (%d) %s", job->jid, job->pid, job->cmdline);
                break;
            case BG:
                break;
            case UNDEF:
            case FG:
                unix_error("error use bg command.");
                break;
        }
    } else {
        switch (job->state) {   /* fg command */
            case ST:
                job->state = FG;
                stopped_child_pid = job->pid;
                kill(-(job->pid), SIGCONT);
                waitfg(job->pid);
                break;
            case BG:
                job->state = FG;
                stopped_child_pid = job->pid;
                waitfg(job->pid);
                break;
            case FG:
            case UNDEF:
                unix_error("error use fg command.\n");
                break;
        }

    }

    sigprocmask(SIG_SETMASK, &prev, NULL);
    return;
}

waitfg

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    while (!stopped_child_pid) {
        sleep(1);
    }

    stopped_child_pid = 0;
    return;
}

sigchld_handler

/* 
 * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
 *     a child job terminates (becomes a zombie), or stops because it
 *     received a SIGSTOP or SIGTSTP signal. The handler reaps all
 *     available zombie children, but doesn't wait for any other
 *     currently running children to terminate.
 */
 void sigchld_handler(int sig) 
{
    int olderrno = errno;

    if (stopped_child_pid) {
        stopped_child_pid = 0;
        return;
    }

    sigset_t mask, prev;
    pid_t pid;
    int status;

    sigfillset(&mask);

    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        sigprocmask(SIG_BLOCK, &mask, &prev);
        if (pid == fgpid(jobs)) {
            stopped_child_pid = 1;
        }

        if (WIFSTOPPED(status)) {
            getjobpid(jobs, pid)->state = ST;
            printf("Job [%d] (%d) stopped by signal %d.\n", pid2jid(pid), pid, WSTOPSIG(status));
        } else {
            if (WIFSIGNALED(status)) {
                printf("Job [%d] (%d) terminated by signal %d.\n", pid2jid(pid), pid, WTERMSIG(status));
            }

            deletejob(jobs, pid);
        }
        fflush(stdout);
        sigprocmask(SIG_SETMASK, &prev, NULL);
    }

    errno = olderrno;
}

sigtstp_handler

/*
 * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
 *     the user types ctrl-z at the keyboard. Catch it and suspend the
 *     foreground job by sending it a SIGTSTP.  
 */
void sigtstp_handler(int sig) 
{
    int olderrno = errno;
    sigset_t mask, prev;
    pid_t pid;

    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &prev);
    pid = fgpid(jobs);
    sigprocmask(SIG_SETMASK, &prev, NULL);

    if (pid != 0) {
        kill(-pid, SIGTSTP);
    }

    errno = olderrno;
    return;
}

sigint_handler

/* 
 * sigint_handler - The kernel sends a SIGINT to the shell whenver the
 *    user types ctrl-c at the keyboard.  Catch it and send it along
 *    to the foreground job.  
 */
void sigint_handler(int sig) 
{
    int olderrno = errno;
    sigset_t mask, prev;
    pid_t pid;

    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &prev);
    pid = fgpid(jobs);
    sigprocmask(SIG_SETMASK, &prev, NULL);

    if (pid != 0) {
        kill(-pid, SIGINT);
    }

    errno = olderrno;
    return;
}

一些疑问

当我在mac上测试trace10.txt运行完fg %1后,会出现如下结果

Job [1] (759) stopped by signal 18.

而在linux端则会有

Job [1] (759) stopped by signal 20.

这个地方我一直没搞明白到底是咋回事,借此也问问大家。

最后

这是我第一次编写博客,是对之前学习进行的总结,也希望能把我的思路与理解分享给大家。最后,感谢网络上的各路大佬,没有他们的帮助,我是无法完成本次实验的,最后,再次感谢。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值