简介

CSAPP实验介绍

学生实现他们自己的带有作业控制的Unix Shell程序,包括Ctrl + C和Ctrl + Z按键,fg,bg,和 jobs命令。这是学生第一次接触并发,并且让他们对Unix的进程控制、信号和信号处理有清晰的了解。

什么是Shell?

​ Shell就是用户与操作系统内核之间的接口,起着协调用户与系统的一致性和在用户与系统之间进行交互的作用。

​ Shell最重要的功能是命令解释,从这种意义上说,Shell是一个命令解释器。

​ Linux系统上的所有可执行文件都可以作为Shell命令来执行。当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。在查找该命令时分为两种情况:

(1)用户给出了命令的路径,Shell就沿着用户给出的路径进行查找,若找到则调入内存,若没找到则输出提示信息;

(2)用户没有给出命令的路径,Shell就在环境变量Path所制定的路径中依次进行查找,若找到则调入内存,若没找到则输出提示信息。

关于本次实验

本次实验需要我们熟读CSAPP第八章异常控制流

需要设计和实现的函数:

Evaluate the command line that the user has just typed in.

If the user has typed a built-in command then execute it immediately.

Execute the builtin bg and fg commands.

Block until process pid is no longer the foreground process

TinyShell辅助函数:

/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv); //解析命令行参数
void sigquit_handler(int sig);//退出的处理函数
/*jobs是全局变量,存储每一个进程的信息。*/
/*jid为job编号ID,pid为进程ID*/
void clearjob(struct job_t *job);//清除所有工作
void initjobs(struct job_t *jobs);//初始化工作结构体
int maxjid(struct job_t *jobs); //返回jobs中jid的最大值
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);//添加job
int deletejob(struct job_t *jobs, pid_t pid); //删除job
pid_t fgpid(struct job_t *jobs);//返回前台运行job的pid
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);//返回对应pid的job
struct job_t *getjobjid(struct job_t *jobs, int jid); //返回jid对应的job
int pid2jid(pid_t pid); //pid转jid
void listjobs(struct job_t *jobs);//遍历
void usage(void);//帮助信息
void unix_error(char *msg);//报错unix-style error routine
void app_error(char *msg);//报错application-style error routine
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);//信号设置

实验要求

回顾

fork

pid_t fork(void)

在函数调用处创建子进程。

父进程函数返回子进程的PID

子进程函数返回0

waitpid

一个进程可以通过waitpid函数来等待它的子进程终止或者停止。

pid_t waitpid(pid_t pid, int *statusp, int options);

pid:判定等待集合的成员

options:修改默认行为

options中有如下选项:

  1. WNOHANG:若当前没有等待集合中的子进程终止,则立即返回0
  2. WUNTRACED:等待直到某个等待集合中的子进程停止或返回,并返回这个子进程的pid。
  3. WCONTINUED:等待直到某个等待集合中的子进程重新开始执行或终止。
  4. 组合WNOHANG | WUNTRACED:立即返回,如果等待集合中的子进程都没有被停止或终止,则返回0。如果有,则返回PID。

statusp:检查已回收子进程的退出状态

如果statusp参数非空,那么waitpid就会在status中放入关于导致返回的子进程的状态信息,status是statusp指向的值。

kill函数

int kill(pid_t pid, int signo);

安全的信号处理

目的

让信号处理程序和主程序它们可以安全地,无错误地,按照我们预期地并发地运行。

方法

  1. 处理程序尽可能简单。

  2. 在处理程序只调用异步信号安全的函数。

    1. 可重入的(只访问局部变量)。
    2. 不能被信号处理程序中断。
  3. 保存和恢复errno。避免干扰其他依赖于errno的部分。解决方法是用局部变量存储,再恢复。

void Example(int sig)
{
	int olderrno = errno;
    /*
    this is your code
    */
    errno = olderrno;
}
  1. 阻塞所有信号,保护对共享全局变量数据结构的访问。
  2. volatile声明全局变量。
  3. sig_atomic_t声明标志。

例:在添加job时,阻塞信号,因为jobs是全局变量。

This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP signals until we can add the job to the job list. This eliminates some nasty races between adding a job to the job list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.

注意

  1. 不可以用信号来对其他进制中发生的事件计数。
  2. 使用原子(atomic)函数如sigsuspend函数消除潜在的竞争并提高效率。

实验

eval

要点分析:

  1. 创建子进程前需要阻塞信号,防止竞争。

  2. 将子进程加入到jobs后,需要恢复,即解除阻塞。

  3. 创建子进程时,为子进程创建一个新的进程组。

/*
 * eval - Evaluate the command line that the user has just typed in
 * 
 * If the user has requested a built-in command (quit, jobs, bg or fg)
 * then execute it immediately. Otherwise, fork a child process and
 * run the job in the context of the child. If the job is running in
 * the foreground, wait for it to terminate and then return.  Note:
 * each child process must have a unique process group ID so that our
 * background children don't receive SIGINT (SIGTSTP) from the kernel
 * when we type ctrl-c (ctrl-z) at the keyboard.  
*/
void eval(char *cmdline) 
{
    /* $begin handout */
    char *argv[MAXARGS]; /* argv for execve() */
    int bg;              /* should the job run in bg or fg? */
    pid_t pid;           /* process id */
    sigset_t mask;       /* signal mask */
    /* Parse command line */
    bg = parseline(cmdline, argv); 
    if (argv[0] == NULL)  
		return;   /* ignore empty lines */
    if (!builtin_cmd(argv)) { 
			/* 
		* This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP
		* signals until we can add the job to the job list. This
		* eliminates some nasty races between adding a job to the job
		* list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.  
		*/
		if (sigemptyset(&mask) < 0)
			unix_error("sigemptyset error");
		if (sigaddset(&mask, SIGCHLD)) 
			unix_error("sigaddset error");
		if (sigaddset(&mask, SIGINT)) 
			unix_error("sigaddset error");
		if (sigaddset(&mask, SIGTSTP)) 
			unix_error("sigaddset error");
		if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
			unix_error("sigprocmask error");
		/* Create a child process */
		if ((pid = fork()) < 0)
			unix_error("fork error");
		/* 
		* Child  process 
		*/
		if (pid == 0) {
			/* Child unblocks signals */
			sigprocmask(SIG_UNBLOCK, &mask, NULL);
			/* Each new job must get a new process group ID 
			so that the kernel doesn't send ctrl-c and ctrl-z
			signals to all of the shell's jobs */
			if (setpgid(0, 0) < 0) 
				unix_error("setpgid error"); 
			/* Now load and run the program in the new job */
			if (execve(argv[0], argv, environ) < 0) {
				printf("%s: Command not found\n", argv[0]);
				exit(0);
			}
	}
	/* 
	 * Parent process
	 */
	/* Parent adds the job, and then unblocks signals so that
	   the signals handlers can run again */
	addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
	sigprocmask(SIG_UNBLOCK, &mask, NULL);
	if (!bg) 
	    waitfg(pid);
	else
	    printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
    }
    /* $end handout */
    return;
}

builtin_cmd

要点分析:

调用listjobs时,属于访问全局变量,需要阻塞和解除阻塞。

/*
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
	if(*argv == NULL)
	{
		return 0;
	}
	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");
	}
	if(! strcmp(argv[0], "quit"))
	{
		if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问全局变量需要阻塞
		{
			unix_error("sigprocmask error!");
		}
		int i;
		for(i = 0; i < MAXJOBS; i ++ )//退出时终止所有所有的子进程
		{
			if(jobs[i].pid)
			{
				kill(- jobs[i].pid, SIGINT);
			}
		}
		if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
		{
			unix_error("sigprocmask error!");
		}
		exit(0);//Shell exit
	}else if(! strcmp(argv[0], "jobs"))
	{
		if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//同理,访问全局变量
		{
			unix_error("sigprocmask error!");
		}
		listjobs(jobs);//遍历
		if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
		{
			unix_error("sigprocmask error!");
		}
		return 1;
	}else if(! strcmp(argv[0], "&"))
	{
		return 1;// &也是内置命令,需要返回1
	}else if(! strcmp(argv[0], "fg") || ! strcmp(argv[0], "bg"))
	{
		do_bgfg(argv);
		return 1;
	}
    return 0;     /* not a builtin command */
}

do_bgfg

要点分析:

  1. 需要保证参数正确,即将不正确的情况排除。

  2. 区别jid和pid。

/*
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    /* $begin handout */
    struct job_t *jobp = NULL;
    /* Ignore command if no argument */
    if (argv[1] == NULL) {
		printf("%s command requires PID or %%jobid argument\n", argv[0]);
		return;
    }
    /* Parse the required PID or %JID arg */
    if (isdigit(argv[1][0])) {
		pid_t pid = atoi(argv[1]);
		if (!(jobp = getjobpid(jobs, pid))) {
	    	printf("(%d): No such process\n", pid);
	    	return;
		}
    }
    else if (argv[1][0] == '%') {
		int jid = atoi(&argv[1][1]);
		if (!(jobp = getjobjid(jobs, jid))) {
	    	printf("%s: No such job\n", argv[1]);
	    	return;
		}
    }	    
    else {
		printf("%s: argument must be a PID or %%jobid\n", argv[0]);
		return;
    }
    /* bg command */
    if (!strcmp(argv[0], "bg")) { 
		if (kill(-(jobp->pid), SIGCONT) < 0)
	    	unix_error("kill (bg) error");
		jobp->state = BG;
		printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);
    }
    /* fg command */
    else if (!strcmp(argv[0], "fg")) { 
		if (kill(-(jobp->pid), SIGCONT) < 0)
	    		unix_error("kill (fg) error");
		jobp->state = FG;
		waitfg(jobp->pid);
    }
    else {
		printf("do_bgfg: Internal error\n");
		exit(0);
    }
    /* $end handout */
    return;
}

waitfg

要点分析:

  1. 在等待的循环不使用可能会无限休眠的pause,也不使用太慢的sleep。

  2. 在等待的循环中使用sigsuspend函数,因为它是原子的。

  3. 在等待前,需阻塞chld信号。

/*
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
	sigset_t mask, prev_mask;
	if(sigemptyset(&mask))
	{
		unix_error("sigempty error!");
	}
	if(sigaddset(&mask, SIGCHLD))
	{
		unix_error("sigaddset error!");
	}
	if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问jobs先阻塞chld信号
	{
		unix_error("sigprocmask error!");
	}
	while(fgpid(jobs) == pid)
	{
		sigsuspend(&prev_mask);//消除竞争
	}
	//
	if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
	{
		unix_error("sigprocmask error!");
	}
    return;
}

sigchld_handler

要点分析:

  1. 删除作业信息时,属于访问全局变量,需要阻塞全部信号。

  2. 保存恢复errno。

/*
 * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
 *     a child job terminates (becomes a zombie), or stops because it
 *     received a SIGSTOP or SIGTSTP signal. The handler reaps all
 *     available zombie children, but doesn't wait for any other
 *     currently running children to terminate.  
 */
void sigchld_handler(int sig) 
{
	int olderrno = errno;
	pid_t pid;
	int status;
	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");
	}
	while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
	{
		if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问全局变量前阻塞所有信号
		{
			unix_error("sigprocmask error!");
		}
		struct job_t *temp = getjobpid(jobs, pid);
		if(WIFEXITED(status))//正常结束
		{
			deletejob(jobs, pid);
		}else if(WIFSIGNALED(status))//被未捕获的信号终止
		{
			int jid = pid2jid(pid);
			printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
			deletejob(jobs, pid);
		}else if(WIFSTOPPED(status))//停止的信号
		{
			temp->state = ST;
			int jid = pid2jid(pid);
			printf("Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status));
		}
		fflush(stdout);//之前printf输出,所以刷新
		if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
		{
			unix_error("sigprocmask error!");
		}
	}
	errno = olderrno; 
    return;
}

sigint_handler

/*
 * sigint_handler - The kernel sends a SIGINT to the shell whenver the
 *    user types ctrl-c at the keyboard.  Catch it and send it along
 *    to the foreground job.  
 */
void sigint_handler(int sig) 
{
	int olderrno = errno;//保存和恢复errno
	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");//阻塞所有信号
	}
	if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
	{
		unix_error("sigprocmask error!");
	}
    	pid_t pid = fgpid(jobs);
	if(pid != 0)//对进程组发送SIGINT
		kill(-pid, sig);
	if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
	{
		unix_error("sigprocmask error!");
	}
	errno = olderrno;
    return;
}

sigtstp_handler

/*
 * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
 *     the user types ctrl-z at the keyboard. Catch it and suspend the
 *     foreground job by sending it a SIGTSTP.  
 */
void sigtstp_handler(int sig) 
{
	int olderrno = errno;
	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");//阻塞所有信号来访问全局变量
	}
	if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
	{
		unix_error("sigprocmask error!");
	}
	pid_t pid = fgpid(jobs);
	if(pid != 0)//向进程组发送SIGTSTP
		kill(-pid, sig);
	if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
	{
		unix_error("sigprocmask error!");
	}
	errno = olderrno;
    return;
}

测试

对比tsh和参考shell程序tshref,测试了16组例子。

发表回复