CSAPP - Shell Lab

写一个模拟Unix Shell的Tiny Shell程序

包括内置命令的处理、信号处理、作业管理的技术

一些用于字符串处理的C函数

char *strchr( const char *str, int ch );

返回ch在str中第一次出现的位置,如果没有,返回NULL

注意在原始程序中注册了SIGINT信号的处理,需要实现,否则用ctrl+c停止不了(只能用kill -9)

信号

  • SIGCONT:继续进程如果该进程处于停止状态
  • SIGTSTP:来自终端的停止信号
  • SIGINT:来自键盘的中断(ctrl+c)

内核为每个进程再pending位向量中维护着待处理信号的集合,blocked位向量中维护着被阻塞的信号集合。只要传送了一个类型为k的信号,内核就会设置pending中第k位,而只要接受了一个类型为k的信号,内核就会清楚pending中的第k位。

当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未被阻塞的待处理信号的集合(pending & ~blocked)。如果该集合非空,内核会选择集合中的某个信号k(通常是最小的k),并且强制p接受信号k。

进程同步

由于tsh中的子进程和父进程是并发执行的,每次增加一个后台作业,需要addjob,为了防止addjob在deletejob之后运行,需要通过设置blocked位向量对信号的接收进行同步(sigprocmask)。

利用显式的阻塞信号(SIGCHLD),实现原子操作(增减任务列表)

ctrl+c杀死前台进程

为了让SIGINT信号只送到前台进程(组),需要在fork后设置子进程的进程组ID,用setpgid(0, 0),使用当前进程(fork出来的子进程)的pid作为进程组ID;之后就能用 kill(-pid, SIGINT) 发送给进程组|-pid|(pid的绝对值)中的每个进程。

waitpid行为

pid_t waitpid(pid_t pid, int *statusp, int options); // 通过options修改默认行为

默认行为:挂起调用进程,直到有子进程终止;

WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。

WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。

前台进程和后台进程

shell需要等待前台进程的结束(调用waitfg()),而后台子进程被创建后父进程直接返回。

一些坑

在waitfg函数中,最好使用忙循环(busy loop)来检查是否jobs中还有需要等待的前台进程,由于在SIGCHLD信号的处理函数中已经调用了waitpid处理,不能再在父进程的waitfg函数中调用waitpid了否则会:

➜  shlab-handout git:(master) ✗ ./tsh
tsh> ./myspin 3
waitfg: waitpid error: No child processes

下面展示最终实现的函数,在写的时候可以参考tshref.out文件的输出。

eval

负责创建子进程,增加jobs列表,解析命令行

void eval(char *cmdline) 
{
    char *argv[MAXARGS];
    int bg;
    pid_t pid = 0;  // process id

    sigset_t mask_all, mask_one, prev_one;
    sigfillset(&mask_all);
    sigemptyset(&mask_one);
    sigaddset(&mask_one, SIGCHLD);

    bg = parseline(cmdline, argv);
    if (argv[0] == NULL) {
        return; // ignore the empty lines
    }

    if (!builtin_cmd(argv)) {
        sigprocmask(SIG_BLOCK, &mask_one, &prev_one); // Block SIGCHLD
        if ((pid = fork()) == 0) {  // child runs user job
            setpgid(0, 0); // 使用当前进程(fork出来的子进程)的pid作为进程组ID
            sigprocmask(SIG_SETMASK, &prev_one, NULL); // Unblock SIGCHLD 子进程会继承父进程的屏蔽信号集,所以取消子进程对SIGCHLD的屏蔽
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        addjob(jobs, pid, bg == 1 ? BG : FG, cmdline);  // 成功地加入job后,再开始监听SIGCHLD信号
        sigprocmask(SIG_SETMASK, &prev_one, NULL); // Unblock SIGCHLD

        if (!bg) { // parent wait for foreground job
            waitfg(pid);
        } else {
            printf("[%d] (%d) %s", maxjid(jobs), pid, cmdline);
        }
    }
}

buildin_cmd

对于tsh的内置命令,直接执行

int builtin_cmd(char **argv) 
{
    if (!strcmp(argv[0], "jobs")) {
        // listing the running and stopping background jobs
        Dprint("Listing jobs...\n");
        listjobs(jobs);
        return 1;
    }
    if (!strcmp(argv[0], "quit")) {
        exit(0);
    }
    if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
        do_bgfg(argv);
        return 1;
    }
    if (!strcmp(argv[0], "kill")) {
        // Terminate a job
        pid_t pid = 0;
        int jid = 0;
        if (argv[1][0] == '%') {
            jid = atoi(argv[1]+1);
        } else {
            pid = atoi(argv[1]);
            jid = pid2jid(pid);
        }
        Dprintf("kill the job:pid = %d, jid = %d\n", pid, jid);
        struct job_t *job = getjobjid(jobs, jid);
        if (!job)
            return 1;
        kill(job->pid, SIGKILL);
        deletejob(jobs, pid);
        return 1;
    }
    return 0;     /* not a builtin command */
}

do_bgfg

使得暂停的进程继续执行,如果是指定为fg,tsh等待这个任务的执行

void do_bgfg(char **argv) 
{
    pid_t pid = 0;
    int jid = 0;
    int input_is_jid = 0;
    if (argv[1] == NULL) {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
    if (argv[1][0] == '%') {
        for (int i = 1; i < strlen(argv[1]); ++i) {
            if (!isdigit(argv[1][i])) {
                printf("%s: argument must be a PID or %%jobid\n", argv[0]);
                return;
            }
        }
        jid = atoi(argv[1]+1);
        input_is_jid = 1;
    } else {
        for (int i = 0; i < strlen(argv[1]); ++i) {
            if (!isdigit(argv[1][i])) {
                printf("%s: argument must be a PID or %%jobid\n", argv[0]);
                return;
            }
        }
        pid = atoi(argv[1]);
        jid = pid2jid(pid);
    }
    struct job_t* job = NULL;
    if (!(job = getjobjid(jobs, jid))) {
        if (input_is_jid) {
            printf("%s: No such job\n", argv[1]);
        } else {
            printf("(%s): No such process\n", argv[1]);
        }
        return;
    }
    Dprintf("Continue: pid = %d, jid = %d\n", job->pid, job->jid);
    // 发送信号使得进程继续运行
    kill(-job->pid, SIGCONT);
    if (!strcmp(argv[0], "fg")) {
        // change a stopped or running background job to a running in the foreground
        job->state = FG;
        waitfg(job->pid);
    } else { // bg
        // Change a stopped background job to a running background job.
        job->state = BG;
        printf("[%d] (%d) %s", jid, pid, job->cmdline);
    }
}

waitfg

tsh等待前台进程的终止或停止

void waitfg(pid_t pid)
{
//    int olderrno = errno;
//    int status;
//    if (waitpid(pid, &status, WUNTRACED) < 0) {
//        unix_error("waitfg: waitpid error");
//    }
//
//    if (errno != ECHILD)
//        unix_error("waitpid error");
//    errno = olderrno;
    while( pid == fgpid(jobs) ) {
        sleep(1);
    }
}

三个信号的处理

sigchld_handler

void sigchld_handler(int sig) 
{
//    int olderrno = errno;
    pid_t pid;
    int status;

    while ((pid = waitpid(-1, &status, WUNTRACED|WNOHANG)) > 0) { // 解决信号不排队的问题(即多个子进程同时结束),不等待后台进程
        if (WIFEXITED(status)) {
            // 子进程正常终止
            deletejob(jobs, pid);
            Dprint("Handler reaped child\n");
        } else if (WIFSIGNALED(status)) {
            // 子进程被信号终止
            printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
            deletejob(jobs, pid);
        } else if (WIFSTOPPED(status)) {
            // 子进程停止
            printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
            struct job_t* job = getjobpid(jobs, pid);
            if (job) {
                job->state = ST;
            }
            return;
        } else {
            Dprintf("child %d terminated abnormally\n", pid);
        }
    }
//    if (errno != ECHILD)
//        unix_error("waitpid error");
//    errno = olderrno;
}

sigint_handler

void sigint_handler(int sig) 
{
    Dprint("ctrl-c pressed.\n");
    pid_t fg = fgpid(jobs);
    if (fg == 0) {
        Dprint("No fg process.\n");
        return;
    } else {
        kill(-fg, sig); // pid = -fg, 发送给进程组|pid|(pid的绝对值)中的每个进程
    }
}

sigtstp_handler

void sigtstp_handler(int sig) 
{
    Dprint("ctrl-z pressed.\n");
    pid_t fg = fgpid(jobs);
    if (fg == 0 ) {
        return;
    } else {
        kill(-fg, sig);
        struct job_t* job = getjobpid(jobs, fg);
        job->state = ST;
    }
}