shell lab

449 阅读5分钟

前置要求

  理解csapp异常处理整章内容,获取了实验资料并且具备实验环境。

实验内容

  这个实验要求实现一个简单的shell,具备类似于linux系统上shell的功能,使用者能够在shell上运行自己的程序,使用shell提供的内置命令。
  要求实现的shell文件名为tsh.c,在这个文件中大部分函数已经被写好,我们只需完成一部分函数,要实现的函数如下:

eval // 对输入进行处理
builtin_cmd// 完成内置命令
do_bgfg // 完成bg,fg内置命令
waitfg // 阻塞直到给定pid任务不在前台
sigchld_handler //捕获 SIGCHLD
sigint_handler // 捕获 SIGINT
sigtstp_handler // 捕获 SIGTSTP

  最终目标为我们实现的tsh和实验文件中的tshref具有同样的输出。   

程序结构

  要实现这个实验,最重要的一点是理解整个shell的结构,分析代码可以发现,shell要维护一个job队列,这个队列保存当前运行在shell上的程序,我们的任务就是在处理用户输入的时候同时维护好这个队列。job的定义如下:

struct job_t {              /* The job struct */
    pid_t pid;              /* job PID */
    int jid;                /* job ID [1, 2, ...] */
    int state;              /* UNDEF, BG, FG, or ST */
    char cmdline[MAXLINE];  /* command line */
};

  关于队列的操作我们不需要关心,实验文件中已经实现好了。我们要注意的是,这个队列是全局变量,一定要注意资源冲突问题。从c语言的角度来考虑就是在访问的时候要注意屏蔽信号,站在其他高级语言的角度容易发现几个handler同时对资源进行操作和多线程类似(不完全相同),其中一个线程在访问资源的时候要“加锁”。

eval

  这个函数的实现相对简单,代码如下:

void eval(char *cmdline) 
{
    static char *argv[MAXARGS];
    int run_in_bg = parseline(cmdline, argv);
    if (argv[0] == NULL) {
        return;
    }
    static sigset_t mask_all, prev_all;
    sigemptyset(&mask_all);
    sigaddset(&mask_all, SIGCHLD);

    pid_t pid;
    if (!builtin_cmd(argv)) {
        sigprocmask(SIG_BLOCK, &mask_all, &prev_all);/* Eliminate the race.*/
        if ((pid = fork()) == 0) { /* Child runs user job.*/
            setpgid(0, 0);
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }
        /* Parent */
        int state;
        if (run_in_bg) {
            state = BG;
        } else {
            state = FG;
        }
        int jid = 0;
        sigfillset(&mask_all);
        sigprocmask(SIG_BLOCK, &mask_all, NULL);/* Eliminate the race in jobs*/

        addjob(jobs, pid, state, cmdline);
        jid = pid2jid(pid);
        sigprocmask(SIG_SETMASK, &prev_all, NULL);

        if (!run_in_bg) {
            waitfg(pid);
        } else {
            printf("[%d] (%d) %s", jid, pid, cmdline);   
        }
    }
    return;
}

  其中有几个需要注意的地方:
1.为了确保执行的顺序为“创建子进程->添加队列->删除队列”, 我们需要在创建子进程之前屏蔽SIGCHLD信号,并且在添加队列的时候屏蔽所有的信号。如果在创建子进程的时候就屏蔽所有信号会出现异常,具体原因碍于知识水平我也不清楚。
2.在创建子进程的时候需要重新设置子进程的process group pid(pgid).调用setpgid(0, 0), 将子进程的pid作为对应的pgid.
3.在操作完毕后将屏蔽信号还原。
4.cmdline自带换行符,输出时不需要再换行

builtin_cmd

int builtin_cmd(char **argv) 
{
    static sigset_t mask_all, prev_all;
    sigfillset(&mask_all);

    int i;
    if (!strcmp(argv[0], "quit")) {
        sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        for (i = 0; i < MAXJOBS; i++) {
            if (jobs[i].pid > 0) {
                kill(jobs[i].pid, SIGKILL);/* Kill all child processes.*/
            }
        }
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
        exit(0); /* Never reached.*/
    }
    else if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
        do_bgfg(argv);
        return 1;
    } else if (!strcmp(argv[0], "jobs")) {
        sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        listjobs(jobs);
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
        return 1;
    }
    return 0;     /* not a builtin command */
}

  注意点:在访问队列的时候注意屏蔽信号。

do_bgfg

void do_bgfg(char **argv) 
{
    char *number = argv[1];
    if (number == NULL) {
        printf("%s command requires PID or \%jobid argument\n", argv[0]);
        return;
    }
    if (!(number[0] == '%' || isdigit(number[0]))) {
        printf("%s: argument must be a PID or \%jobid\n", argv[0]);
        return;
    }
    int len = (int)(strlen(argv[1]));
    int id = 0;
    if (number[0] != '%') {
        id = number[0] - '0';
    }
    for (int i = 1; i < len; i++) {
        if (!isdigit(number[i]))
            return;
        else {
            id = id * 10 + number[i] - '0';
        }
    }
    struct job_t *job;
    static sigset_t  mask_all, prev_all;

    sigfillset(&mask_all);
   
    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    if (number[0] == '%') {
        job = getjobjid(jobs, id);
    } else {
        job = getjobpid(jobs, id);
    }

    if (job == NULL) {
        if (number[0] == '%') 
            printf("%s No such job\n", number);
        else
            printf("(%d) No such job\n", id);
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
        return;
    } 

    pid_t pid = job->pid;
    if (!strcmp(argv[0], "bg")) {
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
        job->state = BG;
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
        kill(-pid, SIGCONT);
    } else {
        job->state = FG;
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
        kill(-pid, SIGCONT);
        waitfg(pid);
    }
    return;
}

  注意点
1.关于字符串的各种条件判断
2.jid和pid的区别处理。
3.使用kill发送信号的时候pid前加负号,表示向这组进程发信号。
4.处理FG命令后应调用waitfg等待。

waitfg

void waitfg(pid_t pid)
{
    static sigset_t  mask_all, prev_all;

    sigfillset(&mask_all);
    while (1) {
        sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        struct job_t *job = getjobpid(jobs, pid);
        int is_fg = ((job != NULL) && (job->state == FG));
        if (!is_fg) {
            sigprocmask(SIG_SETMASK, &prev_all, NULL);
            break;
        }
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
        sleep(1);
    }
    return;
}

  注意点:
1.访问队列前屏蔽信号
2.调用sleep之前要将信号解锁,而且这里不能使用pause(),因为pause()需要信号来将其唤醒,如果信号没来就会一直阻塞。在信号屏蔽解除后,pause()执行前,可能会出现我们所期待的SIGCHLD信号,但是这个信号被handler捕获后,pause就接收不到了。

sigchld_handler

void sigchld_handler(int sig) 
{
    int olderrno = errno;

    static sigset_t mask_all, prev_all;
    sigfillset(&mask_all);

    pid_t pid; 
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        int jid = pid2jid(pid);
        if (!WIFSTOPPED(status)) {
            if (WIFSIGNALED(status)) {
                int sig = WTERMSIG(status);
                printf("Job [%d] (%d) terminated by signal %d\n"
                , jid, pid, sig
                );
            }
            deletejob(jobs, pid);
        } else {
            printf("Job [%d] (%d) stopped by signal %d\n"
            , jid, pid, SIGTSTP
            );
            getjobpid(jobs, pid)->state = ST;
        }
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
    }
    errno = olderrno;
    return;
}

  注意点:
1.按理来说在handler执行前都需要屏蔽对应的信号,但是这一点我们在代码中不需要考虑。因为在main函数中程序已经调用了Signal函数对这个功能进行了跨平台实现(potable).
2.调用waitpid的时候,我们希望同时对终止和暂时停止的程序做出非阻塞反应,所以要使用参数WHOHANG | WUNTRACED.
3.调用WIFSTOPPED宏判断是终止还是暂停,调用WIFSIGNALED判断是正常结束还是由于信号结束,调用WTERMSIG获取对应的信号.
4.按照csapp上的内容,在signal handler中不应该使用异步不安全的函数,而printf恰好就是。这点在重要的项目中一定要注意,但是在这里几乎不会有影响(16个test全部能过),而且用write写起来也很麻烦,所以我不打算改。
5.如果使用异步安全函数,要保存errno变量,我在代码中写了。

sigint_handler

void sigint_handler(int sig) 
{
    int olderrno = errno;

    static sigset_t mask_all, prev_all;
    sigfillset(&mask_all);

    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    pid_t pid = fgpid(jobs);
    sigprocmask(SIG_SETMASK, &prev_all, NULL);

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

注意点:屏蔽信号

sigtstp_handler

void sigtstp_handler(int sig) 
{
    int olderrno = errno;

    static sigset_t mask_all, prev_all;
    sigfillset(&mask_all);

    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    pid_t pid = fgpid(jobs);
    if (pid != 0) {
        kill(-pid, sig);
    }
    sigprocmask(SIG_SETMASK, &prev_all, NULL);
    waitfg(pid);

    errno = olderrno;
    return;
}

注意点:
1.屏蔽信号
2.执行顺序应为“sigtstp_handler处理暂停信号->sigchld_handler处理信号->下一条命令”,所以一定要调用waitfg进行等待,避免提前处理下一条命令。