这次实验要求实现一个简单的shell,它具有以下功能:
- 包含以下built-in函数(在shell进程中执行并等待返回):
- The
quit
command terminates the shell. - The
jobs
command lists all background jobs. - The
bg <job>
command restarts<job>
by sending it aSIGCONT
signal, and then runs it in the background. The<job>
argument can be either a PID or a JID. - The
fg <job>
command restarts<job>
by sending it aSIGCONT
signal, and then runs it in the foreground. The<job>
argument can be either a PID or a JID.
- The
fork
一个子进程(称为job)去加载执行一个可执行文件。- 输入
ctrl-c (ctrl-z)
对前台进程发送SIGINT (SIGTSTP)
信号。 - 支持
&
参数,让一个job在后台运行。 - shell进程要收割所有僵尸子进程。
在开始完成这次实验之前,了解Linux进程和信号的原理是很有必要的。这次实验中使用了以下几个函数:waitpid
, kill
, fork
, execve
, setpgid
, sigprocmask
。
值得一提的是,这个lab的测试脚本不如之前几次的好。由于竞争条件有一定概率对输出结果不造成影响,只靠简单的一两次测试是无法说明程序的安全性的。网上很多答案其实是有问题的,要做对还是得多debug。
首先,使用的系统调用函数都应该像书上Fork
那样,加一层包装,在检查到异常返回值时报错,退出程序:
pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
int Sigprocmask(int how, const sigset_t *set, sigset_t *oldset) {
int rtn;
if ((rtn = sigprocmask(how, set, oldset)) == -1)
unix_error("Sigprocmask error");
return rtn;
}
不过,由于这是一次作业,不是很有必要,我就直接用裸露的系统调用函数了。所有的包装,都可以在CSAPP附带的源码文件csapp.c
中找到。
要实现的函数共有7个,首先是eval
。eval
处理输入命令,判断它是否为builtin
函数,如果是,直接执行直接返回,否则fork
一个子进程在前台或者后台执行之。在fork
之前,要把SIGCHLD
信号阻塞掉,以防竞争条件;在子进程调用execve
之前,要解除SIGCHLD
的阻塞,不然父进程的阻塞集合会被子进程继承。在将子进程PID添加至作业列表前,阻塞所有信号,因为信号处理程序里也会操作作业列表,应该阻塞信号以保护该共享数据结构。最后,根据子进程是在前台执行还是后台执行,主进程选择调用waitfg
等待子进程。
void eval(char *cmdline) {
char *argv[MAXARGS]; /* Argument list execve() */
char buf[MAXLINE]; /* Holds modified command line */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL)
return; /* Ignore empty lines */
if (!builtin_cmd(argv)) {
// initialize sigset_ts
sigset_t mask_chid, mask_all, prev_one;
sigfillset(&mask_all);
sigemptyset(&mask_chid);
sigaddset(&mask_chid, SIGCHLD);
// block SIGCHID before fork
sigprocmask(SIG_BLOCK, &mask_chid, &prev_one);
if ((pid = Fork()) == 0) { /* Child runs user job */
// unblock SIGCHID before execve
sigprocmask(SIG_SETMASK, &prev_one, NULL);
// put the child in a new process group
setpgid(0, 0);
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
// add the child to job list
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, bg ? BG : FG, cmdline);
sigprocmask(SIG_SETMASK, &prev_one, NULL);
if (!bg) { // wait for foreground process
waitfg(pid);
} else {
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}
return;
}
如果输入的命令是一个builtin
函数,那么便由主进程执行之。函数do_bgfg
负责唤醒被停止的子进程,并将其放置在前台或后台执行。在测试样例中,有对do_bgfg
的不合法输入进行测试,所以不能预设do_bgfg
的输入都是合法的。在接收到参数后,首先要判断接收到的是一个jid
还是一个pid
,获取对应子进程的pid
后,发送SIGCONT
唤醒之,并在作业列表中修改其状态。如果是在前台进行,就调用waitfg
等待其完成。
int builtin_cmd(char **argv) {
if (!strcmp(argv[0], "quit")) /* quit command */
exit(0);
else if (!strcmp(argv[0], "jobs")) { // list all background jobs
listBGjobs(jobs);
} else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
// restart a job and run it in background/foreground
do_bgfg(argv);
} else if (!strcmp(argv[0], "&")) { /* Ignore singleton & */ }
else { // not builtin command
return 0;
}
return 1;
}
void do_bgfg(char **argv) {
struct job_t *job;
// invalid input
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (!isdigit(argv[1][0]) && argv[1][0] != '%') {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
if (argv[1][0] == '%') { // receive a jid
job = getjobjid(jobs, atoi(&argv[1][1]));
if (job == NULL) {
printf("%s: No such job\n", argv[1]);
return;
}
} else { // receive a pid
job = getjobpid(jobs, atoi(argv[1]));
if (job == NULL) {
printf("(%s): No such process\n", argv[1]);
return;
}
}
// restart by sending SIGCONT
kill(-job->pid, SIGCONT);
if (!strcmp(argv[0], "fg")) { // wait for foreground process
job->state = FG;
waitfg(job->pid);
} else {
job->state = BG;
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
}
}
waitfg
不可以使用waitpid
,因为如果前台进程被暂停或者改成后台了,waitpid
等待的pid
就失效了。所以要忙等待,不时就要重新获取前台子进程的pid
,在前台pid
不再是参数pid
时说明前台进程暂停/终止/变后台了,主进程也不再等待它。
void waitfg(pid_t pid) {
while (pid == fgpid(jobs)) {
sleep(1);
}
}
接下来是三种信号的signal handler。首先要注意的一个问题是,printf
并不是异步安全的,在信号处理程序中不能用(因为偷懒我就用了),应该用书上的sio_puts
。
在接收到一个SIGCHID
信号后,处理程序使用waitpid
收割子进程。waitpid
带有参数WNOHANG
和WUNTRACED
。WNOHANG
是为了支持while
判断,在处理程序返回前尽可能地收割子进程,以免因阻塞SIGCHLD
信号导致一些子进程发送的SIGCHLD
信号被抛弃。WUNTRACED
使得该处理程序也能捕获因信号而停止的子进程。 在捕获到子进程后,应该判断子进程发送SIGCHLD
信号的原因。如果是正常结束,只需要将其从作业列表中删除就可以;如果是被SIGINT
信号中断,那么输出提示信息后将其从作业列表中删除;如果是被SIGTSTP
信号停止,那么将其在工作列表中的状态改变。最后,判断waitpid
是否返回错误;如果有,输出error
提示的错误信息。由于waitpid
带有参数WNOHANG
,所以不可再像书上那样判断信号处理过程是否有异常(即使用errno != ECHILD
做判断条件);带有参数WNOHANG
下,因调用进程没有孩子而返回时只是把pid
设为0,而不会改变errno
。理论上,在进入信号处理程序之前要保存原来的errno
,在退出后要恢复原来的errno
,使得信号处理过程中不影响errno
。我在SIGCHID
的信号处理程序中采用了这种写法,另外两个就没写了。
在键盘上输入ctrl+c
,会向前台进程组发送一个SIGINT信号。事实上的前台进程组其实只包含主进程,它fork
出来的子进程由于setpgid(0, 0
,已经不再和主进程在同个进程组里。在接收到SIGINT
信号以后,主进程应该向逻辑上的“前台进程组”发送SIGINT
信号;收到信号的子进程终止,像主进程发送SIGCHLD
信号。它们在作业列表中的操作交由SIGCHLD
的信号处理程序处理。SIGTSTP
信号同理,在主进程接收到信号以后暂停前台进程组。暂停以后,前台进程组在工作列表中的变化也交由SIGCHLD
信号处理程序处理。
void sigchld_handler(int sig) {
int olderrno = errno;
pid_t pid;
int status;
sigset_t mask_all, prev_all;
sigfillset(&mask_all);
while ((pid = waitpid(-1, &status, WNOHANG|WUNTRACED)) > 0) {
if (WIFEXITED(status)) { // exit normally
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(jobs, pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
} else if (WIFSIGNALED(status)) { // killed by signal
// should be sio_puts!
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(jobs, pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
} else if (WIFSTOPPED(status)) { // stopped by signal
printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
struct job_t *job = getjobpid(jobs, pid);
if (job != NULL) {
job->state = ST;
}
}
}
// check errno on error
if (pid == -1 && errno != ECHILD)
unix_error("waitpid error");
errno = olderrno;
}
void sigint_handler(int sig) {
pid_t pid = fgpid(jobs);
if (pid != 0) { // kill the whole group
kill(-pid, sig);
}
}
void sigtstp_handler(int sig) {
pid_t pid = fgpid(jobs);
if (pid != 0) {
kill(-pid, sig);
}
}
即使是这样一个短小的应用程序,为了满足信号安全条件,需要做的保护措施也是很多的。我没有严格地遵从,因为测试样例很简单,并没有严格地要求安全性。不过我在文中把我认为应该注意的地方都说了一下。总得来说,这次实验大概可以算是CSAPP实验系列中最简单之一,不过它的坑很多。