哈工大计算机系统Lab4.Tiny Shell

发布时间:2022-09-05 06:00

说明

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

前置知识

fork

#include
#include
pid_t fork(void);

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

waitpid

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

#include
#include
pid_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)终止,该宏返回1
WSTOPPED(status) //若子进程当前停止,返回1

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

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

kill

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改变当前阻塞的信号集合。当一种信号被阻塞,当前进程即使收到该信号也不会去调用信号处理程序。sigprocmask的函数原型如下:

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

set

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

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!");
//阻塞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!");

sigsuspend

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!");

补充函数

builtin_cmd

首先判断一下字符串是否为空,再比对一下内置命令即可。(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

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;
}

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!");
	}
	
	//给前台进程的进程组发送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比对,即可检验结果。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号