SHLAB

   

实验题目:SHLAB

实验目的:

通过构建一个简单的类Unix/Linux Shell来熟悉进程控制和信号的概念。基于已经提供的“微Shell”框架tsh.c,完成部分函数和信号处理函数的编写工作。使用sdriver.pl可以评估你所完成的shell的相关功能。其中需要实现以下六个函数:

eval             主例程,用以分析和解释命令行;

builtin_cmd      执行bg和fg内置命令;

waitfg           等待前台作业执行;

sigchld_handler  响应处理SIGCHILD信号;

sigint_handler   响应处理SIGINT(ctrl-c)信号;

sigtstp_handler  相应处理SIGSTP(ctrl-z)信号。

实验环境:16.04 32位Ubuntu系统

实验内容及操作步骤:

前言:

仔细阅读shlab-overview.pdf和CSAPP第八章。每次修改了tsh.c文件,都需要make它以重新编译。在Linux终端中直接运行tsh(./tsh)就可以进入你所编写完成的tiny shell tsh>了,而运行./tshref这个已经实现的shell,将其输出结果与你所实现的./tsh 输出结果比较,是否一致。

对tsh需要实现:

①输入ctrl-c (ctrl-z) 应导致将SIGINT(SIGTSTP)信号发送到当前前台的作业,以及该作业的所有后代(例如,它派生的任何子进程)。如果没有前台作业,那么信号应该没有效果。

②如果命令行以&结束,那么tsh应该在后台运行该作业。否则,它应该在前台运行作业。

③每个作业都可以通过进程ID (PID) 或作业 ID (JID) 来识别,它是一个正整数,由tsh 分配。 JID 应该在命令行上用前缀 '%' 表示。例如,“%5”表示 JID 5,“5”表示 PID 5。

④tsh 应该支持以下内置命令:

–quit 命令终止shell。

–jobs 命令列出所有后台作业。

–bg <job> 命令通过发送一个 SIGCONT 信号重新启动 <job>,然后让它在后台运行,<job> 参数可以是 PID 或 JID。

–fg <job> 命令通过发送一个 SIGCONT 信号重新启动 <job>,然后让它在前台运行, <job> 参数可以是 PID 或 JID。

⑤tsh应该收回它所有的僵尸子进程。如果任何作业因为收到一个信号而终止导致它没有捕获,那么 tsh 应该识别这个事件并打印一条带有作业 PID 和描述违规信号的信息。

此外提一嘴job和process的区别,job是相对shell 来说的,在shell中执行一条命令,实际上就是提交了一个job,只不过有的job需要运行很长时间,有的job很快就结束。而命令经过shell解析后,交给系统内核执行,会fork出很多进程。

而用于的测试的程序,一些功能如下:

myint.c:Sleeps for <n> seconds and sends SIGINT to itself.

mysplit.c:Fork a child that spins for <n> seconds in 1-second chunks.

myspin.c:Sleeps for <n> seconds in 1-second chunks.

mystop.c:Sleeps for <n> seconds and sends SIGTSTP to itself.

任务一:实现quit

(1.1)测试

trace01.txt:

 

运行make test01和make rtest01,比较tsh和tshref,发现他们对于CLOSE和WAIT指令的执行结果相同,都能在EOF上正常终止,也就是说CLOSE和WAIT并不是我们要实现的shell的内置指令。

trace02.txt:

 

运行make test02和make rtest02,比较tsh和tshref,发现tsh不能正常退出,进程进入wait状态。

(1.2)实现

由上述结果可知,我们需要为tsh增加quit的内置指令。首先我们对eval函数进行修改(修改后仍不是完整版),使其可以解析quit指令:

 

然后我们再对builtin_cmd函数进行修改,使其可以识别输入的是否是内置命令:

 

用trace03.txt来测试是否实现了quit指令:

 

任务2:运行后台作业并实现jobs内置命令

(1.1)测试

trace04.txt:

 

运行make test04和make rtest04,比较tsh和tshref,发现tsh不能在后台执行myspin(将进程挂起来n秒)。

(1.2)实现

为了实现在后台运行进程的功能,我们要对eval函数进行进一步修改:

在实际测试中,builtin_cmd中是否添加对&的识别对于最后结果似乎没有太多影响,可能系统内部就支持这个功能,所以代码是否添加对&的识别都不影响。而且由于&实际上是在命令行最后的位置,不能用argv[0]去测试。

用trace04.txt来测试:

 

(2.1)测试

trace05.txt:

 

 

执行myspin函数,在后台先睡眠2s,然后再执行一次,在后台睡眠3s,最后输入jobs展示所有作业。运行make test05和make rtest05,比较tsh和tshref,发现tsh缺少jobs这个内置命令,需要我们实现。

(2.2)实现

注意,hints 6中提到,在eval中,父进程应在fork之前用sigprocmask阻塞SIGCHLD信号,fork后解除信号阻塞。在通过调用addjob将子进程添加到作业列表后,再次使用sigprocmask。因为子进程会继承父进程的阻塞集,所以子进程必须确保在执行新进程前解除阻塞SIGCHLD信号。父进程需以这种方式阻塞SIGCHLD信号,避免在父进程调用addjob之前,SIGCHLD处理程序获取子进程(从而从任务列表中删除)的竞争状态。

通俗来说,就是fork以后会在job列表里添加job,而信号处理函数sigchld_handler回收进程后才会在job列表中删除,如果信号来的比较早,那么就可能会发生先删除后添加的情况。这样这个job永远不会在列表中消失了(内存泄露),所以我们要先阻塞SIGCHLD ,添加以后再还原。

其中sigprocmask函数是关键,如果不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数,这种情况是通过阻塞信号实现的,即使用sigprocmask函数。它有三个参数:int how,const sigset_t* set,const sigset_t* oldset。how用于指定信号修改的方式,可能选择有三种:

SIG_BLOCK,将set所指向的信号集中包含的信号加到当前的信号掩码中。即信号掩码和set信号集进行或操作。

SIG_UNBLOCK,将set所指向的信号集中包含的信号从当前的信号掩码中删除。即信号掩码和set进行与操作。

SIG_SETMASK,将set的值设定为新的进程信号掩码。即set对信号掩码进行了赋值操作。

set为指向信号集的指针,在此专指新设的信号集,如果仅想读取现在的屏蔽值,可将其置为NULL。oldset也是指向信号集的指针,在此存放原来的信号集。可用来检测信号掩码中存在什么信号。

因此eval函数修改如下:

当一个子进程终止或者停止时,内核会发送一个SIGCHLD信号给父进程。因此父进程必须回收子进程,以避免在系统中留下僵死进程。父进程捕获这个SIGCHLD信号,回收一个子进程。所以我们需要对sigchld_handler函数进行修改:

如果是前台的命令,父进程需要等待现在的前台作业终止,则调用waitfg循环等待该进程结束:

用trace05.txt来测试:

任务3:实现SIGINT和SIGSTOP信号处理函数(sigint_handler和sigtstp_handler)

(1.1)测试

trace06.txt:

 

trace07.txt:

可以看出trace06和trace07重在测试SIGINT信号处理函数。

 

(1.2)实现

我们先要了解SIGINT:SIGINT信号默认行为为终止,是来自键盘的中断CTRL+C,在键盘上输入 CTRL+C 会导致一个SIGINT 信号被发送到外壳。外壳捕获该信号,然后发送 SIGINT 信号到这个前台进程组中的每个进程。在默认情况下,结果是终止前台作业。

实现sigint_handler涉及到以下步骤:

①获取前台进程,用fgpid(jobs)获取前台进程组判断当前是否有前台进程,如果没有直接返回。

②用kill(-pid,sig)函数发送SIGINT信号给前台进程组

kill函数可以用来送参数sig 指定的信号给参数pid 指定的进程参数pid 有几种情况:

 

pid>0将信号传给进程识别码为pid 的进程
pid=0,将信号传给和目前进程相同进程组的所有进程;
pid=-1,将信号广播传送给系统内所有的进程;
pid<0,将信号传给进程组识别码为pid绝对值的所有进程。

 

同时,要在eval中调用setpid函数。setpgid(pid_t pid, pid_t pgid)将参数pid指定进程所属的组识别码设为参数pgid指定的组识别码如果参数pid为0则会用来设置目前进程的组识别码如果参数pgid为0, 则会以目前进程的进程识别码来取代。

例如,如果进程15213是调用进程,那么setpgid(O,O)会创建一个新的进程组,其进程组ID15213, 并且把进程15213加入到这个新的进程组中。

 

此外,sigchld_handler也要进行进一步修改,为了方便,我们放到(2.2)中一起修改。

(2.1)测试

需要实现sigtstp_handler函数。

(2.2)实现

我们先来了解SIGTSPT:SIGTSPT信号默认行为是停止直到下一个SIGCONT,是来自终端的停止信号,在键盘上输入CTR+Z会导致一个 SIGTSPT信号被发送到外壳。外壳捕获该信号,然后发送SIGTSPT信号到这个前台进程组中的每个进程。在默认情况下,结果是停止或挂起前台作业。

实现sigtstp_handler涉及到以下步骤(由于信号是参数传入,所以实际上与sigint_handler一致):

①获取前台进程,用fgpid(jobs)获取前台进程pid,判断当前是否有前台进程,如果没有直接返回。

②用kill(-pid,sig)函数发送SIGTSPT信号给前台进程组。另外对于kill函数,如果 pid 小于零才会发送信号sig给进程组中的每个进程。所以说要发送SIGTSPT信号给前台进程组所有进程需要传递-pid。

 

 

同时,sigchld_handler也要更改,使得sigchld_handler可以实现:

①用while循环调用waitpid(暂时停止目前进程的执行, 直到有信号来到或子进程结束,pid>0 意为等待任何子进程识别码为pid的子进程)直到它所有的子进程终止。

②检查己回收子进程的退出状态。

其中一些函数如下:

WNOHANG:如果没有任何已经结束的子进程则马上返回, 不予以等待。

WUNTRACED:如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会,子进程的结束状态返回后存于status, 底下有几个宏可判别结束情况:

WIFEXITED:子进程通过调用exit 或者return正常终止;

WIFSIGNALED:子进程收到信号终止;

WIFSTOPPED:子进程收到信号停止。

用trace06.txt,trace07.txt,trace08.txt来测试:

 

 

任务4:实现fg和bg内置命令,并完成do_bgfg()处理函数

(1.1)测试

trace09.txt:

 

trace10.txt:

 

trace09测试bg内置命令,trace10测试fg内置命令。并且接受到fg或bg,do_bgfg()函数应该进行相应的处理。

(1.2)实现

builtin_cmd增加“fg”和“bg”的内置命令:

要实现do_bgfg()函数前,我们要先明确bg和fg这两个内置命令的作用。

bg <job> 命令:通过发送一个 SIGCONT 信号重新启动 <job>,然后让它在后台运行,<job> 参数可以是 PID 或 JID。

fg <job> 命令:通过发送一个 SIGCONT 信号重新启动 <job>,然后让它在前台运行, <job> 参数可以是 PID 或 JID。

实现步骤如下:

 

①判断参数合法性。argc表示指令参数个数,如果这个参数不为2,说明命令错误,输出提示。然后判断参数的格式是否正确,如果不正确,输出提示(注意,提示是样例test14编写)。

②根据%来判断是pid还是jid,并获取其job,如果这个job存在则向这个job发送SIGCONT信号,若无则return。

③判断是bg命令还是fg命令,可以使用strcmp函数判断。

对于bg命令:将job的状态修改为BG,并输出jid,pid和之前输入的命令行。

对于fg命令:将job的状态修改为FG,等待job在前台运行完毕,最后会通过sigchld_handler输出子进程的处理结果。

 

用trace09.txt,trace10.txt来测试:

任务5:测试完剩下的trace

完成上面的test之后,剩下的test都是上面的test 的组合和一些特殊情况,方便对代码进行进一步优化。

trace11.txt是Forward SIGINT to every process in foreground process group,即测试fg和SIGINT。

trace12.txt是Forward SIGTSTP to every process in foreground process group,即测试fg和SIGTSTP。

trace13.txt是Restart every stopped process in process group,即重启有INIT信号暂停的进程。

trace14.txt是Simple error handling,即测试JID或PID的错误输入的情况。

trace15.txt是Putting it all together,即将前面测试的命令放在一起进行。

trace16.txt是Tests whether the shell can handle SIGTSTP and SIGINT signals that come from other processes instead of the terminal,即测试tsh能否处理不是来自终端而是来自其他进程的SIGSTP和SIGINT信号。

由于篇幅限制,此处展示test15和test16:

实验结果及分析:

 

收获与体会:

该实验需要编写大量的代码,表面上是完成一个简易的shell,但实际上涉及到进程控制、信号等十分基础但很重要的知识,还涉及到异常控制流、进程、系统调用、信号处理函数与非本地跳转等并发编程的知识。其中,避免子进程被过早结束还需要利用信号阻塞与解除来解决,这一点容易被忽视导致出现各种bug。此外还有很多细节需要注意,比如setpgid的设置,kill函数采取-pid将信号传到进程组,而不是单一进程中。经历这些之后,会对异常控制流这一章的理解有质的飞跃。

验成绩

   

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值