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 # 现在能输入了!因为前台空了
我们要做的事情不过就是通过代码控制任务执行的先后顺序而已,维护一个存放任务的数据库,然后调用能调用的函数来操纵这个数据库,且我们每一个函数的职责如图:

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,只有两种情况,所以要加上去,参考:

加上给出的注释:return 0 旁边写着 "not a builtin command"(不是内置命令)。这就约定了:
return 1的时候,说明我们输入的是内置命令,也就是给出的 jobs、bg、fg 等,不用 fork,继续开始处理,处理完了执行,return 1。return 0的时候,需要 eval 进行 fork 来执行。
那我们就可以开开心心的补全 builtin_cmd 的代码了:
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:



那我们就愉快补全代码了,其实在原来基础上补充一个建立进程的逻辑就可以,先补一个 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 一直在前台就一直等。

然后看了看,能跑一些测试了,尝试 make 然后跑 trace 01-04 的时候,顺利在 trace 04 卡住了……
排查原因:
- 你的 shell 把 /bin/echo 当前台任务,fork+exec,然后
waitfg(pid)等它- echo 瞬间打印完就退出了 → 变成僵尸进程(就是快照里的
echo <defunct>)- 但是
sigchld_handler是空的,没人去deletejob把 echo 从任务表删掉- 于是
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 转发给前台任务"。流程应该是:
- 前台跑 ./myspin 4
- driver 模拟按 Ctrl-C → 给你的 shell 发 SIGINT
- 你的 shell 应该捕获 SIGINT,转发给前台任务 → 任务被杀
- 任务死了 → 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,也必须把它写在参数列表里,否则类型不匹配,注册时编译就报错。
也就是个操作系统的规定传参吧,我们不用管太多。不过我有点好奇,还是询问了一些东西,记录一下:
void handler(int) 这个签名,其实是 C 语言标准规定的,不只是 Unix
signal.h 和信号处理函数的签名 void func(int) 是 **C 标准库(C89 起)**就有的,写在 ISO C 标准里。所以严格说,"handler 接收一个 int" 不是 Unix 独有,是 C 语言层面的约定——任何符合标准 C 的平台(包括 Windows 的 C 运行时)写信号处理函数都得是这个签名。虽然签名是 C 标准,但真正丰富的信号体系(SIGINT、SIGTSTP、SIGCHLD、任务控制、kill、sigaction、sigprocmask…)是 POSIX/Unix 定义的。这才是这个 lab 依赖的东西。具体到平台,就是 Linux、MacOS、BSD 等 Unix 操作系统家族独有的。
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 需求,得到要做的事情:
bg <job>:给停止的任务发 SIGCONT 让它在后台继续运行fg <job>:给任务发 SIGCONT 让它到前台运行,并等它

照着 trace 14 的错误信息,列一个表:
| 输入 | 错误消息 | 适用命令 |
|---|---|---|
fg 或 bg(没参数) | fg command requires PID or %jobid argumentbg command requires PID or %jobid argument | 缺失参数 |
fg a 或 bg a(非数字) | fg: argument must be a PID or %jobidbg: 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 解析后:
argv[0] = "fg"← 用户输入的命令名argv[1] = "%1"← 任务号
所以argv[0] == "fg"的意思是:用户下达了 fg 这个命令,也就是用户想把某个任务搬到前台去。
既然要转移,那么这个任务此刻几乎肯定不在前台:想想看,如果它已经在前台运行,shell 此刻就正卡在 waitfg 里等它,根本没法接收输入的 fg 命令😂
所以执行 fg %1 时,%1 这个任务的当前状态只可能是:
- ST(被 Ctrl-Z 停止的) → fg 把它唤醒并拉到前台
- BG(在后台运行的) → fg 把它拉到前台
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 顺利通过!又完成了一次非常有趣的作业✌️。