CSAPP附带实验中的ShellLab要求实现一个Unix下运行的Shell程序,并且能接受fg、bg、job、quit等指令和ctrl-c、ctrl-z的信号。这里并不从解题思路,而是从该程序的运行过程进行整理。
首先,不带参数启动时,Shell进程的main函数将编写好的handler注册到各个Signal上,然后通过一个不断循环的while来读取指令,在没有异常的情况下送到eval函数进行指令的解析。
int main(int argc, char **argv) // 未经修改的main函数
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);
/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
/* Install the signal handlers */
/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);
/* Initialize the job list */
initjobs(jobs);
/* Execute the shell's read/eval loop */
while (1) {
/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}
exit(0); /* control never reaches here */
}
接着,在eval函数中,先判断命令是否为内建指令,若不是内建指令,则是需要创建一个新的子进程。子进程依靠fork创建,依靠execve切换进程的内容,同时利用pid来判断在父进程还是子进程中,从而分别执行不同的代码。在父进程内部,同时需要创建一个jobs表来记录子进程。
在这里,有两个情况需要加锁:一是父进程在整个从fork到等待子进程被回收的过程,此操作内需要发送SIGCHLD信号,保证前台只有一个子进程;而是在父进程修改jobs表前需要加锁,此操作是保证jobs表不发生冲突。
void eval(char *cmdline)
{
int bg;
pid_t pid;
sigset_t mask, mask_all, prev;
char buf[MAXLINE];
char *argv[MAXARGS];
strcpy(buf, cmdline); // 读指令至buffer
bg = parseline(buf, argv); // 是否后台运行
if (argv[0] == NULL) // 空行
{
return;
}
if (!builtin_cmd(argv)) // 不是内建指令
{
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask, &prev); // 添加阻塞
if ((pid = fork()) == 0) // 子进程
{
sigprocmask(SIG_SETMASK, &prev, NULL); // 子进程的阻塞解除
if (setpgid(0, 0) < 0) // 初始化唯一的pid
{
printf("Setpgid Error!\n");
exit(0);
}
if (execve(argv[0], argv, environ) < 0) // 转换进程
{
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
else // 父进程
{
sigprocmask(SIG_BLOCK, &mask_all, NULL);
if (bg)
{
addjob(jobs, pid, BG, cmdline); // 添加至job表
}
else
{
addjob(jobs, pid, FG, cmdline); // 添加至job表
}
sigprocmask(SIG_SETMASK, &prev, NULL); // 对job操作完后即可解阻塞
if (bg)
{
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
else
{
waitfg(pid);
}
}
}
return;
}
在waitfg函数中等待前台子进程结束的方式,这里简单地采用了sleep+轮询
void waitfg(pid_t pid) // 等待前台子进程
{
while (pid == fgpid(jobs))
{
sleep(1);
}
return;
}
若指令是内建指令,则通过buildin_cmd函数来进行解析
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0],"quit"))
{
exit(0);
}
if (!strcmp(argv[0],"jobs"))
{
listjobs(jobs); // 输出jobs表
return 1;
}
if (!strcmp(argv[0],"bg") || !strcmp(argv[0],"fg"))
{
do_bgfg(argv);
return 1;
}
return 0;
}
其中quit为结束shell,jobs为输出jobs表,而bg或fg指令为将参数(pid或%jobid)所对应的进程切换到后台/前台运行。
因此,需要编写do_bgfg函数来具体执行bg和fg指令,并且发送绑定了handler的信号给子进程。
void sigchld_handler(int sig)
{
int old_errno = errno; // 保存原errno
pid_t pid;
sigset_t mask, prev;
int state; // waitpid的状态
struct job_t *job;
sigfillset(&mask);
while ((pid = waitpid(-1, &state, WNOHANG | WUNTRACED)) > 0) // while保证回收每个子进程
{
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号
if (WIFEXITED(state)) // 正常终止
{
deletejob(jobs, pid);
}
else if (WIFSIGNALED(state)) // 信号导致终止
{
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(state));
deletejob(jobs, pid);
}
else if (WIFSTOPPED(state)) // 子进程停止
{
job = getjobpid(jobs, pid);
if (job == NULL)
{
printf("Error: (%d): No such process\n",pid);
}
else
{
job->state = ST;
}
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(state));
}
sigprocmask(SIG_SETMASK, &prev, NULL); // 解锁
}
errno = old_errno; // 复原errno
}
对SIGCHLD信号,需要判断发送的原因。子进程正常终止、收到信号而终止、或者STOP时都会发送该信号。因此若正常终止或信号导致终止,则从jobs表中删除;若子进程停止,则只需要将job的状态改为STOP。
同样,需要在修改前后对信号加阻塞。
void sigint_handler(int sig) // 响应ctrl-c
{
int old_errno = errno;
pid_t pid = fgpid(jobs);
if (pid != 0)
{
kill(-pid, sig);
}
errno = old_errno;
return;
}
void sigtstp_handler(int sig) // 响应ctrl-z
{
int old_errno = errno;
pid_t pid = fgpid(jobs);
if (pid != 0)
{
kill(-pid, sig);
}
errno = old_errno;
return;
}
最后是ctrl-c和ctrl-z的相应。这里都只需要从jobs表中找到该job,并且将ctrl-c或ctrl-z的signal发送给子进程即可。
配合其他的辅助函数,这个程序便可以实现简单的shell功能,与参考文件中的示例shell输出无差。
./sdriver.pl -t trace01.txt -s ./tsh -a "-p" # # trace01.txt - Properly terminate on EOF. # ./sdriver.pl -t trace01.txt -s ./tshref -a "-p" # # trace01.txt - Properly terminate on EOF. # ./sdriver.pl -t trace02.txt -s ./tsh -a "-p" # # trace02.txt - Process builtin quit command. # ./sdriver.pl -t trace02.txt -s ./tshref -a "-p" # # trace02.txt - Process builtin quit command. # ./sdriver.pl -t trace03.txt -s ./tsh -a "-p" # # trace03.txt - Run a foreground job. # tsh> quit ./sdriver.pl -t trace03.txt -s ./tshref -a "-p" # # trace03.txt - Run a foreground job. # tsh> quit ./sdriver.pl -t trace04.txt -s ./tsh -a "-p" # # trace04.txt - Run a background job. # tsh> ./myspin 1 & [1] (19812) ./myspin 1 & ./sdriver.pl -t trace04.txt -s ./tshref -a "-p" # # trace04.txt - Run a background job. # tsh> ./myspin 1 & [1] (19817) ./myspin 1 & ./sdriver.pl -t trace05.txt -s ./tsh -a "-p" # # trace05.txt - Process jobs builtin command. # tsh> ./myspin 2 & [1] (19824) ./myspin 2 & tsh> ./myspin 3 & [2] (19826) ./myspin 3 & tsh> jobs [1] (19824) Running ./myspin 2 & [2] (19826) Running ./myspin 3 & ./sdriver.pl -t trace05.txt -s ./tshref -a "-p" # # trace05.txt - Process jobs builtin command. # tsh> ./myspin 2 & [1] (19834) ./myspin 2 & tsh> ./myspin 3 & [2] (19836) ./myspin 3 & tsh> jobs [1] (19834) Running ./myspin 2 & [2] (19836) Running ./myspin 3 & ./sdriver.pl -t trace06.txt -s ./tsh -a "-p" # # trace06.txt - Forward SIGINT to foreground job. # tsh> ./myspin 4 Job [1] (19846) terminated by signal 2 ./sdriver.pl -t trace06.txt -s ./tshref -a "-p" # # trace06.txt - Forward SIGINT to foreground job. # tsh> ./myspin 4 Job [1] (19853) terminated by signal 2 ./sdriver.pl -t trace07.txt -s ./tsh -a "-p" # # trace07.txt - Forward SIGINT only to foreground job. # tsh> ./myspin 4 & [1] (19860) ./myspin 4 & tsh> ./myspin 5 Job [2] (19862) terminated by signal 2 tsh> jobs [1] (19860) Running ./myspin 4 & ./sdriver.pl -t trace07.txt -s ./tshref -a "-p" # # trace07.txt - Forward SIGINT only to foreground job. # tsh> ./myspin 4 & [1] (19874) ./myspin 4 & tsh> ./myspin 5 Job [2] (19876) terminated by signal 2 tsh> jobs [1] (19874) Running ./myspin 4 & ./sdriver.pl -t trace08.txt -s ./tsh -a "-p" # # trace08.txt - Forward SIGTSTP only to foreground job. # tsh> ./myspin 4 & [1] (19886) ./myspin 4 & tsh> ./myspin 5 Job [2] (19888) stopped by signal 20 tsh> jobs [1] (19886) Running ./myspin 4 & [2] (19888) Stopped ./myspin 5 ./sdriver.pl -t trace08.txt -s ./tshref -a "-p" # # trace08.txt - Forward SIGTSTP only to foreground job. # tsh> ./myspin 4 & [1] (19898) ./myspin 4 & tsh> ./myspin 5 Job [2] (19900) stopped by signal 20 tsh> jobs [1] (19898) Running ./myspin 4 & [2] (19900) Stopped ./myspin 5 ./sdriver.pl -t trace09.txt -s ./tsh -a "-p" # # trace09.txt - Process bg builtin command # tsh> ./myspin 4 & [1] (19912) ./myspin 4 & tsh> ./myspin 5 Job [2] (19914) stopped by signal 20 tsh> jobs [1] (19912) Running ./myspin 4 & [2] (19914) Stopped ./myspin 5 tsh> bg %2 [2] (19914) ./myspin 5 tsh> jobs [1] (19912) Running ./myspin 4 & [2] (19914) Running ./myspin 5 ./sdriver.pl -t trace09.txt -s ./tshref -a "-p" # # trace09.txt - Process bg builtin command # tsh> ./myspin 4 & [1] (19926) ./myspin 4 & tsh> ./myspin 5 Job [2] (19928) stopped by signal 20 tsh> jobs [1] (19926) Running ./myspin 4 & [2] (19928) Stopped ./myspin 5 tsh> bg %2 [2] (19928) ./myspin 5 tsh> jobs [1] (19926) Running ./myspin 4 & [2] (19928) Running ./myspin 5 ./sdriver.pl -t trace10.txt -s ./tsh -a "-p" # # trace10.txt - Process fg builtin command. # tsh> ./myspin 4 & [1] (19942) ./myspin 4 & tsh> fg %1 Job [1] (19942) stopped by signal 20 tsh> jobs [1] (19942) Stopped ./myspin 4 & tsh> fg %1 tsh> jobs ./sdriver.pl -t trace10.txt -s ./tshref -a "-p" # # trace10.txt - Process fg builtin command. # tsh> ./myspin 4 & [1] (19955) ./myspin 4 & tsh> fg %1 Job [1] (19955) stopped by signal 20 tsh> jobs [1] (19955) Stopped ./myspin 4 & tsh> fg %1 tsh> jobs ./sdriver.pl -t trace11.txt -s ./tsh -a "-p" # # trace11.txt - Forward SIGINT to every process in foreground process group # tsh> ./mysplit 4 Job [1] (19968) terminated by signal 2 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 19964 pts/0 S+ 0:00 make test11 19965 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tsh -a -p 19966 pts/0 S+ 0:00 ./tsh -p 19973 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash ./sdriver.pl -t trace11.txt -s ./tshref -a "-p" # # trace11.txt - Forward SIGINT to every process in foreground process group # tsh> ./mysplit 4 Job [1] (19978) terminated by signal 2 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 19974 pts/0 S+ 0:00 make rtest11 19975 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tshref -a -p 19976 pts/0 S+ 0:00 ./tshref -p 19985 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash ./sdriver.pl -t trace12.txt -s ./tsh -a "-p" # # trace12.txt - Forward SIGTSTP to every process in foreground process group # tsh> ./mysplit 4 Job [1] (19992) stopped by signal 20 tsh> jobs [1] (19992) Stopped ./mysplit 4 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 19986 pts/0 S+ 0:00 make test12 19987 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace12.txt -s ./tsh -a -p 19990 pts/0 S+ 0:00 ./tsh -p 19992 pts/0 T 0:00 ./mysplit 4 19993 pts/0 T 0:00 ./mysplit 4 20000 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash ./sdriver.pl -t trace12.txt -s ./tshref -a "-p" # # trace12.txt - Forward SIGTSTP to every process in foreground process group # tsh> ./mysplit 4 Job [1] (20007) stopped by signal 20 tsh> jobs [1] (20007) Stopped ./mysplit 4 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 20001 pts/0 S+ 0:00 make rtest12 20002 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace12.txt -s ./tshref -a -p 20005 pts/0 S+ 0:00 ./tshref -p 20007 pts/0 T 0:00 ./mysplit 4 20008 pts/0 T 0:00 ./mysplit 4 20011 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash ./sdriver.pl -t trace13.txt -s ./tsh -a "-p" # # trace13.txt - Restart every stopped process in process group # tsh> ./mysplit 4 Job [1] (20020) stopped by signal 20 tsh> jobs [1] (20020) Stopped ./mysplit 4 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 20012 pts/0 S+ 0:00 make test13 20013 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tsh -a -p 20018 pts/0 S+ 0:00 ./tsh -p 20020 pts/0 T 0:00 ./mysplit 4 20021 pts/0 T 0:00 ./mysplit 4 20025 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash tsh> fg %1 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 20012 pts/0 S+ 0:00 make test13 20013 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tsh -a -p 20018 pts/0 S+ 0:00 ./tsh -p 20030 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash ./sdriver.pl -t trace13.txt -s ./tshref -a "-p" # # trace13.txt - Restart every stopped process in process group # tsh> ./mysplit 4 Job [1] (20037) stopped by signal 20 tsh> jobs [1] (20037) Stopped ./mysplit 4 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 20031 pts/0 S+ 0:00 make rtest13 20032 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tshref -a -p 20035 pts/0 S+ 0:00 ./tshref -p 20037 pts/0 T 0:00 ./mysplit 4 20038 pts/0 T 0:00 ./mysplit 4 20041 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash tsh> fg %1 tsh> /bin/ps a PID TTY STAT TIME COMMAND 1054 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 8356 pts/2 Ss+ 0:00 /bin/bash 18883 pts/0 Ss 0:00 -bash 19786 pts/0 S+ 0:00 sh run.sh 20031 pts/0 S+ 0:00 make rtest13 20032 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tshref -a -p 20035 pts/0 S+ 0:00 ./tshref -p 20046 pts/0 R 0:00 /bin/ps a 21890 pts/1 Ss+ 0:00 /bin/bash ./sdriver.pl -t trace14.txt -s ./tsh -a "-p" # # trace14.txt - Simple error handling # tsh> ./bogus ./bogus: Command not found. tsh> ./myspin 4 & [1] (20055) ./myspin 4 & tsh> fg fg command requires PID or %jobid argument tsh> bg bg command requires PID or %jobid argument tsh> fg a fg: argument must be a PID or %jobid tsh> bg a bg: argument must be a PID or %jobid tsh> fg 9999999 (9999999): No such process tsh> bg 9999999 (9999999): No such process tsh> fg %2 %2: No such job tsh> fg %1 Job [1] (20055) stopped by signal 20 tsh> bg %2 %2: No such job tsh> bg %1 [1] (20055) ./myspin 4 & tsh> jobs [1] (20055) Running ./myspin 4 & ./sdriver.pl -t trace14.txt -s ./tshref -a "-p" # # trace14.txt - Simple error handling # tsh> ./bogus ./bogus: Command not found tsh> ./myspin 4 & [1] (20075) ./myspin 4 & tsh> fg fg command requires PID or %jobid argument tsh> bg bg command requires PID or %jobid argument tsh> fg a fg: argument must be a PID or %jobid tsh> bg a bg: argument must be a PID or %jobid tsh> fg 9999999 (9999999): No such process tsh> bg 9999999 (9999999): No such process tsh> fg %2 %2: No such job tsh> fg %1 Job [1] (20075) stopped by signal 20 tsh> bg %2 %2: No such job tsh> bg %1 [1] (20075) ./myspin 4 & tsh> jobs [1] (20075) Running ./myspin 4 & ./sdriver.pl -t trace15.txt -s ./tsh -a "-p" # # trace15.txt - Putting it all together # tsh> ./bogus ./bogus: Command not found. tsh> ./myspin 10 Job [1] (20097) terminated by signal 2 tsh> ./myspin 3 & [1] (20101) ./myspin 3 & tsh> ./myspin 4 & [2] (20103) ./myspin 4 & tsh> jobs [1] (20101) Running ./myspin 3 & [2] (20103) Running ./myspin 4 & tsh> fg %1 Job [1] (20101) stopped by signal 20 tsh> jobs [1] (20101) Stopped ./myspin 3 & [2] (20103) Running ./myspin 4 & tsh> bg %3 %3: No such job tsh> bg %1 [1] (20101) ./myspin 3 & tsh> jobs [1] (20101) Running ./myspin 3 & [2] (20103) Running ./myspin 4 & tsh> fg %1 tsh> quit ./sdriver.pl -t trace15.txt -s ./tshref -a "-p" # # trace15.txt - Putting it all together # tsh> ./bogus ./bogus: Command not found tsh> ./myspin 10 Job [1] (20122) terminated by signal 2 tsh> ./myspin 3 & [1] (20127) ./myspin 3 & tsh> ./myspin 4 & [2] (20129) ./myspin 4 & tsh> jobs [1] (20127) Running ./myspin 3 & [2] (20129) Running ./myspin 4 & tsh> fg %1 Job [1] (20127) stopped by signal 20 tsh> jobs [1] (20127) Stopped ./myspin 3 & [2] (20129) Running ./myspin 4 & tsh> bg %3 %3: No such job tsh> bg %1 [1] (20127) ./myspin 3 & tsh> jobs [1] (20127) Running ./myspin 3 & [2] (20129) Running ./myspin 4 & tsh> fg %1 tsh> quit ./sdriver.pl -t trace16.txt -s ./tsh -a "-p" # # trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT # signals that come from other processes instead of the terminal. # tsh> ./mystop 2 Job [1] (20147) stopped by signal 20 tsh> jobs [1] (20147) Stopped ./mystop 2 tsh> ./myint 2 Job [2] (20156) terminated by signal 2 ./sdriver.pl -t trace16.txt -s ./tshref -a "-p" # # trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT # signals that come from other processes instead of the terminal. # tsh> ./mystop 2 Job [1] (20163) stopped by signal 20 tsh> jobs [1] (20163) Stopped ./mystop 2 tsh> ./myint 2 Job [2] (20168) terminated by signal 2