接下来一口气读完了整本书,然后开始按顺序解决剩下三个lab
Shell lab要求我们实现一个shell程序,整体来说还是比较简单的,不过看到自己实现的一个shell运行起来,还是十分令人激动的。

Shell lab 简介

这个lab要求我们实现一个简易的unix shell程序,内容依托CSAPP第八章的异常控制流,主要来练习信号量的处理问题,在完成之后对这一章的理解可以说是一个很大的提升。

所给的文件中已经有了大致的框架,我们要做的就是完成一下几个函数:

函数原型 作用
void eval(char *cmdline) 分析命令,并派生子进程执行
int builtin_cmd(char **argv) 执行内建的命令
void do_bgfg(char **argv) 执行bgfg命令
void waitfg(pid_t pid) 阻塞知道一个进程不在前台运行
void sigchld_handler(int sig) SIGCHID信号处理函数
void sigint_handler(int sig) SIGINT信号处理函数
void sigtstp_handler(int sig) SIGTSTP信号处理函数

文件说明

github地址:Shell Lab

  • tsh.c:我们要写代码的文件
  • tsh:我们的shell
  • tshref:标准实现,给我们拿来对比从而帮助debug
  • traceXX:这里是16个测试文件,分别一步步帮助我们实现完整的功能

简易在写的时候,不要直接按照trace来一个个完成,应该先去写好一个大致的样子,再来慢慢用trace来debug

我们的shell只需要实现:

  • 能执行可执行文件
  • 简单的几个内建命令
    1. quit:退出shell
    2. jobs:列出任务
    3. bg <job>:启动一个暂停的任务,并让其在后台运行
    4. fg <job>:启动一个暂停的任务,并让其在前台运行
  • 上述命令的 <job> 要求可以是jid或者pid
  • 支持 Ctrl + C 与 Ctrl + Z
  • 有完善的输入错误提示

tsh.c 解析

首先我们需要分析tsh.c这个文件,然后才能明确我们应该怎么搞

函数原型 作用
int main(int argc, char **argv) 主函数,我们只需要看到eval来执行命令行就行
parseline 解析命令行,我们注意一下返回值就行
void initjobs(struct job_t *jobs) 初始化任务队列
void clearjob(struct job_t *job) 清空任务队列
int maxjid(struct job_t *jobs) 获得最大的jid
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline) 添加任务
int deletejob(struct job_t *jobs, pid_t pid) 删除任务
pid_t fgpid(struct job_t *jobs) 获得前台运行的pid
struct job_t *getjobpid(struct job_t *jobs, pid_t pid) 通过pid获得job的结构体
struct job_t *getjobjid(struct job_t *jobs, int jid) 通过jid获得job的结构体
int pid2jid(pid_t pid) pid转jid
void listjobs(struct job_t *jobs) 列出任务

文件中给出了大量的处理job的函数,我们要在不出现偏差的情况下灵活使用这些函数。

一些建议与入手思路

在熟悉书本之后,我们应该是可以比较容易的入手的。

  1. 首先在eval中要注意同步流的问题,避免竞争带来的问题(如子进程在addjob之前就结束了),需要在调用fork之前要注意阻塞SIGCHLD信号,addjob之后再取消阻塞。不过,子进程也继承了父进程的被阻塞集合,我们需要在调用execve之前取消阻塞。
  2. bg用来表明是否是前台任务,是前台任务的话waitfg用来阻塞主进程。这个阻塞我们可以通过循环判断前台是否存在任务,然后手动sleep来阻塞。
  3. 三个处理函数中sigchld函数稍微复杂些,对于三种情况(1.已经停止的进程。2.没有捕捉到的信号。3.正常结束的程序。)第一种一定要注意判断state,如果就是ST的话是没必要再处理的。其他两种情况就没什么难度,判断的宏参考CSAPP书本。
  4. 在每个信号处理函数中都应该阻塞所有的信号。
  5. 在do_bgfg中处理好错误提示。
  6. 合理使用一些包装函数,这一点书上已经有说明。

具体的细节就不再多说了,这个lab不难,就是要对书本做到完全理解,明确各个部分的作用就好。

我的解答

几个简单的包装函数

/*
 * Fork - A wrapper function of fork
 */
pid_t Fork(void) {
  pid_t pid;
  if ((pid = fork()) < 0)
	  unix_error("Fork error");
  return pid;
}

/*
 * Sigfillset - A wrapper function of sigfillset
 */
int Sigfillset(sigset_t *__set) {
  int ret;
  if ((ret = sigfillset(__set)) < 0)
    unix_error("Sigfillset error");
  return ret;
}

实验要求的函数:

/*
 * 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];
  char buf[MAXLINE];
  int bg;
  pid_t pid;
  sigset_t mask_all, mask_one, prev_one;

  /* set mask */
  Sigfillset(&mask_all);
  Sigemptyset(&mask_one);
  Sigaddset(&mask_one, SIGCHLD);

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

  if (argv[0] == NULL)
    return ;

  if (!builtin_cmd(argv)) {
    Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
    if ((pid = Fork()) == 0) {
      setpgid(0, 0);
      /* unblock before execve in child pid */
      Sigprocmask(SIG_UNBLOCK, &prev_one, NULL);
      if (execve(argv[0], argv, environ) < 0) {
        printf("tsh: command not found: %s\n", argv[0]);
        exit(0);
      }
    }

    /* add job */
    Sigprocmask(SIG_BLOCK, &mask_all, NULL);
    addjob(jobs, pid, bg + 1, cmdline);
    Sigprocmask(SIG_SETMASK, &prev_one, NULL);

    /* fg pid */
    if (!bg)
      waitfg(pid);
    /* bg pid */
    else
      printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
  }

  return ; 
}

/*
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.
 */
int builtin_cmd(char **argv) {
  if (!strcmp(argv[0], "quit"))
    exit(0);
  if (!strcmp(argv[0], "jobs")) {
    listjobs(jobs);
    return 1;
  }
  if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
    do_bgfg(argv);
    return 1;
  }
  if (!strcmp(argv[0], "&"))
    return 1;
  return 0; /* not a builtin command */ 
}

/*
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) {
  struct job_t *job;
  int jid;
  pid_t pid;
  char cmd[3];
  if (!strcmp(argv[0], "bg"))
    strcpy(cmd, "bg");
  else
    strcpy(cmd, "fg");

  /* settle errors */
  if (argv[1] == NULL) {
    printf("%s command requires PID or %%jobid argument\n", cmd);
    return ;
  }
  if (argv[1][0] == '%') {
    jid = atoi(argv[1] + 1);
    if ((job = getjobjid(jobs, jid)) == NULL) {
      printf("%s: No such job\n", argv[1]);
      return ;
    }
  }
  else if (argv[1][0] > '0' && argv[1][0] <= '9') {
    pid = atoi(argv[1]);
    if ((job = getjobpid(jobs, pid)) == NULL) {
      printf("(%d) No such job\n", pid);
      return ;
    }
  }
  else {
    printf("%s: argument must be a PID or %%jobid\n", cmd);
    return ;
  }

  /*  Change a stopped background job to a running background job */
  if (!strcmp(argv[0], "bg")) {
    job->state = BG;
    kill(-job->pid, SIGCONT);
    printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
  }
  /* Change a stopped or running background job to a running in the foreground */
  else {
    job->state = FG;
    kill(-job->pid, SIGCONT);
    waitfg(job->pid);
  }
  return ;
}

/*
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid) {
  while(pid == fgpid(jobs)) {
    usleep(1000);
  }
  return ;
}

/*
 * 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;
  int status;
  sigset_t mask_all, prev_all;
  pid_t pid;

  Sigfillset(&mask_all);
  while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
    /* pid has stopped */
    if (WIFSTOPPED(status)) {
      int jid = pid2jid(pid);
      if (jid != 0 && getjobjid(jobs, jid)->state != ST) {
        printf("job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status));
        getjobjid(jobs, jid)->state = ST;
      }
    }
    /* uncaptured signal */
    else if (WIFSIGNALED(status)) {
      int jid = pid2jid(pid);
      if (jid != 0) {
        printf("job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
        Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        deletejob(jobs, pid);
        Sigprocmask(SIG_SETMASK, &prev_all, NULL);
        kill(-pid, WSTOPSIG(status));
      }

    }
    /* normal excited */
    else if (WIFEXITED(status)) {
      Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
      deletejob(jobs, pid);
      Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    }
  }

  errno = olderrno;
	return;
}

/*
 * 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_all, prev_all;  
  pid_t pid = fgpid(jobs);

  Sigfillset(&mask_all);
  int jid = pid2jid(pid);
  if (jid != 0) {
    printf("job [%d] (%d) terminated by signal %d\n", jid, pid, sig);
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    deletejob(jobs, pid);
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    kill(-pid, sig);
  }

  errno = olderrno;
  return ;
}

/*
 * 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_all, prev_all;  
  pid_t pid = fgpid(jobs);

  Sigfillset(&mask_all);
  int jid = pid2jid(pid);
  if (jid != 0 && getjobjid(jobs, jid)->state != ST) {
    printf("job [%d] (%d) stopped by signal %d\n", jid, pid, sig);
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    getjobjid(jobs, jid)->state = ST;
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    kill(-pid, sig);
  }

  errno = olderrno;
  return;
}

总结

shell lab在熟悉课本之后就写的很快了,这次的收获,不仅熟练了信号量的处理,同时也获得了一个shell的框架,以后可以用了!