CSAPP Shell Lab

Posted joker D888

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CSAPP Shell Lab相关的知识,希望对你有一定的参考价值。

CSAPP Shell Lab

本实验的目的是更加熟悉过程控制和信号的概念。 您将通过编写一个支持作业控制的简单 Unix shell 程序来完成此操作。

我们要做的是填写tsh.c文件的空函数,完善其内容,使其达到我们预期的目的。详细的需要实现的功能请查看实验文档。根据文档的提示,我们可以根据测试文件tracei.txt来从易到难一步一步实现相应功能,通过make testimake rtesti对比输出内容判断相应功能是否正确实现。做之前先看一遍已经给实现好的辅助函数一遍后续解题正确使用,并且要仔细看实验文档,里面有很多重要提示,最好仔细阅读过第八章内容。

关键点

以下是程序中的一些要点,一些甚至关乎着程序的正确性:

  • 实现信号处理程序时,务必将信号发送至整个进程组,即在kill函数中使用-pid而不是pid
  • 使用sigprocmask来避免deletejobaddjob出现竞争情况。做法是父进程必须在派生子进程之前使用 sigprocmask 来阻塞SIGCHLD信号,在将子进程通过addjob添加到作业列表后再次使用 sigprocmask以取消阻塞这些信号。 由于子进程继承了父进程的阻塞向量,因此子进程必须在执行新程序之前取消阻塞 SIGCHLD 信号。
  • 使用setpgid(0,0)fork的子进程创建新的进程组。确保在前台进程中只有一个进程,即我们的shelltsh进程。这样做是为避免我们所执行的作业影响我们的shell,如果我们的shell创建了一个子进程,默认情况该子进程也是前台进程组,若此时键入ctrl-c将会向前台的进程组发送SIGINT,将导致shell退出,显然不对。
  • waitpidWUNTRACEDWNOHANG选项,可避免等待任何运行中的子进程,同时可以获知子进程的终止和停止状态。
  • waitfg中使用一个围绕睡眠函数的繁忙循环。
  • 只要子进程的状态发生改变就会向父进程发送SIGCHLD信号。
  • 在使用printf等有输出缓冲区的函数往标准输出上输出后,及时使用fflush刷新缓冲区,避免出现令人意想不到的结果。
  • 阻塞所有的信号,保护对共享全局数据结构的访问。
  • 保护和恢复errno

准备工作

我是预先添加了错误处理函数,简化程序结构,如下。参考csapp官网

// 额外增加错误处理包装函数--begin
pid_t Fork(void) 
    pid_t pid;

    if ((pid = fork()) < 0) unix_error("Fork error");
    return pid;


pid_t Waitpid(pid_t pid, int *iptr, int options) 
    pid_t retpid;

    if ((retpid = waitpid(pid, iptr, options)) < 0) unix_error("Waitpid error");
    return (retpid);

void Kill(pid_t pid, int signum) 
    int rc;

    if ((rc = kill(pid, signum)) < 0) unix_error("Kill error");


void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset) 
    if (sigprocmask(how, set, oldset) < 0) unix_error("Sigprocmask error");
    return;


void Sigemptyset(sigset_t *set) 
    if (sigemptyset(set) < 0) unix_error("Sigemptyset error");
    return;


void Sigfillset(sigset_t *set) 
    if (sigfillset(set) < 0) unix_error("Sigfillset error");
    return;


void Sigaddset(sigset_t *set, int signum) 
    if (sigaddset(set, signum) < 0) unix_error("Sigaddset error");
    return;


int Sigsuspend(const sigset_t *set) 
    int rc = sigsuspend(set); /* always returns -1 */
    if (errno != EINTR) unix_error("Sigsuspend error");
    return rc;

void Setpgid(pid_t pid, pid_t pgid) 
    int rc;

    if ((rc = setpgid(pid, pgid)) < 0) unix_error("Setpgid error");
    return;

// 额外增加错误处理包装函数--end

builtin_cmd

最简单是先完成builtin_cmd,判断是否是内置命令,若是执行。

int builtin_cmd(char **argv) 
    if (!strcmp(argv[0], "quit")) exit(EXIT_SUCCESS);
    if (!strcmp(argv[0], "jobs")) 
        listjobs(jobs);
        return 1;
    
    if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) 
        do_bgfg(argv);
        return 1;
    
    return 0; /* not a builtin command */

eval

再仿照书上完成eval的内容,需要额外注意的是,如果是运行在前台的,要调用waitfg函数等待其不再执行。

void eval(char *cmdline) 
    char *agrv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    pid_t pid;

    strcpy(buf, cmdline);
    bg = parseline(buf, agrv);
    if (agrv[0] == NULL) return;  // Ignore empty lines

    if (!builtin_cmd(agrv)) 
        sigset_t mask_all, mask_one, prev_one;
        Sigfillset(&mask_all);
        Sigemptyset(&mask_one);
        Sigaddset(&mask_one, SIGCHLD);

        // 阻塞SIGCHLD信号,消除竞争,避免父进程运行到addjob之前子进程就退出
        // 在调用fork之前,阻塞SIGCHLD信号,然后再调用addjob之后取消阻塞这些信号,保证了在子进程被添加到作业列表之后回收该子进程
        Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);  // 阻塞SIGCHLD
        if ((pid = Fork()) == 0)                      // Child runs user job
            // 创建一个新的进程组,和tsh进程组分离,避免后续操作影响到tsh进程
            Setpgid(0, 0);
            Sigprocmask(SIG_SETMASK, &prev_one, NULL);  // 子进程继承父进程状态,需要恢复上面阻塞SIGCHLD信号之前的状态
            if (execve(agrv[0], agrv, environ) < 0) 
                printf("%s: Command not found\\n", agrv[0]);
                exit(EXIT_SUCCESS);
            
        
        int status = bg ? BG : FG;
        Sigprocmask(SIG_BLOCK, &mask_all, NULL);  // 阻塞全部信号
        addjob(jobs, pid, status, cmdline);
        // bg需要打印信息,也会使用全局数据,为保证安全,也需要在Sigprocmask之间,屏蔽所有信号
        if (bg) printf("[%d] (%d) %s", getjobpid(jobs, pid)->jid, pid, cmdline);
        Sigprocmask(SIG_SETMASK, &prev_one, NULL);  // 恢复阻塞SIGCHLD信号之前的状态

        if (!bg)   // No background, waiting for child process to end
            waitfg(pid);
        
    

waitfg

阻塞直到pid不再是前台进程。这里使用sigsuspend作为睡眠函数,得益于其本身的原子性,避免潜在的竞争,详细原因见8.5.7节。

void waitfg(pid_t pid) 
    sigset_t mask;
    Sigemptyset(&mask);
    while (pid == fgpid(jobs)) Sigsuspend(&mask);

sigtstp_handler

SIGTSTP信号的处理程序,捕获它并通过发送SIGTSTP挂起前台作业。

void sigtstp_handler(int sig) 
    int olderrno = errno;
    pid_t pid = fgpid(jobs);
    if (pid) Kill(-pid, sig);
    errno = olderrno;

sigint_handler

SIGINT信号的处理程序,捕获它并将其发送到前台作业。

void sigint_handler(int sig) 
    int olderrno = errno;
    pid_t pid = fgpid(jobs);
    if (pid) Kill(-pid, sig);
    errno = olderrno;

sigchld_handler

只要子进程的状态发生改变就会向父进程发送SIGCHLD信号。如终止,停止,继续,只要导致这三种状态的出现,都会触发SIGCHLD信号。下面的处理程序,只处理终止和停止的状态,继续执行的这种状态专由do_bgfg处理。检查子进程的状态由waitpid函数的statusp记录,详见8.4.3节。

void sigchld_handler(int sig) 
    pid_t pid;
    int status;

    sigset_t mask_all, prev_all;
    Sigfillset(&mask_all);  // 设置全阻塞
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    // 这里要判断>0,因为WNOHANG | WUNTRACED表示若等待集合中的子进程都没有停止或终止,则返回0,
    // 若有任一个终止或停止,返回该子进程的pid
    // 由于我们的SIGCONT信号的处理是放在do_bgfg()中的,所以有可能是SIGCONT信号导致的SIGCHLD信号而返回0
    while ((pid = Waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) 
        if (WIFEXITED(status))   // 正常终止
            deletejob(jobs, pid);
         else if (WIFSIGNALED(status))   // 信号导致终止
            printf("Job [%d] (%d) terminated by signal %d\\n", pid2jid(pid), pid, WTERMSIG(status));
            fflush(stdout);
            deletejob(jobs, pid);
         else if (WIFSTOPPED(status))   // 信号导致停止
            struct job_t *job = getjobpid(jobs, pid);
            job->state = ST;
            printf("Job [%d] (%d) stopped by signal %d\\n", job->jid, job->pid, WSTOPSIG(status));
            fflush(stdout);
        
    

    Sigprocmask(SIG_SETMASK, &prev_all, NULL);

do_bgfg

此函数则是用于处理内置命令bg fg的,程序相当一部分是关于参数判断和错误的处理。需要注意的是进程状态的改变也要相应的在jobs中改变,如果是调到前台运行的,要调用相应的waitfg函数。

void do_bgfg(char **argv) 
    // argv错误处理
    if (argv[1] == NULL) 
        printf("%s command requires PID or %%jobid argument\\n", argv[0]);
        fflush(stdout);
        return;
    

    sigset_t mask_all, prev_all;
    Sigfillset(&mask_all);

    struct job_t *job;

    if (argv[1][0] == '%')                                              // jid
        if (strspn(argv[1] + 1, "0123456789") != strlen(argv[1] + 1))   // 如果%后面的不全是数字,错误
            printf("%s: argument must be a PID or %%jobid\\n", argv[0]);
            fflush(stdout);
            return;
        
        int jid = atoi(argv[1] + 1);
        job = getjobjid(jobs, jid);
        if (job == NULL) 
            printf("%s: No such job\\n", argv[1]);
            fflush(stdout);
            return;
        
     else                                                      // pid
        if (strspn(argv[1], "0123456789") != strlen(argv[1]))   // 如果不全是数字,错误
            printf("%s: argument must be a PID or %%jobid\\n", argv[0]);
            fflush(stdout);
            return;
        
        pid_t pid = atoi(argv[1]);
        job = getjobpid(jobs, pid);
        if (job == NULL) 
            printf("(%s): No such process\\n", argv[1]);
            fflush(stdout);
            return;
        
    
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);  // 需要访问jobs全局变量,先屏蔽全部信号
    if (!strcmp(argv[0], "fg"))                   // fg,将一个停止或运行的后台作业改变为在前台运行
        if (job->state == ST)                     // 如果是停止的,发送SIGCONT信号重新启动
            Kill(-(job->pid), SIGCONT);
        
        job->state = FG;
        Sigprocmask(SIG_SETMASK, &prev_all, NULL);
        waitfg(job->pid);  // 等待前台作业运行
     else                // bg,将一个后台停止作业改变为后台运行
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
        Kill(-(job->pid), SIGCONT);
        job->state = BG;
        Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    

总结

可以通过以下命令将测试的输出内容重定向到新建的文件tsh.out,随后通过文本比较和tshref.out进行比较,检查是否除了pidps a不同,其他都和tshref.out相同,经测试,以上所编写的代码和tshref.out比较无误:

./sdriver.pl -t trace01.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace02.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace03.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace04.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace05.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace06.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace07.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace08.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace09.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace10.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace11.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace12.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace13.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace14.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace15.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace16.txt -s ./tsh -a "-p" >> tsh.out 

本实验难度适中,代码量也不大,若是认真阅读过第八章内容以及实验文档,编写起来还是比较顺利的。通过此实验,较清晰的了解了shell的工作原理,以及信号的处理,如何避免出现竞争,并发的编程规范,但以上程序中,对于getjobpid此类从job获取信息但不对其进行修改我并没有使用sigprocmask屏蔽全部信号,因为我认为,及时指令序列被处理程序中断,也不会影响其后续的结果,关于这一问题,暂存疑,若有缺陷的地方,后续将会修改。

以上是关于CSAPP Shell Lab的主要内容,如果未能解决你的问题,请参考以下文章

CSAPP Shell Lab

CSAPP Lab3: The Attack Lab

CSAPP-Lab01 Data Lab 记录

CSAPP Lab:Data Lab——第一个实验

CSAPP Lab4 Cache Lab

CSAPP Data Lab