csapp 笔记(3) shell lab
csapp 的 Shell Lab 要求完成一 个简单的 unix shell(tsh, tiny shell), 实现外壳程序的几个基本功能:
- 执行命令, 并为命令创建任务(job);
- 查看或修改当前任务的状态;
- 回收已完成的任务.
- 接收并处理信号, 如 ctrl-c, ctrl-z 等.
tsh 不用支持 shell 的高级特性如环境变量, 重定向和管道等.
作业发放的材料里已经给 tsh 搭好了框架, 只需要填写几个关键函数就好了.
执行命令 eval
eval 函数用来执行输入给 tsh 的命令. 如果是内置命令, 就直接执行; 如果不
是就创建一个子进程, 利用 execve
加载.
需要注意的是, 在为子进程建立 shell 任务的时候, 需要先阻塞 SIGCHLD 信号, 否则如果子进程在 shell 任务创建前就结束的话, 这个任务就永远不会结束了.
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
然后父进程在添加任务后再恢复接收 SIGCHLD 信号.
if (addjob(jobs, pid, status, cmdline) != 1) {
unix_error("addjob failed");
}
// unblock sigchld after addjob
sigprocmask(SIG_UNBLOCK, &mask, NULL);
默认情况下, tsh 创建的子进程会属于 tsh 进程组. 子进程需要创建一个进程 组, 避免收到系统发送给 tsh 进程组的信号, 从而由 tsh 接管所有的子进程.
if ((pid = fork()) == 0) {
// unblock sigchld
sigprocmask(SIG_UNBLOCK, &mask, NULL);
// reset process group id
if (setpgid(0, 0) != 0) {
unix_error("setpgid failed");
}
if (execve(argv[0], argv, environ) < 0) {
printf("%s: command not found", argv[0]);
exit(0);
}
如果是前台命令的话, 就阻塞用户输入直到收到信号(ctrl-c, ctrl-z 或者子进程结束的 SIGCHLD), 可以利用 sigsuspend
来实现原子地信号唤醒. 同样需要先阻塞 SIGCHLD 信号, 避免子进程在进入等待前就结束了.
void waitfg(pid_t pid) {
// block sigchld
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &prev);
while (pid == fgpid(jobs)) {
sigsuspend(&prev);
}
// unblock sigchld
sigprocmask(SIG_UNBLOCK, &mask, NULL);
}
信号处理
SIGINT 和 SIGTSTP 的处理都很直接, 直接把信号转发给相应的子进程就好.
void sigint_handler(int sig) {
pid_t pid = fgpid(jobs);
if (pid != 0 && (kill(-pid, SIGINT) != 0)) {
unix_error("sending sigint to fg failed");
}
}
void sigtstp_handler(int sig) {
pid_t pid = fgpid(jobs);
if (pid != 0 && (kill(-pid, SIGTSTP) != 0)) {
unix_error("sending sigtstp to fg failed");
}
}
注意这两个信号处理不需要直接管理 shell 任务, 因为子进程被终止(SIGINT)或暂停(SIGTSTP)后会向父进程发送 SIGCHLD 信号, 所以统一在 SIGCHLD 信号处理程序中处理就好了.
void sigchld_handler(int sig) {
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG|WUNTRACED)) > 0) {
struct job_t *job = getjobpid(jobs, pid);
if (WIFSTOPPED(status)) {
// child process is stopped
printf("[%d] (%d) stopped\n", job->jid, job->pid);
job->state = ST;
} else {
if (WIFEXITED(status)) {
// child process exited normally
} else if (WIFSIGNALED(status)) {
// child process terminited by signals
printf("[%d] (%d) terminated by signal %d\n", job->jid, job->pid, WTERMSIG(status));
} else {
printf("[%d] (%d) terminated unknown\n", job->jid, job->pid);
}
// deletejob
if (deletejob(jobs, pid) != 1) {
unix_error("sigchld_handler: deletejob failed");
}
}
}
}
任务管理
有了信号处理和 waitfg
之后, 改变任务状态就可以控制子进程的状态了.
bg
命令:
switch (job->state) {
case BG:
printf("job %d is already in background\n", job->jid);
break;
case ST:
job->state = BG;
if (kill(-(job->pid), SIGCONT) != 0) {
unix_error("sending SIGCONT to bg job failed");
}
break;
case FG:
job->state = BG;
break;
}
fg
命令:
switch (job->state) {
case FG:
printf("job %d is already in foreground\n", job->jid);
break;
case ST:
if (kill(-(job->pid), SIGCONT) != 0) {
unix_error("sending SIGCONT to bg job failed");
}
case BG:
printf("[%d] (%d) %s\n", job->jid, job->pid, job->cmdline);
job->state = FG;
waitfg(job->pid);
break;
}
小结
Shell Lab 主要考察了异常控制的进程控制和信号处理. 因为信号可能会在任意
时刻到来, 所以需要考虑信号处理逻辑的先后顺序. 必要时可以阻塞信号.
sigsuspend
可以用来安全地挂起进程, 直到接收到相应信号.