ShellLab

01 Ctrl+d 退出

好像已经给出了

    if (feof(stdin)) { /* End of file (ctrl-d) */
      fflush(stdout);
      exit(0);
    }

02 内置 quit

在 eval 函数当中实现,要先处理输入(利用已经给出的 parseline 函数),然后执行即可。

如果先进行判断 cmdline == "quit",会导致最后有换行符的影响!

// 参数处理
  char *argv[MAXARGS]; // 存放参数
  char buf[MAXLINE];   // 存放修改后的命令行副本
  int bg;              // 是否后台运行
  pid_t pid;
  strcpy(buf, cmdline); // 拷贝备份,因为 parseline 会改变原字符串
  bg = parseline(buf, argv);
  // 02 内置quit指令,要先处理输入再判断,否则 \n 会有影响
  if (strcmp(argv[0], "quit") == 0) {
    exit(0);
  }

03 waitpid 处理前台进程

if (!bg) {
    pid = fork();
    if (pid == 0) {
      if (execve(argv[0], argv, environ) < 0) {
        fprintf(stderr, "%s command not found\n", argv[0]);
        exit(1);
      }
    } else {
      waitpid(pid, NULL, 0);
    }
  }

04 sigchld_handler 异步收割后台进程

当子进程退出后,会向父进程发出 SIGCHLD 信号,内核会强制中断父进程的工作流,跳转到 sigchld_handler 指向的处理地址,所以只需要在一开始的时候将处理地址安装好即可。

安装: Signal(SIGCHLD, sigchld_handler);

Signal 是 sigaction 的封装:

handler_t *Signal(int signum, handler_t *handler) {
  struct sigaction action, old_action;
 
  action.sa_handler = handler;
  sigemptyset(&action.sa_mask); /* block sigs of type being handled */
  action.sa_flags = SA_RESTART; /* restart syscalls if possible */
 
  if (sigaction(signum, &action, &old_action) < 0)
    unix_error("Signal error");
  return (old_action.sa_handler);
}

而 handler 的实现需要有: while + waitpid (回收子进程)+ WNOHANG,同时要注意,由于可能在任何时候打断父进程,所以需要保存 errno 变量(用于记录系统调用结果的)。

void sigchld_handler(int sig) {
  int old_errno = errno;
  int state;
  pid_t pid;
 
  while((pid = waitpid(-1, &state, WNOHANG | WUNTRACED)) > 0){
    deletejob(jobs, pid);
  }
  errno = old_errno;
}

然后在 eval 的分支中添加 addjobs 即可。

05 处理内置 jobs

现在发现,如果和以前一样,直接在 eval 当中判断内置函数已经不行了,因为原来的 quit 可以直接退出,而这里的 jobs 需要在执行完以后再继续等待。解决办法是实现 buildin_cmd 函数,如果执行了内部函数,就不需要再 fork 了。

int builtin_cmd(char **argv) {
  // 返回 1 则不继续执行
  if (strcmp(argv[0], "quit") == 0) {
    exit(0);
  }
  if (strcmp(argv[0], "jobs") == 0) {
    listjobs(jobs);
    return 1;
  }
  return 0;
}

但是,这样写是有隐患的

  • 如果父进程还没有写进 addjob 就查询,会返回 jid=0
  • 如果后台子进程运行极快,会导致在调用 addjob 之前该进程已经死亡,就会触发 deletejob,删除一个空 job.

所以,需要 sigprocmask 函数来让 addjob 成为一个不可打断的操作,让内核发出的死亡信号延缓执行

逻辑:屏蔽-fork - 子进程重置屏蔽、父进程保护 addjob - add后要恢复prev

void eval(char *cmdline) {
    // ... 解析参数等 ...
 
    sigset_t mask_all, mask_one, prev_one;
    sigfillset(&mask_all);         // 包含所有信号的集合
    sigemptyset(&mask_one);
    sigaddset(&mask_one, SIGCHLD); // 只包含 SIGCHLD 的集合
 
    /* 步骤 1:在 fork 之前屏蔽 SIGCHLD */
    // 这样子进程即使现在死了,SIGCHLD 也会被排队挂起,不会触发 handler
    sigprocmask(SIG_BLOCK, &mask_one, &prev_one); 
 
    if ((pid = fork()) == 0) {   /* 子进程 */
        /* 步骤 2:子进程继承了屏蔽位,必须在 execve 前恢复 */
        sigprocmask(SIG_SETMASK, &prev_one, NULL); 
        if (execve(argv[0], argv, environ) < 0) {
            // 报错处理...
            exit(1);
        }
    }
 
    /* 父进程 */
    /* 步骤 3:在 addjob 期间屏蔽所有信号(可选但推荐,保护全局变量 jobs) */
    sigprocmask(SIG_BLOCK, &mask_all, NULL); 
    
    // 现在 addjob 是绝对安全的,因为 handler 被堵在门外了
    addjob(jobs, pid, (bg ? BG : FG), cmdline); 
 
    /* 步骤 4:解除屏蔽,让挂起的信号“喷涌而出” */
    // 此时如果子进程早已结束,handler 会在这一行代码执行完后立即触发
    sigprocmask(SIG_SETMASK, &prev_one, NULL); 
 
    if (!bg) {
        waitpid(pid, NULL, 0); // 或者使用 waitfg
    } else {
        printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
    }
}

在写到这里的时候,发现前台的处理有问题,因为显式地调用了 waitpid,所以不会将前台的程序交给 handler 来处理,优解是将前台的 waitpid 替换为一个 waitfg 函数,只进行 sleep,以中断程序并且不抢占 waitpid:

void waitfg(pid_t pid) {
    sigset_t empty;
    sigemptyset(&empty);
    
    // 只要前台进程还在,就“挂起”等待信号
    while (fgpid(jobs) == pid) {
        // sigsuspend 会暂时清空屏蔽位并让进程休眠
        // 直到捕捉到一个信号并从 handler 返回
        sigsuspend(&empty); 
    }
}

06 SIGINT Ctrl+C 终止前台进程

由于已经安装了 sigint_handler,所以只需要进行一次信号转发,将接收到的 SIGINT 转换为传递给前台的 kill 函数。但是由于前台进程可能会 fork 出很多个进程来执行操作,所以需要用 kill(-pid,SIGINT) 来进行组 kill

void sigint_handler(int sig) {
  int old_errno = errno;
  pid_t pid = fgpid(jobs);
  if(pid != 0){
    kill(pid,SIGINT);
  }
  errno = old_errno;
}

但是这只是发送了关闭信号,题目要求关闭以后输出信息,这就需要将逻辑添加到 sigchld_handler 当中。用 status 来记录退出的信息,用 WIFSIGNALED、WIFEXITED 来判断。

void sigchld_handler(int sig) {
  int old_errno = errno;
  int status;
  pid_t pid;
 
  while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
    if(WIFSIGNALED(status)){
      // 被异常信号终止
      printf("Job [%d] (%d) terminated by signal %d\n", 
                   pid2jid(pid), pid, WTERMSIG(status));
    }
    else if(WIFEXITED(status)){
      // ...
    }
    deletejob(jobs, pid);
  }
  errno = old_errno;
}

07 SIGINT 只发送给前台

在 06 已经完成

08 SIGTSTP 只发送给前台

写到这里发现有点混淆了 sigstp_handlersigchld_handler,chld 是用来收尸的,而 sigstp 是用来发送信号的。当信号给到 shell 的时候,通过 stp 发送给前台进程,然后进程挂起,发送信号给 chld。

void sigtstp_handler(int sig) { 
  int old_errno = errno;
  pid_t pid = fgpid(jobs);
  if(pid != 0){
    kill(-pid,SIGTSTP);
  }
  errno = old_errno;
}

但是在验证的时候发现 sigstp 一直无法正常输出,最后问了 AI 才发现是在生成子进程后,没有和父进程分开 pgid,即还需要给子进程 setpgid(0, 0);,否则 C-z 会发送给所有进程,导致 shell 将自己也挂起了(因为 kill(-pid,…)) ?

09 10 bg\fg 内置指令

需要处理字符串,然后发送信号给指定的进程,注意还要修改对应的状态,如果是前台,则需要调用 waitfg

void do_bgfg(char **argv) {
  char *id_str = argv[1];
  struct job_t *job = NULL;
  if (id_str == NULL) {
    printf("%s command requires PID or %%jobid argument\n", argv[0]);
    return;
  }
  if (id_str[0] == '%') {
    int jid = atoi(&id_str[1]);
    job = getjobjid(jobs, jid);
    if (job == NULL) {
      printf("No Such job");
      return;
    }
  } else {
    int pid = atoi(id_str);
    job = getjobpid(jobs, pid);
    if (job == NULL) {
      printf("No such process");
      return;
    }
  }
  if (strcmp(argv[0], "bg") == 0) {
    kill(-(job->pid), SIGCONT);
    job->state = BG;
    printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
  }
  if (strcmp(argv[0], "fg") == 0) {
    kill(-(job->pid), SIGCONT);
    job->state = FG;
    waitfg(job->pid);
  }
}

11 12 SIGINT\SIGSTP 发送给组

上面已经实现,将 sigint_handlersigspt_handler 的 pid 改成 -pid 即可发送给所有同组进程。

13 Restart 进程组

类似,将 fg 的指令调整为 -pid 即可

14 Simple error handling

边界异常条件的处理,在 fg、bg 的输入有问题时,需要输出错误原因