发布时间:2022-09-05 06:00
补全所给的shell框架tsh.c(tsh.c的完整代码见此处,程序包见此处),使其支持linux下的fg、bg、jobs内置命令,并且可以运行可执行程序。可以用trace01.txt~trace16.txt这些测试数据作为输入,与tshref这个标准答案进行比较,检验tsh的行为是否正确。
#include
#include
pid_t fork(void);
fork函数从调用处开始创建一个新进程,子进程相当于父进程的一个副本。fork函数的最大特点就是调用一次,返回两次:子进程中fork的返回值为0;父进程中fork的返回值为子进程的PID。
当父进程已经返回而子进程还在运行,子进程就称为一个僵死进程,它们会占用系统的内存资源,因此总是应该回收创建的子进程。这可以通过waitpid实现:
#include
#include
pid_t waitpid(pid_t pid, int *statusp, int options);
若不设置options选项,waitpid的默认行为是阻塞在调用处,直到有子进程返回。它需要三个参数:
(1)当pid>0时,waitpid等待进程ID为pid的进程;
(2)当pid=-1时,waitpid等待所有它的子进程。
options中有如下选项:
(1)WNOHANG:若当前没有等待集合中的子进程终止,则立即返回0。
(2)WUNTRACED:等待直到某个等待集合中的子进程停止或返回,并返回这个子进程的pid。(实验中要用到这个选项)
(3)WCONTINUED:等待直到某个等待集合中的子进程重新开始执行或返回,并返回这个子进程的pid。
若需要多个选项,可以用C的’|’ (或)运算将它们组合起来,如WNOHANG | WUNTRACED.
若传入了一个指针statusp,waitpid会把子进程返回的状态信息存到这个指针指向的int处。可以用wait.h头文件中的宏获取这些信息,如:
WIFSIGNALED(status) //若子进程被未捕获的信号(如SIGINT)终止,该宏返回1
WSTOPPED(status) //若子进程当前停止,返回1
以下对waitpid的调用都是合法的:
int status;
waitpid(pid, &status, WNOHANG);
waitpid(pid, NULL, 0);
kill函数向指定进程发送信号。
#include
#include
int kill(pid_t pid, int sig);
其中pid是目标进程的进程号;sig是信号类型。本实验中用到的有以下几种:
序号 | 名称(宏) | 默认行为 | 相应时间 |
---|---|---|---|
2 | SIGINT | 终止 | 来自键盘的中断(Ctrl+C) |
17 | SIGCHLD | 忽略 | 一个子进程停止或终止 |
18 | SIGCONT | 忽略 | 继续进程如果该进程停止 |
20 | SIGTSTP | 停止直到下一个SIGCONT | 来自终端的停止信号 |
如果不设置信号处理程序,给目标进程发送信号会按照其默认行为处理。可以通过设置信号处理程序来改变其默认行为。在给定的tsh.c中已经设置好了信号处理程序(SIGCHLD,SIGINT,SIGTSTP三种信号的),需要我们补全。
可以利用sigprocmask改变当前阻塞的信号集合。当一种信号被阻塞,当前进程即使收到该信号也不会去调用信号处理程序。sigprocmask的函数原型如下:
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
set是一个掩码,它的每一位携带了一种信号的信息。可以通过sigemptyset、sigaddset等函数来初始化该集合。下面仅给一个例子:
sigset_t mask;
//这系列函数执行成功时返回0,否则返回-1
if(sigemptyset(&mask))
unix_error("sigemptyset error!");
if(sigaddset(&mask, SIGINT))
unix_error("sigaddset error!");
上面的代码将mask清空,并向集合中添加SIGINT信号。
how参数指定了该函数处理set的方式。有三种选择:
(1)SIG_BLOCK:将set中的信号阻塞。
(2)SIG_UNBLOCK:取消阻塞set中的信号。
(3)SIG_SETMASK:设置当前阻塞集合为set中的信号。
可以通过传入一个sigset_t型的指针来保存修改信号阻塞集合前的阻塞状态,以便之后恢复。
下面是一个阻塞信号的例子:
sigset_t mask, prev_mask;
//构造掩码
if(sigemptyset(&mask))
unix_error("sigemptyset error!");
if(sigaddset(&mask, SIGINT))
unix_error("sigaddset error!");
//阻塞SIGINT
if(sigprocmask(SIG_BLOCK, &mask, &prev_mask))
unix_error("sigprocmask error!");
//do something
//取消阻塞SIGINT
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
unix_error("sigprocmask error!");
int sigsuspend(const sigset_t *mask);
这个函数用于显式地等待子进程的执行结束。由于要消除子进程与父进程的竞争,需要暂时屏蔽SIGCHLD信号;初步可以用如下语句:
sigset_t mask, prev;
//阻塞SIGCHLD
if(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,竞争已经消除,取消阻塞SIGCHLD
if(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;
//阻塞SIGCHLD
if(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
while(!pid);//等待pid变为子进程的pid
sigsuspend(&prev);//暂时解除阻塞SIGCHLD
//完全解除SIGCHLD
if(sigprocmask(SIG_SETMASK, &prev, NULL))
unix_error("sigprocmask error!");
首先判断一下字符串是否为空,再比对一下内置命令即可。(do_bgfg和listjobs还已经给出来了)在quit之前需要回收一下子进程(给列表里所有子进程发送一个SIGINT),否则它们会在后台一直运行。
int builtin_cmd(char **argv)
{
//检查是否为空
if(*argv != NULL) {
//quit
if(strcmp(*argv, "quit") == 0) {
int i;
for(i = 0; i < MAXJOBS; i++) {
if(jobs[i].pid) {
kill(-jobs[i].pid, SIGINT);
}
}
exit(0);
}
//jobs
else if(strcmp(*argv, "jobs") == 0) {
listjobs(jobs);
return 1;
}
//fg,bg
else if(strcmp(*argv, "fg") == 0 || strcmp(*argv, "bg") == 0) {
do_bgfg(argv);
return 1;
}
}
return 0; /* not a builtin command */
}
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);
}
//解除阻塞SIGCHLD
if(sigprocmask(SIG_SETMASK, &prev, NULL)) {
unix_error("sigprocmask error!");
}
return;
}
该信号处理程序在子进程返回、终止、或被停止时触发。为了回收所有子进程,要一次尽可能多地回收子进程(用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;
}
这两个处理程序区别就是发送的信号不一样。由于要给进程组发送信号,需要给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!");
}
//给前台进程的进程组发送SIGINT
kill(-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比对,即可检验结果。