说明

补全所给的shell框架tsh.c(tsh.c的完整代码见此处,程序包见此处),使其支持linux下的fg、bg、jobs内置命令,并且可以运行可执行程序。可以用trace01.txt~trace16.txt这些测试数据作为输入,与tshref这个标准答案进行比较,检验tsh的行为是否正确。

前置知识

fork

#include#includepid_t fork(void);

fork函数从调用处开始创建一个新进程,子进程相当于父进程的一个副本。fork函数的最大特点就是调用一次,返回两次:子进程中fork的返回值为0;父进程中fork的返回值为子进程的PID。

waitpid

当父进程已经返回而子进程还在运行,子进程就称为一个僵死进程,它们会占用系统的内存资源,因此总是应该回收创建的子进程。这可以通过waitpid实现:

#include#includepid_t waitpid(pid_t pid, int *statusp, int options);

若不设置options选项,waitpid的默认行为是阻塞在调用处,直到有子进程返回。它需要三个参数:

pid

(1)当pid>0时,waitpid等待进程ID为pid的进程;
(2)当pid=-1时,waitpid等待所有它的子进程。

options

options中有如下选项:
(1)WNOHANG:若当前没有等待集合中的子进程终止,则立即返回0。
(2)WUNTRACED:等待直到某个等待集合中的子进程停止或返回,并返回这个子进程的pid。(实验中要用到这个选项)
(3)WCONTINUED:等待直到某个等待集合中的子进程重新开始执行或返回,并返回这个子进程的pid。
若需要多个选项,可以用C的’|’ (或)运算将它们组合起来,如WNOHANG | WUNTRACED.

statusp

若传入了一个指针statusp,waitpid会把子进程返回的状态信息存到这个指针指向的int处。可以用wait.h头文件中的宏获取这些信息,如:

WIFSIGNALED(status) //若子进程被未捕获的信号(如SIGINT)终止,该宏返回1WSTOPPED(status) //若子进程当前停止,返回1

以下对waitpid的调用都是合法的:

int status;waitpid(pid, &status, WNOHANG);
waitpid(pid, NULL, 0);

kill

kill函数向指定进程发送信号。

#include#includeint kill(pid_t pid, int sig);

其中pid是目标进程的进程号;sig是信号类型。本实验中用到的有以下几种:

序号名称(宏)默认行为相应时间
2SIGINT终止来自键盘的中断(Ctrl+C)
17SIGCHLD忽略一个子进程停止或终止
18SIGCONT忽略继续进程如果该进程停止
20SIGTSTP停止直到下一个SIGCONT来自终端的停止信号

如果不设置信号处理程序,给目标进程发送信号会按照其默认行为处理。可以通过设置信号处理程序来改变其默认行为。在给定的tsh.c中已经设置好了信号处理程序(SIGCHLD,SIGINT,SIGTSTP三种信号的),需要我们补全。

sigprocmask

可以利用sigprocmask改变当前阻塞的信号集合。当一种信号被阻塞,当前进程即使收到该信号也不会去调用信号处理程序。sigprocmask的函数原型如下:

#includeint sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

set

set是一个掩码,它的每一位携带了一种信号的信息。可以通过sigemptyset、sigaddset等函数来初始化该集合。下面仅给一个例子:

sigset_t mask;//这系列函数执行成功时返回0,否则返回-1if(sigemptyset(&mask))unix_error("sigemptyset error!");if(sigaddset(&mask, SIGINT))unix_error("sigaddset error!");

上面的代码将mask清空,并向集合中添加SIGINT信号。

how

how参数指定了该函数处理set的方式。有三种选择:
(1)SIG_BLOCK:将set中的信号阻塞。
(2)SIG_UNBLOCK:取消阻塞set中的信号。
(3)SIG_SETMASK:设置当前阻塞集合为set中的信号。

oldset

可以通过传入一个sigset_t型的指针来保存修改信号阻塞集合前的阻塞状态,以便之后恢复。
下面是一个阻塞信号的例子:

sigset_t mask, prev_mask;//构造掩码if(sigemptyset(&mask))unix_error("sigemptyset error!");if(sigaddset(&mask, SIGINT))unix_error("sigaddset error!");//阻塞SIGINTif(sigprocmask(SIG_BLOCK, &mask, &prev_mask))unix_error("sigprocmask error!");//do something//取消阻塞SIGINTif(sigprocmask(SIG_SETMASK, &prev_mask, NULL))unix_error("sigprocmask error!");

sigsuspend

int sigsuspend(const sigset_t *mask);

这个函数用于显式地等待子进程的执行结束。由于要消除子进程与父进程的竞争,需要暂时屏蔽SIGCHLD信号;初步可以用如下语句:

sigset_t mask, prev;//阻塞SIGCHLDif(sigemptyset(&mask))unix_error("sigemptyset error!");if(sigaddset(&mask, SIGCHLD))unix_error("sigaddset error!");if(sigprocmask(SIG_BLOCK, &mask, &prev))unix_error("sigprocmask error!");if(fork() == 0) {//do something...exit(0);}pid = 0;//设置pid=0,等待其在SIGCHLD handler变为子进程pid//由于设置完pid,竞争已经消除,取消阻塞SIGCHLDif(sigprocmask(SIG_SETMASK(SIG_SETMASK, &prev, NULL))unix_error("sigprocmask error!");while(!pid);//等待pid变为子进程的pid

但是这个会引起很多不必要的判断(指while(!pid)),消耗资源。若将这一句写为

while(!pid)pause();//仍然需要循环,因为SIGINT信号也会引起pause的结束

则又会引起竞争:若在判断!pid之后,pause之前信号到达,在handler回收完子进程后pause由于再也收不到SIGCHLD信号,程序会永远停在pause处。
sigsuspend可以用来解决这个问题。调用

sigsuspend(&mask);

等价于以下三条语句的原子版本(即,这三条语句中间不会被中断):

sigprocmask(SIG_SETMASK, &mask, &prev);pause();sigprocmask(SIG_SETMASK, &prev, NULL);

其中prev是调用sigsuspend之前的信号阻塞状态。
因此,sigsuspend可以暂时取消对SIGCHLD的阻塞(通过设置mask为未阻塞SIGCHLD的掩码),进入pause状态。若SIGINT到达,pause被打断,继续下一次循环;否则,若SIGCHLD到达,pid变为非0,循环结束。由于这三条语句是原子的,SIGCHLD不会在判断!pid之后,pause之前被消耗(因为在pause之前SIGCHLD被阻塞,信号不会被清空),因此总能保留到pause处,这就消除了单独一条pause语句引起的竞争。代码可以修改如下:

sigset_t mask, prev;//阻塞SIGCHLDif(sigemptyset(&mask))unix_error("sigemptyset error!");if(sigaddset(&mask, SIGCHLD))unix_error("sigaddset error!");if(sigprocmask(SIG_BLOCK, &mask, &prev))unix_error("sigprocmask error!");if(fork() == 0) {//do something...exit(0);}pid = 0;//设置pid=0,等待其在SIGCHLD handler变为子进程pidwhile(!pid);//等待pid变为子进程的pidsigsuspend(&prev);//暂时解除阻塞SIGCHLD//完全解除SIGCHLDif(sigprocmask(SIG_SETMASK, &prev, NULL))unix_error("sigprocmask error!");

补充函数

builtin_cmd

首先判断一下字符串是否为空,再比对一下内置命令即可。(do_bgfg和listjobs还已经给出来了)在quit之前需要回收一下子进程(给列表里所有子进程发送一个SIGINT),否则它们会在后台一直运行。

int builtin_cmd(char **argv) {//检查是否为空if(*argv != NULL) {//quitif(strcmp(*argv, "quit") == 0) {int i;for(i = 0; i < MAXJOBS; i++) {if(jobs[i].pid) {kill(-jobs[i].pid, SIGINT);}}exit(0);}//jobselse if(strcmp(*argv, "jobs") == 0) {listjobs(jobs);return 1;}//fg,bgelse if(strcmp(*argv, "fg") == 0 || strcmp(*argv, "bg") == 0) {do_bgfg(argv);return 1;}}return 0; /* not a builtin command */}

waitfg

waitfg等待进程号为pid的前台进程终止。其核心就是前面提到的sigsuspend函数。思想与前面介绍的一致,只不过while的条件设置为“前台进程还是pid”。

void waitfg(pid_t pid){sigset_t mask, prev;if(sigemptyset(&mask)) {unix_error("sigemptyset error!");}if(sigaddset(&mask, SIGCHLD)) {unix_error("sigaddset error!");}//在对全局数据操作之前阻塞SIGCHLD,避免竞争if(sigprocmask(SIG_SETMASK, &mask, &prev)) {unix_error("sigprocmask error!");}while(fgpid(jobs) == pid) {//只要前台进程还是pid,就一直等待sigsuspend(&prev);}//解除阻塞SIGCHLDif(sigprocmask(SIG_SETMASK, &prev, NULL)) {unix_error("sigprocmask error!");}return;}

sigchld_handler

该信号处理程序在子进程返回、终止、或被停止时触发。为了回收所有子进程,要一次尽可能多地回收子进程(用while),其原因在CSAPP P538有解释。要把waitpid的选项设为WUNTRACED是为了捕获SIGTSTP信号。在回收一个进程之后还要检查其status来做出相应的反应。(见注释)
此外,(1)在对全局数据的操作之前要暂时阻塞所有信号,防止产生意外的错误;(2)保存errno这个全局变量,在处理程序结束后再恢复它(CSAPP P536:保存和恢复errno)

void sigchld_handler(int sig)//17{int olderrno = errno;sigset_t mask, prev;if(sigfillset(&mask)) {unix_error("sigfillset error!");}if(sigprocmask(SIG_SETMASK, &mask, &prev)) {unix_error("sigprocmask error!");}int pid, status;//等待所有的子进程while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {struct job_t *job = getjobpid(jobs, pid);if(!WIFSTOPPED(status)) {if(WIFSIGNALED(status)) { //被SIGINT终止printf("Job [%d] (%d) terminated by signal %d\n",job->jid, job->pid, SIGINT);//输出SIGINT对应的提示信息}deletejob(jobs, pid);//由于进程不是STOP状态,可以删除}else { //被SIGTSTP停止printf("Job [%d] (%d) stopped by signal %d\n",job->jid, job->pid, SIGTSTP);//输出SIGTSTP对应的提示信息job->state = ST;//改变进程状态为ST}}if(sigprocmask(SIG_SETMASK, &prev, NULL)) {unix_error("sigprocmask error!");}errno = olderrno;return;}

sigint_handler && sigtstp_handler

这两个处理程序区别就是发送的信号不一样。由于要给进程发送信号,需要给kill的pid参数加个负号。

void sigint_handler(int sig)//2{int olderrno = errno;sigset_t mask, prev;//阻塞所有信号if(sigfillset(&mask)) {unix_error("sigfillset error!");}if(sigprocmask(SIG_SETMASK, &mask, &prev)) {unix_error("sigprocmask error!");}//给前台进程的进程组发送SIGINTkill(-fgpid(jobs), SIGINT);//取消阻塞信号if(sigprocmask(SIG_SETMASK, &prev, NULL)) {unix_error("sigprocmask error!");}errno = olderrno;return;}void sigtstp_handler(int sig)//20{int olderrno = errno;sigset_t mask, prev;if(sigfillset(&mask)) {unix_error("sigfillset error!");}if(sigprocmask(SIG_SETMASK, &mask, &prev)) {unix_error("sigprocmask error!");}kill(-fgpid(jobs), SIGTSTP);if(sigprocmask(SIG_SETMASK, &prev, NULL)) {unix_error("sigprocmask error!");}errno = olderrno;return;}

检验正确性

可以在文件夹中打开终端,执行如

./sdriver.pl -t trace01.txt -s ./tsh -a "-p"

其中trace01.txt中的01为序号,有01~16共16组测试数据。将输出与tshref.out比对,即可检验结果。