CSAPP shelllab实验

一、准备工作

Hints
• 这是基于CSAPP教材第八章的配套实验。
• 使用跟踪文件来指导 shell 的开发。从 trace01.txt 开始,确保 shell 产生与reference shell 相同的输出。然后继续跟踪文件 trace02.txt,以此类推。
• waitpid、kill、fork、execve、setpgid 和 sigprocmaskfunctions将会被用到。waitpid 的WUNTRACED 和 WNOHANG 选项也会被用到。
• 当我们实现信号处理程序时,请确保向整个前台进程组发送 SIGINT 和 SIGTSTP 信号,在kill 函数的参数中使用“-pid”而不是“pid”。sdriver.pl 程序可以测试此错误。
• 这项任务的棘手部分之一是决定 waitfg和 sigchldhandler 函数之间的工作分配。我们建议采用以下方法:
– In waitfg, use a busy loop around the sleep function.
– In sigchld handler, use exactly one call to waitpid.
• 在 eval中,父进程必须在分叉子进程之前使用 sigprocmask来阻塞 SIGCHLD信号,然后取消阻塞这些信号,在通过调用 addjob 将子进程添加到作业列表之后,再次使用 sigprocmask。因为孩子继承了他们父母被阻止的向量,所以在执行新程序之前,孩子必须确保解除对SIGCHLD 信号的阻止。父进程需要以这种方式阻塞 SIGCHLD 信号,以避免子进程在父进程调用 addjob 之前被sigchldhandler 捕获(并因此从作业列表中删除)的竞争情况。
• more, less, vi 和 emacs等程序会对终端设置做一些奇怪的事情。不要从 shell 运行这些程序。坚持使用简单的基于文本的程序,如/bin/ls、/bin/ps 和/bin/echo。
• 当从标准的 Unix shell 运行您的 shell 时,我们的 shell 正在前台进程组中运行。如果我们的 shell创建了一个子进程,默认情况下,该子进程也是前台进程组的成员。由于键入 ctrl-c 会向前台组中的每个进程发送一个 SIGINT,因此键入 ctrl-c 会向我们的 shell 以及 shell 创建的每个进程发送一个 SIGINT,这显然是不正确的。
解决方法是:在 fork 之后,但在 execve 之前,子进程应该调用 set GID(0,0),这将子进程放在一个新的进程组中,该进程组的组标识与子进程的组标识相同。这确保前台进程组中只有一个进程,即 shell。当键入 ctrl-c 时,shell 应该捕获结果 SIGINT,然后将其转发到适当的前台作业(或者更准确地说,包含前台作业的进程组)。

实验介绍
首先输入命令tar xvf shlab-handout.tar来扩展目标文件。不要解压之后再上传到服务器,否则会出现下图所示的情况。
在这里插入图片描述
然后输入make 命令来编译tsh.c文件,如果程序被修改则需要先输入make clean指令。
在这里插入图片描述

  • eval:读取指令并产生子进程执行。
  • builtin_cmd:执行内置命令,bg、fg、quit、jobs。 do_bgfg:处理fg和bg操作。
  • waitfg:等待前台进程结束。
  • sigchld_handler:捕捉SIGCHILD信号,给子进程收尸,避免产生僵尸进程。
  • sigint_handler:捕捉SIGINT(ctrl-c)信号,将SIGINT信号发给前台进程组。
  • sigtstp_handler:捕捉SIGTSTP(ctrl-z)信号,将SIGTSTP信号发给前台进程组。
  • 使用make testXX和make rtestXX指令比较traceXX.txt文件在编写的shell和reference shell的运行结果;或者也可以使用”./sdriver.pl -t traceXX.txt -s ./tsh -a “-p”和”./sdriver.pl -t traceXX.txt -s ./tshref -a “-p”

跟系统任务相关的几个命令:fg、bg、jobs、&、ctrl+z。

  1. & :这个用在一个命令的最后,可以把这个命令放到后台执行;
  2. ctrl + z:可以将一个正在前台执行的命令放到后台,并且暂停;
  3. jobs:查看当前有多少在后台运行的命令,列举出后台作业信息([作业号] 运行状态 作业名称);
  4. fg:将后台中的命令调至前台继续运行,如果后台中有多个命令,可以用 fg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid);
  5. bg:将一个在后台暂停的命令,变成继续执行,如果后台中有多个命令,可以用bg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid)。

二、对实验的一些研究

Step1
输入make rtest01和make test01;因为trace01.txt中只有CLOSE和WAIT命令,在EOF上正常终止。
在这里插入图片描述
Step2
输入make rtest02和make test02;tshref能够正常退出,而tsh因为quit的内置命令还没有写,所以不能正常退出。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210129081532266.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTk3NTU3NQ==,size_16,color_FFFFFF,t_70

Step3
trace03.txt中tsh>quit是打开bin目录下的echo可执行文件,在foreground开启一个子进程运行它(如果末尾有&,则说明是在background运行)。
运行echo进程时,通过tsh>quit命令。调用tsh并执行内置命令quit,退出子进程。
最后在tsh中执行内置命令quit,退出tsh进程,回到终端。
在这里插入图片描述
Step4
了解tsh.c中作业表job struct和操作管理函数(如addjob())

(1)文件符号:
空格:用来分隔命令和参数或者参数与参数;
&:如果一个命令以&结尾,shell应该在后台运行它,否则在前台运行;
#:以 # 开头的行就是注释,会被解释器忽略。
2)命令:
包括内建命令和外部命令。内置命令包括quit、jobs、bg、fg等。可以使用type来确定一个命令是否是内置命令。

在这里插入图片描述
用户程序myspin:使用myspin 指令可将进程挂起n秒

int main(int argc, char **argv)  {
     int i, secs;
     pid_t pid; 
     //判断命令长度是否为2,若不为2则输出错误信息并终止程序
     if (argc != 2) { 	fprintf(stderr, "Usage: %s <n>\n", argv[0]); 	exit(0);
     }
     secs = atoi(argv[1]); //将字符串转换为整型
     for (i=0; i < secs; i++) //将进程挂起secs秒
        sleep(1);
 	
     pid = getpid(); 
     if (kill(pid, SIGINT) < 0)
        fprintf(stderr, "kill (int) error");
     exit(0);
}

在这里插入图片描述

Step5
编程实现jobs内建命令,使用trace05验证。在原有builtin_cmd函数中添加一个判断函数,如果参数是jobs,则执行listjobs函数的功能。

if(!strcmp(argv[0],"jobs"))
{
     listjobs(jobs);
     return 1;
}
/* listjobs - Print the job list */
void listjobs(struct job_t *jobs) 
{
    int i;
    
    for (i = 0; i < MAXJOBS; i++) {
	if (jobs[i].pid != 0) {
	    printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
	    switch (jobs[i].state) {
		case BG: 
		    printf("Running ");
		    break;
		case FG: 
		    printf("Foreground ");
		    break;
		case ST: 
		    printf("Stopped ");
		    break;
	    default:
		    printf("listjobs: Internal error: job[%d].state=%d ", 
			   i, jobs[i].state);
	    }
	    printf("%s", jobs[i].cmdline);
	}
    }
}

定义一个作业的结构体,该结构体具有的属性有作业的作业号、进程号、状态、命令行参数

struct job_t {              /* The job struct */
    pid_t pid;              /* job PID */
    int jid;                /* job ID [1, 2, ...] */
    int state;              /* UNDEF, BG, FG, or ST */
    char cmdline[MAXLINE];  /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */

下列函数进行清空、初始化、最大作业号、添加、删除功能。

void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs); 
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *jobs, pid_t pid);

在这里插入图片描述
Step6
编程实现sigint_handler()、waitfg(),sigchld_handler(),验证trace06~07.

了解接收信号、信号处理、信号阻塞概念

  1. 接收信号:当目的进程被内核强迫以方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理 程序的用户层函数捕获这个信号。
  2. 信号处理:signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
    ①如果handler是SIG_IGN,那么忽略类型为signum的信号;
    ②如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认模式;
    ③否则,handler就是用户定义的函数的地址,这个函数称为信号处理程序;
  3. 当一个程序要捕获多个信号时,会有以下问题:
    待处理信号被阻塞;待处理信号不会排队等待;系统调用可以被中断。
  4. 信号阻塞:Unix信号处理程序通常会阻塞当前处理程序正在处理类型的待处理信号。

在这里插入图片描述在这里插入图片描述
Step7
编程实现sigtstp_handler捕获TSTP响应
在这里插入图片描述
Step8
编程实现内置命令bg和fg的do_bgfg()处理函数;比较trace09~10执行不同结果,在trace09中执行完bg %2命令后会将作业2放到后台运行,所以最终【2】后面会显示running;在trace10中执行fg %1会将后台的作业1停止之后添加到前台运行。
在这里插入图片描述
在这里插入图片描述
Step9
Trace11:./mysplit 4创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGINT 信号使子进程终止。
在这里插入图片描述

Trace12:./mysplit 4 创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGTSTP 信号使子进程停止直到下一个 SINCONT,因此执行 jobs 指令,可以看到子进程处 于 Stopped 状态,用 ps a 指令查看。
在这里插入图片描述

Trace13:./mysplit 4 创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGTSTP 信号使子进程停止直到接收一个 SINCONT 信号,因此执行 jobs 指令,可以看到子进程处于 Stopped 状态,用 ps a 指令查看;然后执行 fg %1 指令, 将后台停止的作业 1 切换至前台运行,再次使用 ps a 指令查看。
在这里插入图片描述
Step10

Trace14:处理输入未实现的命令,fg 和 bg 参数不正确等错误情况。
在这里插入图片描述
Step11
因为没有实现命令 bogus,所以执行./bogus 命令会报错。执行./myspin 10 命令,挂起10秒,但在挂起2秒后被SIGINT信号终止。./myspin 3 &和./myspin 4 &分别在后台执行./myspin 3 和./myspin 4 命令,且前者作业号为1,后者作业号为2。此时使用 jobs 命令查看,两者都在后台运行。使用 fg %1 命令将作业1切换至前台运行,挂起2秒后,发送 SIGTSTP 信号使其停止直到接收一个SINCONT 信号。此时再次使用 jobs 命令查 看,发现作业1已经处于Stopped状态,而作业2仍然处于 Running 状态。因为没有作业3,所以执行bg %3 命令会报错。然后执行 bg %1命令将已经在后台停止的作业1切换至运行状态,即重启作业1。此时使用jobs命令查看,作业1和作业2都处于后台运行状态。然后执行fg %1 命令将作业从后台运行状态切换至前台运行状态。
在这里插入图片描述在这里插入图片描述

三、代码

1、eval

void eval(char *cmdline) 
{
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    sigset_t prev;
    sigset_t mask;
    pid_t pid;
    sigprocmask(SIG_BLOCK,NULL,&mask);
    sigaddset(&mask,SIGCHLD);
    strcpy(buf,cmdline);
    bg=parseline(buf,argv);//内置函数,前台任务返回0,后台任务返回1
    if(argv[0]==NULL) return;
    if(!builtin_cmd(argv))//非内置指令时执行
    {
        sigprocmask(SIG_BLOCK,&mask,&prev);//屏蔽SIGCHLD
        if((pid=fork())==0)//子进程
        {
            sigprocmask(SIG_SETMASK,&prev,NULL);
            setpgid(0,0);//每个任务单独开一个进程组,方便信号处理中使用kill
            if(execve(argv[0],argv,environ)<0)
            {
                printf("%s:Command not found.\n",argv[0]);
                exit(0);
            }
        }
   
        else//父进程
        {
            if(bg)//后台任务
                addjob(jobs,pid,BG,cmdline);
            else//前台任务
                addjob(jobs,pid,FG,cmdline);
            sigprocmask(SIG_SETMASK,&prev,NULL);
            if(bg)//后台任务不等待任务结束
                printf("[%d](%d)%s",pid2jid(pid),pid,cmdline);
            else//前台任务,等待任务结束
                waitfg(pid);
        }
    }
    return;
}

2、builtin_cmd

int builtin_cmd(char** argv)
{
	if (!strcmp(argv[0], "quit")) {
		exit(0);
	}
	if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
		do_bgfg(argv);
		return 1;
	}
	if(!strcmp(argv[0], "jobs")) {
        //访问全局变量,阻塞所有信号
		//sigset_t mask_all, prev_mask;
		//sigfillset(&mask_all);
		//sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
		listjobs(jobs);
		//sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		return 1;
	}
	if(!strcmp(argv[0], "&")){
		return 1;
	}
	return 0; /* not a builtin command */
}
char* argv[MAXARGS];
	char buf[MAXLINE];
	int bg;
	pid_t pid;

	strcpy(buf, cmdline);
	bg = parseline(buf, argv);
	if (argv[0] == NULL) {
		return;
	}
	if (!builtin_cmd(argv)) {
		sigset_t mask_chld, prev_mask, mask_all;
		sigemptyset(&mask_chld);
		sigaddset(&mask_chld, SIGCHLD);
		sigfillset(&mask_all);
        
		/*因为子进程可能在addjob前就结束并调用deleltejob,所以我们要先阻塞掉SIGCHLD,
		保证addjob操作成功*/
		sigprocmask(SIG_BLOCK, &mask_chld, &prev_mask);

		if ((pid = fork()) == 0) {
            //子进程默认继承父进程的mask,所以这里要恢复一下
			sigprocmask(SIG_SETMASK, &prev_mask, NULL);
			setpgid(0, 0); //令进程组号等于进程号
			if (execve(argv[0], argv, environ) <= 0) {
				printf("%s: Command not found\n", argv[0]);
				exit(0);
			}
		}

		// addjob涉及到全局变量的操作,需要保证操作的原子性,故这里阻塞掉所有信号
		sigprocmask(SIG_SETMASK, &mask_all, NULL);
		addjob(jobs, pid, bg?BG:FG, cmdline);
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		
		// 在线程终止前需要打印些相关信息,所以addjob完还要阻塞一会儿SIGCHLD
		sigprocmask(SIG_BLOCK, &mask_chld, NULL);
		
		if (!bg) {
			waitfg(pid);
		} else {
            // 同上,操作全局变量时阻塞
			sigprocmask(SIG_SETMASK, &mask_all, NULL);
			printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
		}
        // 操作结束后解除阻塞
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
	}

	return;

3、do_bgfg

void do_bgfg(char **argv) 
{
    sigset_t mask_all, prev_mask;
	sigfillset(&mask_all);
    // 访问全局变量jobs,阻塞所有信号
	sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);

	struct job_t *job;
	int pid;
	if(argv[1] == NULL){
		printf("%s command requires PID or %%jobid argument\n", argv[0]);
		return;
	}
	else if(argv[1][0] == '%'){
		int jid = atoi(argv[1] + 1);
		job = getjobjid(jobs, jid);
		if(job == NULL) {
			printf("%%%d: No such job\n", jid);
			return;
		}
		pid = job->pid;
	}
	else {
		pid = atoi(argv[1]);
		if(pid <= 0){
			printf("%s: argument must be a PID or %%jobid\n", argv[0]);
			return;
		}
		job = getjobpid(jobs, pid);
		if(job == NULL){
			printf("(%d): No such process\n", pid);
			return;
		}
	}
	if(!strcmp(argv[0], "bg")){
		job->state = BG;
		printf("[%d] (%d) %s", job->jid, pid, job->cmdline);
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
		return;
	}
	else if(!strcmp(argv[0], "fg")){
		job->state = FG;
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
		waitfg(pid); // 子进程切换到了前台,故要等待它执行完
		return;
	}
	else if(!strcmp(argv[0], "kill")){
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		kill(-pid,SIGQUIT); // 对子进程及其后代发送,故加负号
		return;
	}
	return;
}

4、waitfg

void waitfg(pid_t pid)
{
    sigset_t m;
    sigemptyset(&m);
    while(pid==fgpid(jobs))
        sigsuspend(&m);//有信号时被唤醒检查前台进程pid是否变化,变化则说明前台进程结束。
    return;
}

5、sigchld_handler

void sigchld_handler(int sig) 
{
    pid_t pid;
    int status;
    while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)
    {
        if(WIFEXITED(status))//正常结束
        {
            deletejob(jobs,pid);
        }
        if(WIFSTOPPED(status))//任务挂起时
        {
            struct job_t *job=getjobpid(jobs,pid);
            int jid=pid2jid(pid);
            printf("Job [%d] (%d) stopped by signal %d\n",jid,pid,WSTOPSIG(status));
            job->state=ST;
        }
        if(WIFSIGNALED(status))//任务被终止
        {
            int jid=pid2jid(pid);
            printf("Job [%d] (%d) terminated by signal %d\n",jid,pid,WTERMSIG(status));
            deletejob(jobs,pid);
        }

    }
    return;
}

6、sigint_handler

void sigint_handler(int sig) 
{
    pid_t pid=fgpid(jobs);
    if(pid!=0)
    {
        kill(-pid,sig);
    }
    return;
}

7、sigtstp_handler

void sigtstp_handler(int sig) 
{
    pid_t pid=fgpid(jobs);
    if(pid>0)
    {
        kill(-pid,sig);
    }
    return;
}
  • 8
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值