CSAPP ShellLab

实验内容

实现一个简易的shell,包含以下功能:

  • 通过shell运行程序,可选前台运行或后台运行
  • 实现:jobs(打印所有前台和后台的任务信息),bg(将某一任务调整为后台运行),fg(将某一任务调整为前台运行),quit(退出shell)
  • 接收ctrl + c和ctrl + z发出的信号,并传给前台进程

整个shell的框架已经搭好,需要补齐核心的函数即可,实现时需要用到信号和进程控制。

代码

首先对一些函数进行错误处理包装,函数声明如下:

pid_t Fork();
void Kill(pid_t pid, int sig);
void Sigfillset(sigset_t* set);
void Sigemptyset(sigset_t* set);
void Sigaddset(sigset_t* set, int sig);
void Sigprocmask(int how, sigset_t* mask, sigset_t* prev);

eval

主要对命令行解析,遇到内置命令则立即执行,否则则创建子进程运行指定的程序。

void eval(char *cmdline) 
{
  char* argv[MAXARGS];
  /* paresline为提供的解析函数 */
  int bg = parseline(cmdline, argv);
  if (argv[0] == NULL) {
    return;
  }

  /* 准备在访问全局变量时信号阻塞 */
  sigset_t mask_one, mask_all, prev_one;
  Sigemptyset(&mask_one);
  Sigaddset(&mask_one, SIGCHLD);
  Sigfillset(&mask_all);
  
  /* 若不是内置命令 */
  if (!builtin_cmd(argv)) {

    /* 判断路径是否有制定的文件 */
    if (access(argv[0], F_OK) < 0) {
      printf("%s: Command not found\n", argv[0]);
      return;
    }

    Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
    pid_t pid;

    /* child */ 
    if ((pid = Fork()) == 0) {
      /* 子进程要立刻解除阻塞 */
      Sigprocmask(SIG_SETMASK, &prev_one, NULL);
      execve(argv[0], argv, environ);
    }

    /* 每个子进程单独一个进程组 */
    setpgid(pid, pid);

    Sigprocmask(SIG_BLOCK, &mask_all, NULL);
    addjob(jobs, pid, (bg == 0 ? FG : BG), cmdline);
    if (bg == 1) {
      printf("[%d] (%d) %s", getjobpid(jobs, pid)->jid, pid, cmdline);
    }
    Sigprocmask(SIG_SETMASK, &prev_one, NULL);

    /* 若为前台任务则waitfg中会等待任务执行 */
    waitfg(pid);
  }
  return;
}

waitfg

对于pid进程,如果在jobs中是前台任务,则等待其停止执行(可能执行完,也可能被信号终止或停止)。

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

  Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
  struct job_t* job = getjobpid(jobs, pid);
  while (job->pid == pid && job->state == FG) {
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    sleep(1);
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
  }
  Sigprocmask(SIG_SETMASK, &prev_all, NULL);

}

sigint_handler

处理父进程收到的SIGINT信号,需要将该信号发送给前台任务进程。

void sigint_handler(int sig) {
  int olderrno = errno;
  sigset_t mask_all, prev_all;
  Sigfillset(&mask_all);

  Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
  /* 找出前台任务的pid */
  pid_t pid = fgpid(jobs);
  Sigprocmask(SIG_SETMASK, &prev_all, NULL);

  if (pid > 0) {
    /* -pid表示发给pid所在的进程组(实验要求发给整个进程组) */
    Kill(-pid, SIGINT);
  }
  errno = olderrno;
}

sigstp_handler

处理父进程收到的SIGTSTP信号,需要将该信号发送给前台任务进程,实现与上相同。

sigchld_handler

当父进程shell收到子进程发出的SIGCHLD后需要对之进行处理,在回收进程时,需要根据status对不同状态的进程进行不同的处理。

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

  sigset_t mask_all, prev_all;
  Sigfillset(&mask_all);

  Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
  /* 回收子进程 */
  while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
    struct job_t* job = getjobpid(jobs, pid);
    
    if (WIFEXITED(status)) {
      /* 通过exit或return正常结束的进程 */
      deletejob(jobs, pid);
    } else if (WIFSIGNALED(status)) {
      /* 被信号终止的进程 */
      printf("Job (%d) terminated by siganl %d\n", pid, WTERMSIG(status));
      deletejob(jobs, pid);
    } else if (WIFSTOPPED(status)) {
      /* 被信号停止的进程 */
      printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(status));
      job->state = ST;
    } else if (WIFCONTINUED(status)) {
      /* ... */
    }
  }
  Sigprocmask(SIG_SETMASK, &prev_all, NULL);

  errno = olderrno;
}

builtin_cmd

主要为对内置命令的处理,分为quit,jobs,fg和bg,如果是内置命令,则返回0,否则返回1。

int builtin_cmd(char **argv) 
{
  if (!strcmp(argv[0], "quit")) {
    exit(1);
  }
  if (!strcmp(argv[0], "jobs")) {
    listjobs(jobs);
    return 1;
  }
  if ((!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg"))) {
    if (argv[1] != NULL) {
      do_bgfg(argv);
    } else {
      printf("%s ", argv[0]);
      printf("command requires PID or %%jobid argument\n");
    }
    return 1;
  }
  return 0;     /* not a builtin command */
}

do_bgfg

执行bg或者fg命令,向对应的进程发送一个SIGCONT信号即可,需要注意的错误处理,判断给出的命令是否正确。

void do_bgfg(char **argv) {
  char* buf = argv[1];
  int err = 0;
  struct job_t* job;

  sigset_t mask_all, prev_all;
  Sigfillset(&mask_all);

  Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
  /* 判断给出的是%jid还是pid */
  if (buf[0] == '%') {
    int jid;
    if ((jid = atoi(++buf)) > 0) {
      job = getjobjid(jobs, jid);
    } else {
      err = 1;
    }
  } else {
    int pid;
    if ((pid = atoi(buf)) > 0) {
      job = getjobpid(jobs, pid);
    } else {
      err = 1;
    }
  }
  Sigprocmask(SIG_SETMASK, &prev_all, NULL);

  /* 给出的id有非数字字符 */
  if (err == 1) {
    printf("%s: ",argv[0]);
    printf("argument must be a PID or %%jobid\n");
    return;
  }

  /* 给出的id合法,但是并不在jobs中 */
  if (job == NULL) {
    printf("(%s): No such job\n", argv[1]);
    return;
  }

  Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
  if ((job->state = (!strcmp(argv[0], "fg")) ? FG : BG) == BG) {
    printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
  }
  Sigprocmask(SIG_SETMASK, &prev_all, NULL);
   
  /* 给目标任务进程发送SIGCONT信号,让其继续执行 */
  Kill(-job->pid, SIGCONT);
  waitfg(job->pid);
}

问题记录

在写SIGTSTP信号处理的时候遇到了一个问题,想了挺久,当时的代码如下:

void sigtstp_handler(int sig) {
  int olderrno = errno;
  sigset_t mask_all, prev_all;
  Sigfillset(&mask_all);

  Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
  pid_t pid = fgpid(jobs);
  if (pid > 0) {
    struct job_t* job = getjobpid(jobs, pid);
    job->state = ST;
    Kill(-pid, SIGTSTP);
  }
  Sigprocmask(SIG_SETMASK, &prev_all, NULL);
  errno = olderrno;
}

在测试时,当父进程接收SIGTSTP并发送给子进程后,父进程没有打印出"job is stopped..."信息,测试程序就结束了。原因在于,我直接在父进程的SIGTSTP信号处理中把子进程任务状态给修改了,而一旦前台状态被修改,waitfg就不会再等待这个任务。

父进程与子进程的执行顺序是不确定的,如果处理器先执行的是父进程,那么有可能waitfg已经退出了,但是子进程连SIGTSTP的处理程序都还没执行,也就更不可能给父进程发送SIGCHLD信号了。这就导致父进程waitfg退出后的一小段时间内,测试程序将结束测试。然而此时子进程还没有把SIGCHLD信号发送给父进程,所以父进程就不能在SIGCHLD处理程序中打印相关的信息,也就是少了一个"job is stopped..."的信息。

解决方法很简单,父进程不要在SIGTSTP处理中修改子进程状态,只在SIGCHLD处理中修改子进程状态即可,这就能保证在SIGCHLD处理中子进程状态修改、打印相关信息后,waitfg才会退出循环。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值