返回博客

CSAPP - Shell Lab

目录
目录

实现一个专属自己的 shell 终端,听上去就很有趣🤔。

这是 HNU 计算机系统的最后一个课程实验,让大家自己实现一个 shell,在 Linux 折腾太久了,感觉这也是一个很好的去了解终端指令们的机会……

按照助教给的要求,我需要做这些事情:

你要实现的重要函数列出如下:
eval 主例程,用以分析和解释命令行(好消息:该函数原型在教材一第8章8.4节中可以找到!);
builtin_cmd 执行bg和fg内置命令;
waitfg 等待前台任务执行;
sigchld_handler 响应处理SIGCHILD信号
sigint_handler 响应处理SIGINT(ctrl-c)信号
sigtstp_handler 相应处理SIGSTP(ctrl-z)信号
do_bgfg (这个助教忘记说了!我补充一下) 处理 bg <job>fg <job> 两个命令。

其实就是根据给出的 tsh.c,补充下述的几个空函数了:

 
/* 
 * 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) 
{
    return;
}

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
    return 0;     /* not a builtin command */
}

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    return;
}

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    return;
}

/*****************
 * Signal handlers
 *****************/

/* 
 * 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) 
{
    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) 
{
    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) 
{
    return;
}

/*********************
 * End signal handlers
 *********************/

然后参考官方文档 shlab-overview.pdf 来进行填空~


先进行总体分析,我们要做的「Shell Lab」本质上其实就是写一个程序,职责是进行任务管理的任务输入输出循环。
用户操作序列 belike:

tsh> ./myspin 100        # 前台跑 myspin,shell 卡在 waitfg 等它
# (按 Ctrl-Z)            # sigtstp_handler 转发 → myspin 停止 → sigchld 把它 state 改成 ST
tsh> fg %1               # 现在能输入了!因为前台空了

我们要做的事情不过就是通过代码控制任务执行的先后顺序而已,维护一个存放任务的数据库,然后调用能调用的函数来操纵这个数据库,且我们每一个函数的职责如图:
image.png


eval && builtin_cmd

根据提示,其实可以在 CSAPP 第八章 「8.4.6 利用 fork 和 execve 运行程序」的图 8-24 找到两个函数的参考代码:

void eval(char *cmdline) //ps:手敲一下代码,感觉很舒适。😂
{
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    pid_t pid;

    strcpy(buf,cmdline);
    bg=parseline(buf,argv);
    if (argv[0]==NULL) return;
    if(!builtin_cmd(argv)){
        if ((pid=fork())==0) {
            if (execve(argv[0],argv,environ)<0){
                printf("%s: Command not found.\n",argv[0]);
                exit(0);
            }
        }
    }

    if(!bg){
        int status;
        if(waitpid(pid,&status,0)<0) unix_error("waitfg: waitpid error");
        else printf("%d %s",pid,cmdline);
    }
    return;
}
int builtin_command(char **argv) 
{
    if(!strcmp(argv[0],"quit")) exit(0);
    if(!strcmp(argv[0],"&")) return 1;
    return 0;     /* not a builtin command */
}

但其实丢过去分析后,发现其实不能完全照搬。
关于 builtin_cmd 函数,上面有一个在 eval 函数的注释,参考给出的 PDF 文件 shlab-overview.pdf

If the user has requested a built-in command (quit, jobs, bg or fg)

  • then execute it immediately.
  • builtin_cmd: Recognizes and interprets the built-in commands: quit, fg, bg, and jobs.

可以知道内置命令一共四个:「quit、jobs、bg、fg」,builtin_cmd 的职责就是认出这四个并处理。而书上的 builtin_command,只有两种情况,所以要加上去,参考:

bg和fg决定任务是前台还是后台,jobs就是用于列表

加上给出的注释:return 0 旁边写着 "not a builtin command"(不是内置命令)。这就约定了:

int builtin_cmd(char **argv) 
{
    if(!strcmp(argv[0],"quit")) exit(0); // 这个就是 kill
    //if(!strcmp(argv[0],"&")) return 1; //其实这一行可以不要,因为不会触发,加了也没事,后面的parseline 已经帮我们处理了输入符号的情况
    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 函数:

① 缺 addjob(最关键)
你 fork 出子进程后,从来没把它登记到 jobs 链表。结果 jobs 命令列不出任何东西,以后信号也找不到任务。
→ 父进程拿到 pid 后,要 addjob(jobs, pid, 状态, cmdline)。状态是 FG 还是 BG 看 bg 变量。

② 子进程缺 setpgid(0, 0)
第 177-182 行的子进程分支里,execve 之前要加一句 setpgid(0, 0),把子进程放进新进程组(否则将来 Ctrl-C 会连 shell 一起杀)。

③ 缺 sigprocmask 防竞态
fork 之前要阻塞 SIGCHLD,addjob 之后再解除。防止子进程结束太快、SIGCHLD 比 addjob 先到导致的 bug。(现在还没写 sigchld_handler,暂时看不出问题,但必须现在就加好。)

不过我丢过去分析等时候会一脸懵逼,哪里来的 addjob 函数,为什么还要 setpgid、sigprocmask 这些鬼东西?
然后回头看了眼 tsh.c,发现上面提示着:

/* Function prototypes */

/* Here are the functions that you will implement */
void eval(char *cmdline);
int builtin_cmd(char **argv);
void do_bgfg(char **argv);
void waitfg(pid_t pid);

void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);

/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv); 
void sigquit_handler(int sig);

void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs); 
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);
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);
struct job_t *getjobjid(struct job_t *jobs, int jid); 
int pid2jid(pid_t pid); 
void listjobs(struct job_t *jobs);

void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);

啊,原来是已经给了我的函数了,那么我就要好好地用上,跟个选词填空一样😂。且参考 PDF 里面有 Hints:

waitpid、kill、fork、execve、setpgid、sigprocmask 这几个函数会非常有用。waitpid 的 WUNTRACED 和 WNOHANG 选项也会很有用。

在 eval 里,父进程必须在 fork 子进程之前用 sigprocmask 阻塞 SIGCHLD 信号,然后在调用 addjob把子进程加入任务列表之后再解除阻塞。由于子进程会继承父进程的阻塞集合,子进程在 exec 新程序之前必须解除 SIGCHLD 的阻塞。

在 fork 之后、execve 之前,子进程应调用 setpgid(0, 0),把子进程放进一个新的进程组(组 ID = 子进程的PID)。这保证前台进程组里只有一个进程——你的 shell。当你按 Ctrl-C,shell 应捕获 SIGINT,然后把它转发给相应的前台任务(更准确说,是包含前台任务的进程组)。

那我们就愉快补全代码了,其实在原来基础上补充一个建立进程的逻辑就可以,先补一个 AI 加的逻辑,怎么理解我们之后再看:

void eval(char *cmdline) 
{
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    pid_t pid;
    sigset_t mask,prev; //信号集

    strcpy(buf,cmdline);
    bg=parseline(buf,argv);
    if (argv[0]==NULL) return;
    if(!builtin_cmd(argv)){
        sigemptyset(&mask); //准备mask
        sigaddset(&mask,SIGCHLD); 
        sigprocmask(SIG_BLOCK,&mask,&prev); // how,set,oldset 的输入顺序开阻塞

        if ((pid=fork())==0) { //开子进程
            sigprocmask(SIG_SETMASK,&prev,NULL); //子进程解除信号阻塞
            setpgid(0,0); //创建新进程组

            if (execve(argv[0],argv,environ)<0){
                printf("%s: Command not found\n",argv[0]);
                exit(0);
            }
        }
        addjob(jobs,pid,bg?BG:FG,cmdline); //登记父进程的任务
        sigprocmask(SIG_SETMASK,&prev,NULL); //解除阻塞
        if(!bg) waitfg(pid); //前台等待
    	else printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline); //参考所需输出~
    }
    return;
}

waitfg && sigchld_handler

在前面的 eval 函数里可以看到有一行 if(!bg) waitfg(pid); //前台等待 ,那我们要继续跑 trace 的话,只好先把 waitfg 给填上。

void waitfg(pid_t pid)
{
    while (fgpid(jobs) == pid) sleep(1);
    return;
}

因为 Hints ⑤ 说的「在 waitfg 里用一个绕着 sleep 的忙等循环」,所以我们直接用 while 循环来一直等待了,只要 pid 一直在前台就一直等。

这个任务的难点之一是决定 waitfg 和 sigchld_handler 之间如何分工。我们推荐:1.在 waitfg 里,用一个绕着 sleep 的忙等循环(busy loop)。2.在 sigchld_handler 里,只用一次 waitpid 调用。

然后看了看,能跑一些测试了,尝试 make 然后跑 trace 01-04 的时候,顺利在 trace 04 卡住了……
排查原因:

  1. 你的 shell 把 /bin/echo 当前台任务,fork+exec,然后 waitfg(pid) 等它
  2. echo 瞬间打印完就退出了 → 变成僵尸进程(就是快照里的 echo <defunct>
  3. 但是 sigchld_handler 是空的,没人去 deletejob 把 echo 从任务表删掉
  4. 于是 waitfg fgpid(jobs) == pid 永远成立 → 死循环,shell 永远卡在waitfg

在刚才的 eval 里我们是通过 SIGCHLD 来开阻塞的,现在我们没写 SIGCHLD 的处理逻辑,就必然卡住啦😂。

SIGCHLD handler 存在的根本理由是:子进程是异步结束的,shell 不知道它们啥时候结束。
想想后台任务 ./myspin 10 &:你 shell 启动它之后就回到主循环继续接收用户命令了,不会守着它。10 秒后它结束了——这时候谁来把它从 jobs 数据库里删掉?谁来回收这个僵尸进程?

只能靠信号。 子进程一结束,内核就发 SIGCHLD 通知 shell。handler 就是那个"接到通知、去更新数据库、回收僵尸"的人。没有 handler,后台任务结束后就永远留在数据库里,还变成僵尸进程堆积。

所以不管 eval 阻不阻塞,handler 都是必须的。它的存在和阻塞无关。
恰恰是因为我有了这个 handler,它会异步地、在任何时刻打断主程序去调 deletejob,才引出了一个新麻烦,竞态条件

  eval 里:  fork() 出子进程

     (如果子进程跑得飞快,瞬间就结束了)

     内核发 SIGCHLD → handler 抢先运行 → deletejob(还没加进去的 job!)

     eval 才慢悠悠执行 addjob → 加了个永远删不掉的幽灵 job

  阻塞 SIGCHLD 就是为了堵住这个时间窗口:在 eval 里"fork 到 addjob"这段期间先把 SIGCHLD 阻塞,保证 addjob 一定在 deletejob 之前发生,加完了再放开。
  

那我们的 handler 的逻辑就很清晰了,要做的就是发信号,在什么时候发信号:当子进程结束或停止时,内核给 shell 发 SIGCHLD。


回到最开始那张图:当子进程结束或停止时,内核给 shell 发 SIGCHLD。这个 handler 的任务就是根据子进程发生了什么,去更新 jobs 数据库:

子进程发生了什么handler 要做的事情
正常结束从数据库删除(deletejob)
被信号杀死(比如 Ctrl-C 的 SIGINT)打印一行消息 + 删除
被信号停止(比如 Ctrl-Z 的 SIGTSTP)改状态为 ST,打印一行消息(不删,因为还能恢复)

然后需要一些宏,用于调用状态来打印:

含义配套取值宏
WIFEXITED(status)是不是正常 exit 结束的WEXITSTATUS(status) 取出退出码(如 exit(0) 中的 0)
WIFSIGNALED(status)是不是被信号杀死的WTERMSIG(status) 取出是哪个信号导致的(如 SIGINT)
WIFSTOPPED(status)是不是被信号停止的WSTOPSIG(status) 取出是哪个信号暂停的(如 SIGTSTP)

利用 waitpid(pid, &status, options) 回收子进程,并通过 status 告知任务怎么没的。一样采用 while 循环,因为信号会合并。如果同时有 3 个子进程结束,内核可能只给发一次 SIGCHLD(信号不排队)。所以 handler 必须用循环,一次性把所有能回收的都回收掉。

虽然 Hints 说用 exactly one call to waitpid,但应该指的是只在这一个地方写 waitpid(不要在 waitfg 里也写),不是说只调用一次。这个 while 里就一处 waitpid,应该符合要求吧。

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

      while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
      //WNOHANG:如果当前没有子进程状态变化,立刻返回 0,不阻塞。为什么要这个?因为 handler 不能卡住,否则 shell 就僵了。
//  WUNTRACED:不仅报告"结束"的子进程,也报告"被停止"的(Ctrl-Z 那种)。没这个标志就检测不到停止。
          if (WIFEXITED(status)) {
            deletejob(jobs,pid);
              // 正常结束 → deletejob,pid就是waitpid的pid
          }
          else if (WIFSIGNALED(status)) {
            printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
            deletejob(jobs,pid);
              // 被信号杀死 → 打印 terminated 消息 + deletejob
              // 参考 tshref.out 的 「Job [1] (26263) terminated by signal 2」数据,设置printf
          }
          else if (WIFSTOPPED(status)) {
            struct job_t *job = getjobpid(jobs,pid);
            job->state=ST;
            printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status)); //是哪个信号干的
              // 被停止 → getjobpid 拿到任务,把 state 改成 ST,也就是stop,在line 22-26 有定义。ST定义为3,打印 stopped 消息
              // 参考 「Job [2] (26276) stopped by signal 20」
          }
      }

      errno = olderrno;           //  恢复 errno,前面有可能修改掉errno
  }

跑 trace 04 和 05 终于不卡死啦~


sigint_handler && sigtstp_handler

跑 trace 06、07、08 都卡住了,因为两个信号 handler 还没写。

trace06 测的是 "把 SIGINT 转发给前台任务"。流程应该是:

  1. 前台跑 ./myspin 4
  2. driver 模拟按 Ctrl-C → 给你的 shell 发 SIGINT
  3. 你的 shell 应该捕获 SIGINT,转发给前台任务 → 任务被杀
  4. 任务死了 → SIGCHLD → sigchld_handler 打印 terminated by signal 2

trace08(SIGTSTP,stopped by signal 20)也是一个道理

也就是说,sigint 要做的应该就是杀死任务、sigtstp 要做的是挂起/停止任务,这个上课的时候看信号集就能找到对应关系了。
而且 sigint 与 sigtstp 的 handler 都应该不打印任何东西。

void sigint_handler(int sig) 
{
    pid_t pid=fgpid(jobs); //找前台任务是谁
    if (pid!=0){
        kill(-pid,SIGINT); // 杀掉任务,负号!发给整个前台进程组
    }
    return;
}

terminated by signal 2 那行谁打印?不需要在这里 deletejob 或打印,这里只管转发信号。
因为任务被杀后会触发 SIGCHLD,由刚刚写的 sigchld_handler 去打印 terminated 和删除,大家各司其职。

sigtstp 和 sigint 基本上一模一样,只有两处不同:发的信号是 SIGTSTP(不是 SIGINT)。结构照着 sigint_handler 改一下就行,fgpid 找前台任务,kill(-pid, SIGTSTP) 转发。就是代码含义不同。

void sigtstp_handler(int sig) 
{
    pid_t pid=fgpid(jobs); //找前台任务是谁
    if (pid!=0){
        kill(-pid,SIGTSTP); // 挂起任务,信号从SIGINT变成了SIGTSTP
    }
    return;
}

(int sig) 的引入是为了干啥的?问了一下 AI:

你从来不会自己调用 sigtstp_handler(...)。它是内核在信号到达时自动帮你调用的,调用时内核会把信号的编号当参数塞进 sig。

回到 main 里安装 handler 的那几行(第 121-123 行):
Signal(SIGINT, sigint_handler); // 注册:SIGINT 来了就调 sigint_handler
Signal(SIGTSTP, sigtstp_handler); // 注册:SIGTSTP 来了就调 sigtstp_handler
Signal(SIGCHLD, sigchld_handler); // 注册:SIGCHLD 来了就调 sigchld_handler

这叫"注册回调"。之后当 SIGTSTP 信号到达,内核就替你执行 sigtstp_handler(20)——20 就是 SIGTSTP 的编号,自动作为 sig 传进来。

这个签名 void handler(int) 是操作系统强制规定的格式(看第 85 行 typedef void handler_t(int))。所有信号处理函数都必须长这样:返回 void、接收一个 int。哪怕你用不上那个 int,也必须把它写在参数列表里,否则类型不匹配,注册时编译就报错。

也就是个操作系统的规定传参吧,我们不用管太多。不过我有点好奇,还是询问了一些东西,记录一下:

  1. void handler(int) 这个签名,其实是 C 语言标准规定的,不只是 Unix
    signal.h 和信号处理函数的签名 void func(int) 是 **C 标准库(C89 起)**就有的,写在 ISO C 标准里。所以严格说,"handler 接收一个 int" 不是 Unix 独有,是 C 语言层面的约定——任何符合标准 C 的平台(包括 Windows 的 C 运行时)写信号处理函数都得是这个签名。

  2. 虽然签名是 C 标准,但真正丰富的信号体系(SIGINT、SIGTSTP、SIGCHLD、任务控制、kill、sigaction、sigprocmask…)是 POSIX/Unix 定义的。这才是这个 lab 依赖的东西。具体到平台,就是 Linux、MacOS、BSD 等 Unix 操作系统家族独有的。

  3. Windows 没有真正的 Unix 信号。它的 C 运行时为了兼容 C 标准,提供一个残缺版 signal(),只支持 6 个标准信号(SIGINT/SIGABRT 等),没有 SIGTSTP、SIGCHLD、没有任务控制。

这或许也解答了整个 CSAPP LAB 一开始的问题的一部分:为什么这个实验必须要我们先拥有 Linux 环境(😭),而不能直接用每个中国大学生最熟悉的 Windows 。
至于 MacOS(现在应该只能买到 M 芯片版本了),它是个合适的 UNIX 系统,只是芯片架构和参考材料对不上而已。不过必须 Linux 更多是为了和 CMU 的标准答案环境保持一致(架构 + 工具链),而非 Unix 能力的有无。

很多人确实就在 macOS 上原生做完了整个 CSAPP shell lab,只是放弃用 tshref 实时对比、改成肉眼比对 tshref.out。开个 x 86 的 ubuntu 容器是为了让自己能 make rtest 爽快对照,脱离肉眼对照的苦海~


do_bgfg

跑最后 trace 14 专门测各种错误输入,所以 do_bgfg 要先做参数校验。参考 PDF 需求,得到要做的事情:

照着 trace 14 的错误信息,列一个表:

输入错误消息适用命令
fgbg(没参数)fg command requires PID or %jobid argument
bg command requires PID or %jobid argument
缺失参数
fg abg a(非数字)fg: argument must be a PID or %jobid
bg: argument must be a PID or %jobid
参数格式错误
fg 9999999(PID 不存在)(9999999): No such process找不到该进程
fg %2(JID 不存在)%2: No such job找不到该任务

然后照着这些,我们就可以写啦,其实就是 if else

void do_bgfg(char **argv)
  {
      struct job_t *job;
      int id;

      if (argv[1] == NULL) { //不给参数<PID>
          printf("%s command requires PID or %%jobid argument\n", argv[0]);
          return;
      }

      if (argv[1][0] == '%') { //参数给了个 %jid 形式
          id = atoi(&argv[1][1]);          // 跳过'%',取后面数字
          job = getjobjid(jobs, id);       // 按 jid 找
          if (job == NULL) {
              printf("%s: No such job\n", argv[1]);
              return;
          }
      }
 
      else if (isdigit(argv[1][0])) { //参数纯数字形式
          id = atoi(argv[1]);
          job = getjobpid(jobs, id);       // 按 pid 找
          if (job == NULL) {
              printf("(%d): No such process\n", id);
              return;
          }
      }
      else { // 参数不是 % 也不是数字
          printf("%s: argument must be a PID or %%jobid\n", argv[0]); // 在 printf 里 % 是特殊字符,要打印一个真正的 %,得写 %%。所以 "%%jobid" 打印出来是 %jobid
          return;
      }

      // 有 job了,发SIGCONT唤醒
      kill(-(job->pid), SIGCONT); //任务可能是被 Ctrl-Z 停止(ST)的,SIGCONT 是"继续"信号,唤醒它。还是用 -(job->pid) 负号发给整个进程组

        //看是fg还是bg,如果是bg,就打印一行 [jid] (pid) cmdline,然后立刻返回,相当于执行了,否则就waitfg
      if (!strcmp(argv[0], "bg")) {
          job->state = BG;
          printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
      } else {  /* fg */
          job->state = FG;
          waitfg(job->pid);                // 前台要等它
      }
  }

关于最后的 argv[0]argv[0] 表达的是用户的意图/命令
用户在 shell 里输入 fg %1,parseline 解析后:

既然要转移,那么这个任务此刻几乎肯定不在前台:想想看,如果它已经在前台运行,shell 此刻就正卡在 waitfg 里等它,根本没法接收输入的 fg 命令😂

所以执行 fg %1 时,%1 这个任务的当前状态只可能是:

do_bgfg 要做的就是把它改成 FG(这行 job->state = FG;),然后 waitfg 等它。

如果输入的是 bg %1,那么就是给用户的一条通知,告诉用户这个任务现在在后台跑起来了。shell 立刻把控制权还给用户,马上又能输入下一条命令。
任务到前台了,shell 要 waitfg 守着它、等它结束,这期间用户的终端被这个任务占着,啥也输入不了,就没必要 printf 了。


结束

最后答案:

  
/* 
 * 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,prev; //信号集

    strcpy(buf,cmdline);
    bg=parseline(buf,argv);
    if (argv[0]==NULL) return;
    if(!builtin_cmd(argv)){
        sigemptyset(&mask); //准备mask
        sigaddset(&mask,SIGCHLD); 
        sigprocmask(SIG_BLOCK,&mask,&prev); // how,set,oldset 的输入顺序

        if ((pid=fork())==0) { //开子进程
            sigprocmask(SIG_SETMASK,&prev,NULL); //子进程解除信号阻塞
            setpgid(0,0); //创建新进程组

            if (execve(argv[0],argv,environ)<0){
                printf("%s: Command not found\n",argv[0]);
                exit(0);
            }
        }
        addjob(jobs,pid,bg?BG:FG,cmdline); //登记父进程的任务
        sigprocmask(SIG_SETMASK,&prev,NULL); //解除阻塞

        if(!bg) waitfg(pid); //前台等待
        else printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline); //参考trace01-05的所需输出~
    }

    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],"&")) return 1;
    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 */
}

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv)
  {
      struct job_t *job;
      int id;

      if (argv[1] == NULL) { //不给参数<PID>
          printf("%s command requires PID or %%jobid argument\n", argv[0]);
          return;
      }

      if (argv[1][0] == '%') { //参数给了个 %jid 形式
          id = atoi(&argv[1][1]);          // 跳过'%',取后面数字
          job = getjobjid(jobs, id);       // 按 jid 找
          if (job == NULL) {
              printf("%s: No such job\n", argv[1]);
              return;
          }
      }
 
      else if (isdigit(argv[1][0])) { //参数纯数字形式
          id = atoi(argv[1]);
          job = getjobpid(jobs, id);       // 按 pid 找
          if (job == NULL) {
              printf("(%d): No such process\n", id);
              return;
          }
      }
      else { // 参数不是 % 也不是数字
          printf("%s: argument must be a PID or %%jobid\n", argv[0]); // 在 printf 里 % 是特殊字符,要打印一个真正的 %,得写 %%。所以 "%%jobid" 打印出来是 %jobid
          return;
      }

      // 有 job了,发SIGCONT唤醒
      kill(-(job->pid), SIGCONT); //任务可能是被 Ctrl-Z 停止(ST)的,SIGCONT 是"继续"信号,唤醒它。还是用 -(job->pid) 负号发给整个进程组

        //看是fg还是bg,如果是bg,就打印一行 [jid] (pid) cmdline,然后立刻返回,相当于执行了,否则就waitfg
      if (!strcmp(argv[0], "bg")) {
          job->state = BG;
          printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
      } else {  /* fg */
          job->state = FG;
          waitfg(job->pid);                // 前台要等它
      }
  }

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

/*****************
 * Signal handlers
 *****************/

/* 
 * 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;       // 保存 errno
      int status;
      pid_t pid;

      while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
          if (WIFEXITED(status)) {
            deletejob(jobs,pid);
              // 正常结束 → deletejob,pid就是waitpid的pid
          }
          else if (WIFSIGNALED(status)) {
            printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
            deletejob(jobs,pid);
              // 被信号杀死 → 打印 terminated 消息 + deletejob
              // 参考 tshref.out 的 「Job [1] (26263) terminated by signal 2」数据,设置printf
          }
          else if (WIFSTOPPED(status)) {
            struct job_t *job = getjobpid(jobs,pid);
            job->state=ST;
            printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status)); //是哪个信号干的
              // 被停止 → getjobpid 拿到任务,把 state 改成 ST,也就是stop,在line 22-26 有定义。ST定义为3,打印 stopped 消息
              // 参考 「Job [2] (26276) stopped by signal 20」
          }
      }

      errno = olderrno;           //  恢复 errno,前面有可能修改掉errno
  }

/* 
 * 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) 
{
    pid_t pid=fgpid(jobs); //找前台任务是谁
    if (pid!=0){
        kill(-pid,SIGINT); // 杀掉任务
    }
    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) 
{
    pid_t pid=fgpid(jobs); //找前台任务是谁
    if (pid!=0){
        kill(-pid,SIGTSTP); // 挂起任务
    }
    return;
}

16 个 trace 顺利通过!又完成了一次非常有趣的作业✌️。



0 / 2000
正在加载评论...