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