欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CSAPP: Shell Lab实现思路与细节

程序员文章站 2022-07-04 19:30:05
...

题目要求

今天,我们要实现一个简易的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.

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

最后

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

相关标签: CSAPP