CSAPP:shlab

CSAPP:shlab

该实验对应CSAPP第8章:异常控制流,可以先温习该章内容再做实验。

通读实验指南,了解实验目的:

简介

The purpose of this assignment is to become more familiar with the concepts of process control and signalling. You’ll do this by writing a simple Unix shell program that supports job control.

本实验的目的就是写一个简单的Unix shell,主要任务就是修改tsh.c文件,需修改以下7个函数。

  • 1.void eval(char *cmdline); //解释命令[70 lines]

  • 2.int builtin_cmd(char **argv);//识别命令是否为内置命令quit、jobs、fg、bg ,若是,则执行对应命令.[25
    lines]

  • quit命令会终止shell。
  • jobs 命令列出所有后台工作。
    bg 命令通过发送一个 SIGCONT 信号重新启动,然后在后台运行。参数可以是 PID 或 JID。
  • fg 命令通过发送一个 SIGCONT 信号重新启动,然后在前台运行。
  • 3.void do_bgfg(char **argv);//执行bg,fg操作[50 lines]

  • 4.void waitfg(pid_t pid);//等待前台操作完成 [20 lines]

  • 5.void sigchld_handler(int sig);//抓住 SIGCHILD [信号80 lines]

  • 6.void sigtstp_handler(int sig);//抓住 SIGINT (ctrl-c) 信号[15 lines]

  • 7.void sigint_handler(int sig);//抓住 SIGTSTP (ctrl-z) 信号[15 lines]

关于你输入的命令:
如果第一个词是内置的命令,shell会在当前进程中立即执行该命令,否则,假定该词是可执行程序的路径名,在这种情况下,shell会fork一个子进程,然后在子进程的上下文中加载并运行程序。

如果命令行以"&"结尾,则作业在后台运行,这意味着shell在打印提示符和等待下一行命令前,不会等待job结束。否则,job在前台运行,这意味着shell在等待下一个命令行之前会等待job终止。因此,在任何时间点上,最多只能有一个job在前台运行。但是,可以有任意数量的job在后台运行。

下面一段说明很好地解释了main中的参数argc、argv代表什么意思(这里环境的参数被省略了):

Typing the command line tsh> /bin/ls -l -d runs the ls program in the foreground. By convention, the shell ensures that when the program begins executing its main routine

int main(int argc, char *argv[])

the argc and argv arguments have the following values:

• argc == 3,

• argv[0] == ‘‘/bin/ls’’,

• argv[1]== ‘‘-l’’,

• argv[2]== ‘‘-d’’.

argc指的是参数个数,argv是指针数组,每个指针指向一个参数字符串。

一些注意事项:

  • 如果命令行以&结束,则tsh应该在后台运行job。 否则,它将在前台运行该job。
  • 每个作业都可以由进程ID(PID)或作业ID(JID)标识,该ID是tsh分配的正整数。 JID应该在命令行上以前缀“%”表示。 例如,“%5”表示JID 5,“ 5”表示PID5。
  • tsh应该管理回收(reap)所有的僵尸子进程。 如果任何job由于接收到未捕获到的信号而终止,则tsh应该识别此事件并打印一条带有该job的PID的消息以及对该问题的信号的描述。
  • job Id 和 process id 是有区别的,前者需要以%为前缀,后者为一个数字。这一点在处理bg、fg指令时尤为重要
  • eval中一定要
    • 在fork子进程前用sigprocmask阻塞SIGCHLD信号;fork之后解除该阻塞
    • 在加子进程到joblist前调用sigprocmask阻塞全部信号
    • 因为子进程会从父进程处继承阻塞信息,所以在execve其他二进制文件前,一定要解除信号阻塞
    • 子进程按照建议应该在 sigchld_handler被收割(reap),所以父进程需要在调用addjob之前阻塞SIGCHLD信号,以防止竞态条件
  • 当在标准unix shell中运行tsh程序时,从tsh程序fork出来的子程序会和tsh处于同一个process group;因此你需要使用setpgid来重置子进程process group id;如若不然,在ctrl-c & ctrl-z的处理上会有一些问题
  • 用sigprocmask阻塞信号,因为有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。
检验结果用到的一些命令

make

共16组测试数据,test结果要和rtest完全一样才算通过。

make test01 相当于 ./sdriver.pl -t trace01.txt -s ./tsh -a "-p"

make rtest01 相当于 ./sdriver.pl -t trace01.txt -s ./tshref -a "-p"

实验代码和注释
eval

eval解析输入的命令:
如果第一个词是内置的命令builtin_cmd(argv),shell会在当前进程中立即执行该命令。
否则,假定该词是可执行程序的路径名,在这种情况下,shell会fork一个子进程,然后在子进程的上下文中加载并运行程序。

注意:在创建子进程前要阻塞信号 ,为了避免父进程运行到addjob之前子进程就退出了,导致信号丢失,所以在fork子进程前阻塞sigchld信号,并在fork,addjob后解除

  
/* 
 * 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) 
{	
	// static char array[MAXLINE]; /* holds local copy of command line */
    // char *buf = array;          /* ptr that traverses command line */
	char *argv[MAXARGS];	//命令行参数
	pid_t pid;	//子进程PID
	int bg; // 最后是否是&,即是否后台执行,
	sigset_t mask_one, prev, mask_all;
	
	// strcpy(buf, cmdline);	//缓存命令行
	bg = parseline(cmdline, argv);
		
	if(argv[0] == NULL)
		return;//忽略空行
	
	if(!builtin_cmd(argv)){//如果不是shell的内嵌命令
		sigemptyset(&mask_one);//初始化信号量集 
		sigaddset(&mask_one, SIGCHLD);//将SIGCHLD添加到信号量集中 
		sigfillset(&mask_all);// 设置全阻塞
		sigprocmask(SIG_BLOCK, &mask_one, &prev);//阻塞信号 ,为了避免父进程运行到addjob之前子进程就退出了,所以在fork子进程前阻塞sigchld信号,addjob后解除
		if((pid = fork()) == 0){	
			 // 子进程继承了父进程的阻塞向量,也要解除阻塞,避免收不到它本身的子进程的信号 
			setpgid(0, 0);	//把pid=0放到gpid=0的进程组
			sigprocmask(SIG_SETMASK, &prev, NULL);//恢复被屏蔽的信号,防止遗漏
			//容错
			if(execve(argv[0], argv, environ) < 0){
				printf("%s: Command not found\n", argv[0]);
				exit(0);
			}
		}
	
		sigprocmask(SIG_BLOCK, &mask_all, NULL);
		addjob(jobs, pid, bg?BG:FG, cmdline);
		sigprocmask(SIG_SETMASK, &prev, NULL);		
		if(bg){
			//在后台解除屏蔽			
			printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
		}else{	
			waitfg(pid);
		}
	}
    return;
}
builtin_cmd

判断是否内置命令(1是,0否),并执行对应操作。

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
	if(!strcmp(argv[0], "quit")){
		exit(0);
	}
	if(!strcmp(argv[0], "jobs")){
		listjobs(jobs);
		return 1;
	}
	if(!strcmp(argv[0], "&")){
		// ignore singleton '&'
		return 1;
	}
	if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")){
		do_bgfg(argv);
		return 1;
	}

    return 0;     /* not a builtin command */
}
do_bgfg

在命令行中的第一个参数argv[0]可以用于判断输入的是bg还是fg命令。
PID和JID有不同的格式,分别用不同的方式读入sscanf(argv[1],"%d",&pid)和sscanf(argv[1],"%%%d",&jid),若都不是,则会输出错误提示。

  • 每个作业都可以由进程ID(PID)或作业ID(JID)标识,该ID是tsh分配的正整数。 JID应该在命令行上以前缀“%”表示。 例如,“%5”表示JID 5,“ 5”表示PID5。

看process或者job是否存在,不存在则报错,若存在则修改状态,并重启。
若是fg前台则等待前一个进程结束,若是bg后台则输出相应字符串。

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{	int jid;
	struct job_t *job;
	pid_t pid;
	sigset_t mask, prev;
	
//argv[0]判断bg还是fg
	if(argv[1] == NULL){
		printf("%s command requires PID or %%jobid argument\n",argv[0]);
		return;
	}
	
	//首先确定是pid还是jid,然后将其转化为kill的参数
	//读jid
	if(sscanf(argv[1],"%%%d",&jid) > 0){	
		job = getjobjid(jobs, jid);	//需要获得job,因为要修改job信息
		if(job == NULL || job->state == UNDEF){
			printf("%s: No such job\n", argv[1]);
			return;
		}
	//读pid
	}else if(sscanf(argv[1],"%d",&pid) > 0){	
		job = getjobpid(jobs, pid);
		if(job == NULL || job->state == UNDEF){
			printf("(%s): No such process\n", argv[1]);
			return;
		}
	}else{
		printf("%s: argument must be a PID or %%jobid\n", argv[0]);
		return;
	}
	//修改job信息
	sigfillset(&mask);
	sigprocmask(SIG_BLOCK, &mask, &prev);
	//更改状态
	if(!strcmp(argv[0], "fg")){	
		job->state = FG;
	}else{
		job->state = BG;
	}
	sigprocmask(SIG_SETMASK, &prev, NULL);
	
	pid = job->pid;
	//发送SIGCONT重启
	kill(-pid, SIGCONT);
	if(!strcmp(argv[0], "fg")){
		waitfg(pid);//若是前台则等待前一个进程结束
	}else{
		printf("[%d] (%d) %s", job->jid, pid, job->cmdline);//若是后台则输出相应字符串
	}
    return;
}
waitfg

等待pid进程不再是前台进程

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
	while(pid == fgpid(jobs)){
		sleep(1);
	}
    return;
}
sigchld_handler

//抓住 SIGCHILD 信号
内核在每当子进程终止(成为僵尸)或停止时,因为它收到SIGSTOP或SIGTSTP信号,该处理zombie进程,但不等待任何其他的。终止当前正在运行的子进程。
回收的子进程有三种状态:正常退出、信号退出和信号停止。

	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);
			job->state = ST;//注意
			printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(state));
		}
/*****************
 * Signal handlers
 *****************/

/* 
 * 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 old_errno = errno;	
	pid_t pid;
	sigset_t mask, prev;
	int state;	
	struct job_t *job;
	
	sigfillset(&mask);// 设置全阻塞
	
	while((pid = waitpid(-1, &state, WNOHANG | WUNTRACED)) > 0){	
		// WNOHANG | WUNTRACED 是立即返回
        // 用WIFEXITED(status),WIFSIGNALED(status),WIFSTOPPED(status)等来补获终止或者被停止的子进程的退出状态。	
		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);
			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; 
    return;
}
sigint_handler

捕捉 SIGINT (ctrl-c) 信号

sigtstp_handler

捕捉 SIGTSTP (ctrl-z) 信号

两个函数写法看起来相似,但是在用sigchld_handler处理的时候有区别,对于 sigtstp_handler我们需要修改job的状态job->state = ST。


/* 
 * 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;
        pid_t pid = fgpid(jobs);
        if(pid!=0){
                kill(-pid,sig);
        }
        errno = olderrno;

    return;
}

/*
 * 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;
    pid_t pid = fgpid(jobs);
    if(pid!=0){
        kill(-pid,sig);
    }
	errno = olderrno; 
    return;
}

/*********************
 * End signal handlers
 *********************/

参考

README:http://csapp.cs.cmu.edu/3e/README-shlab

说明:http://csapp.cs.cmu.edu/3e/shlab.pdf

代码:http://csapp.cs.cmu.edu/3e/shlab-handout.tar

复习:http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/recitations/rec09.pdf

博客

https://zhuanlan.zhihu.com/p/151050267

https://zhuanlan.zhihu.com/p/119034923

https://zhuanlan.zhihu.com/p/89224358

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值