实验报告
实 验(七)
题 目 TinyShell
微壳
专 业 计算机科学与技术
计算机科学与技术学院
tsh.c源代码见文章末尾
目 录
2.1 进程的概念、创建和回收方法(5分)... - 5 -
2.3 信号的发送方法、阻塞方法、处理程序的设置方法(5分)... - 13 -
2.4 什么是shell,功能和处理流程(5分)... - 16 -
第1章 实验基本信息
1.1 实验目的
理解现代计算机系统进程与并发的基本知识
掌握linux 异常控制流和信号机制的基本原理和相关系统函数
掌握shell的基本原理和实现方法
深入理解Linux信号响应可能导致的并发冲突及解决方法
培养Linux下的软件系统开发与测试能力
1.2 实验环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
1.2.3 开发工具
1.3 实验预习
上实验课前,必须认真预习实验指导书(PPT或PDF)
了解实验的目的、实验环境与软硬件工具、实验操作步骤,复习与实验有关的理论知识。
了解进程、作业、信号的基本概念和原理
了解shell的基本原理
熟知进程创建、回收的方法和相关系统函数
熟知信号机制和信号处理相关的系统函数
第2章 实验预习
总分20分
2.1 进程的概念、创建和回收方法(5分)
1.进程的概念:
狭义上:进程是一个执行中程序的示例。
广义上:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
2.进程的创建方法:
每次用户通过shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程。
具体到实现:父进程同通过调用fork函数创建一个新的运行的子进程。
进程的创建: //内核里面只有fork 和exec两种可以创建进程。其他方式都是使用的这两种方式,如system()封装了exec。
1.system函数:不常用
#include <stdlib.h>
int main(){
system("ls -l") //创建了一个ls -l的进程。system("clear")表示清屏。
}
2.fork()
复制出一份。各自是各自的。每个进程都有一个文件表项。
其实就是为了
比如说双胞胎,出生之后完全一样。一个身上有什么东西,另一个人相同的地方就有什么东西。
父进程先返回子进程的pid(),第二次子进程返回0.
例子1:
pid_t pid;
pid=fork();//fork不需要传递参数。
if(pid==0){ //这里写子进程接下来做的事
printf("%d%d",getpid(),getppid());
while(1);
}else{ //这里写父进程接下来做的事
printf("%d",pid);
}
3.exec系列(工作中用的太少了)
一个进程创建另一个进程,会直接用新进程覆盖原有进程的代码段。
exec系列有6个函数:execl()、execlp()、execle()、execv()、execvp()、execvpe()
例:
add.c
int main(int argc,char *argv[]){
int i=atoi(argv[0]);
int j=atoi(argv[1]);
return i+j;
}
execl.c
#include <unistd.h>
#include <stdio.h>
int main(){
excel("./add","add","1","2",NULL);//./可执行文件 argv[0] argv[1] argv[2].......NULL 。参数一定要从argv[0]开始写,最后写NULL
printf("hello\n"); printf("hello\n");及以下的都不会再执行了,因为从excel走进了另一个世界
....
}
4.popen
3.进程的回收:
如果父进程没有回收它的僵死子进程就终止了,那么内核会安排init进程去回收它们。一个进程可以调用waitpid函数来等待它的子进程终止或者停止,其中wait函数是waitpid函数的简单版本,wait(&status)等价于调用waitpid(-1,&status,0) 。
孤儿进程:
如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被PID为1的进程(即init)接管。孤儿进程退出后,它的清理工作有祖先进程init自动处理。init清理没那么及时,毕竟不是亲爹。
子进程退出时,应当由父进程回收资源。应当避免父进程退出了,子进程还在的情况。即,应当避免孤儿进程。所以父进程要等子进程运行完了再退出。
例:孤儿进程
pid_t pid = fork();
if( pid == 0) {
while(1) ; //父进程退出了,之后子进程还在运行。通过ps -elf 可以看到进程的ppid变成了1.
}else{
exit(0);
}
僵尸进程:你死了没人给你收尸!
如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用wait或waitpid函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
例:僵尸进程
pid_t pid=fork();
if(pid == 0){
exit(0);
}else{
while(1); //子进程都已经退出了,但是父进程没有回收。ps-elf 可以看到僵尸进程。
}
避免僵尸进程(wait函数):
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options); //opeions若为WNOHANG,则父进程运行到这,看看有僵尸就回收,没僵尸就直接走了,往下运行,不等了。
例1:
pid_t pid;
pid=fork();
if(pid == 0){
sleep(5);
exit(0);
}else{
wait(NULL); //子进程sleep(5)期间,可以看到父进程的状态时阻塞(sleep),等待子进程结束。等子进程结束之后回收资源。
exit(0); wait(NULL)表示等待所有进程。
}
例2:
pid_t pid;
pid=fork();
if(pid == 0){
sleep(5);
exit(0);
}else{
waitpid(pid,NULL,0); //等待指定pid的子进程。
exit(0);
}
}
例3:
pid_t pid;
pid=fork();
if(pid == 0){
while(1);
exit(2); //如果进程正常结束,返回2。如果不正常的话(比如,ctrl+c),返回0.
}else{
int status; //status存放子进程的退出状态,status含有很多信息,其中就包括返回值。
wait(&status);
printf("I am wake\n");
if(WIFEXITED(status)){//这个宏用来获取状态信息。如果进程正常结束,则结果非零。
printf("the exit value=%d\n",WEXITSTATUS(status)); //这个宏返回子进程的退出码。这个例子中是2.
}else{
printf("the child abort\n"); //如果WIFEXITED(status)==0,说明不是正常退出。
}
exit(0);
}
2.2信号的机制、种类(5分)
一. 信号机制
信号机制指的是内核如何向一个进程发送信号、进程如何接收一个信号、进程怎样控制自己对信号的反应、内核在什么时机处理和怎样处理进程收到的信号。还要介绍一下setjmp和longjmp在信号中起到的作用。
1、内核对信号的基本处理方法
内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看 该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。前面介绍概念的时候讲过,处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
在信号的处理方法中有几点特别要引起注意。第一,在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal系统调用。这可能会使得进程 在调用signal之前又得到该信号而导致退出。在BSD中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢 出。为了避免出现上述情况。在BSD系统中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。
第二个要引起注意的是,如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上,这时该信号引起进程作一次longjmp,跳出睡眠状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就象从系统调用返回一样,但返回了一个错误代码,指出该次系统调用曾经被中断。这要注意的是,BSD系统中内核可以自动地重新开始系统调用。
第三个要注意的地方:若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp,一般是继续睡眠。但用户感觉不到进程曾经被唤醒,而是象没有发生过该信号一样。
第四个要注意的地方:内核对子进程终止(SIGCHLD)信号的处理方法与其他信号有所区别。当进程检查出收到了一个子进程终止的信号时,缺省情况下,该进程就像没有收到该信号似的,如果父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操作(找出僵死的子进程,释放子进程的进程表项),然后从wait中返回。SIGCHLD信号的作用是唤醒一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就像普通信号处理一样转到处理例程。如果进程忽略该信号,那么系统调用wait的动作就有所不同,因为SIGCHLD的作用仅仅是唤醒一个睡眠在可被中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操作,然后等待其他的子进程。
如果一个进程调用signal系统调用,并设置了SIGCLD的处理方法,并且该进程有子进程处于僵死状态,则内核将向该进程发一个SIGCLD信号。
2、setjmp和longjmp的作用
前面在介绍信号处理机制时,多次提到了setjmp和longjmp,但没有仔细说明它们的作用和实现方法。这里就此作一个简单的介绍。
在介绍信号的时候,我们看到多个地方要求进程在检查收到信号后,从原来的系统调用中直接返回,而不是等到该调用完成。这种进程突然改变其上下文的情况,就是使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区,并继续在旧的上下文中执行。这就是说,进程执行一个系统调用,当因为资源或其他原因要去睡眠时,内核为进程作了一次setjmp,如果在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核 为进程将原先setjmp调用保存在进程用户区的上下文恢复成现在的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使得进程知道该次系统调用失败。这就是它们的作用。
二. 信号的种类
linux信号有如下的种类:
编号 | 信号名称 | 缺省动作 | 说明 |
1 | SIGHUP | 终止 | 终止控制终端或进程 |
2 | SIGINT | 终止 | 键盘产生的中断(Ctrl-C) |
3 | SIGQUIT | dump | 键盘产生的退出 |
4 | SIGILL | dump | 非法指令 |
5 | SIGTRAP | dump | debug中断 |
6 | SIGABRT/SIGIOT | dump | 异常中止 |
7 | SIGBUS/SIGEMT | dump | 总线异常/EMT指令 |
8 | SIGFPE | dump | 浮点运算溢出 |
9 | SIGKILL | 终止 | 强制进程终止 |
10 | SIGUSR1 | 终止 | 用户信号,进程可自定义用途 |
11 | SIGSEGV | dump | 非法内存地址引用 |
12 | SIGUSR2 | 终止 | 用户信号,进程可自定义用途 |
13 | SIGPIPE | 终止 | 向某个没有读取的管道中写入数据 |
14 | SIGALRM | 终止 | 时钟中断(闹钟) |
15 | SIGTERM | 终止 | 进程终止 |
16 | SIGSTKFLT | 终止 | 协处理器栈错误 |
17 | SIGCHLD | 忽略 | 子进程退出或中断 |
18 | SIGCONT | 继续 | 如进程停止状态则开始运行 |
19 | SIGSTOP | 停止 | 停止进程运行 |
20 | SIGSTP | 停止 | 键盘产生的停止 |
21 | SIGTTIN | 停止 | 后台进程请求输入 |
22 | SIGTTOU | 停止 | 后台进程请求输出 |
23 | SIGURG | 忽略 | socket发生紧急情况 |
24 | SIGXCPU | dump | CPU时间限制被打破 |
25 | SIGXFSZ | dump | 文件大小限制被打破 |
26 | SIGVTALRM | 终止 | 虚拟定时时钟 |
27 | SIGPROF | 终止 | profile timer clock |
28 | SIGWINCH | 忽略 | 窗口尺寸调整 |
29 | SIGIO/SIGPOLL | 终止 | I/O可用 |
30 | SIGPWR | 终止 | 电源异常 |
31 | SIGSYS/SYSUNUSED | dump | 系统调用异常 |
2.3 信号的发送方法、阻塞方法、处理程序的设置方法(5分)
一. 发送信号
内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程.
发送方法:
1. 用 /bin/kill 程序发送信号
/bin/kill程序可以向另外的进程或进程组发送任意的信号
Examples:/bin/kill –9 24818 发送信号9(SIGKILL)给进程 24818
/bin/kill –9 –24817 发送信号SIGKILL给进程组24817 中的每个进程
(负的PID会导致信号被发送到进 程组PID中的每个进程)
2. 从键盘发送信号输入 ctrl-c (ctrl-z) 会导致内核发送一个 SIGINT (SIGTSTP)信 号到前台进程组中的每个作业 SIGINT 默认情况是终止前台作业 SIGTSTP默认情况是停止(挂起)前台作业。
3. 发送信号的函数主要有kill(),raise(),alarm(),pause()
(1)kill()和raise()
kill()函数和熟知的kill系统命令一样,可以发送信号给信号和进程组(实际上kill系统命令只是kill函数的一个用户接口),需要注意的是他不仅可以终止进程(发送SIGKILL信号),也可以向进程发送其他信号。
与kill函数不同的是raise()函数允许进程向自身发送信号。
(2)函数格式:
kill函数的语法格式
raise()函数语法要点:
(3)alarm()和pause()
alarm()-----也称为闹钟函数,可以在进程中设置一个定时器,等到时间到达时,就会想进程发送SIGALARM信号,注意的是一个进程只能有一个闹钟时间,如果调用alarm()之前已经设置了闹钟时间,那么任何以前的闹钟时间都会被新值所代替
pause()----此函数用于将进程挂起直到捕捉到信号为止,这个函数很常用,通常用于判断信号是否已到
alarm()函数语法:
pause()函数语法如下:
二. 阻塞信号
阻塞和解除阻塞信号
隐式阻塞机制 :
内核默认阻塞与当前正在处理信号类型相同的待处理信号 如: 一个SIGINT 信号处理程序不能被另一个 SIGINT信号中断 (此时另一个SIGINT信号被阻塞)
显示阻塞和解除阻塞机制 :
sigprocmask 函数及其辅助函数可以明确地阻塞/解除阻塞
选定的信号辅助函数:
sigemptyset – 初始化set为空集合
sigfillset – 把每个信号都添加到set中
sigaddset – 把指定的信号signum添加到set
sigdelset – 从set中删除指定的信号signum
三. 设置信号处理程序
可以使用 signal函数修改和信号signum相关联的默认行为: handler_t *signal(int signum, handler_t *handler)
handler的不同取值:
1. SIG_IGN: 忽略类型为signum的信号
2. SIG_DFL: 类型为 signum的信号行为恢复为默认行为
3. 否则, handler就是用户定义的函数的地址,这个函数称为信号处理程序
只要进程接收到类型为 signum 的信号就会调用信号处理程序
将处理程序的地址传递到signal函数从而改变默认行为,这叫作设置信号处理程序。调用信号处理程序称为捕获信号
执行信号处理程序称为处理信号
当处理程序执行return时,控制会传递到控制流中被信号接收所中断的指令处
2.4 什么是shell,功能和处理流程(5分)
一. shell定义
Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行
二. shell功能
实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果
三. shell处理流程
shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
第3章 TinyShell的设计与实现
总分45分
3.1 设计
3.1.1 void eval(char *cmdline)函数(10分)
函数功能:评估用户刚输入的命令行。如果用户请求了内置命令(quit,jobs,bg或fg),则立即执行。 否则,fork一个子进程并在子进程的上下文中运行该作业。 如果作业在前台运行,等待它终止然后返回。
参 数:形参 用户输入的命令行cmdline 。实参 argv , mask , SIG_UNBLOCK , argv[0] , environ
处理流程:
第一步:定义各个变量。使用parseline()函数函数解析命令行,得到命令行参数。
第二步:使用builtin_cmd()函数判断命令是否为内置命令,如果不是内置命令,则继续执行。
第三步:设置阻塞集合。先初始化mask为空集合,再将SIGCHLD , SIGINT ,SIGTSTP 信号加入阻塞集合。
第四步:阻塞SIGCHLD,防止子进程在父进程之前结束,防止addjob()函数错误地把(不存在的)子进程添加到作业列表中。
第五步:子进程中,先解除对SIG_CHLD的阻塞,再使用setpgid(0,0)创建一个虚拟的进程组,进程组ID是15213,不和tsh进程在一个进程组。然后调用execve函数,执行相应的文件。
第六步:将job添加到job list,解除SIG_CHLD阻塞信号。判断进程是否为前台进程,如果是前台进程,调用waitfg()函数,等待前台进程,如果是后台进程,则打印出进程信息。
要点分析:
1.每个子进程必须具有唯一的进程组ID,以便当我们在键盘上键入ctrl-c(ctrl-z)时,我们的后台子进程不会从内核接收SIGINT(SIGTSTP)
2.在执行addjob之前需要阻塞信号,防止addjob()函数错误地把(不存在的)子进程添加到作业列表中。
3. 1.2 int builtin_cmd(char **argv)函数(5分)
函数功能:如果用户键入了内置命令,则立即执行。
参 数:形参 传入的参数数组argv 。实参argv[0] ,"quit" ,"&" ,"jobs" ,"bg" ,"fg", argv
SIG_BLOCK ,mask ,prev_mask ,NULL
处理流程:
函数需要根据传入的参数数组,判断用户键入的是否为内置命令,采取的办法就是比较argv[0]和内置命令,如果是内置命令,则跳到相应的函数,并且返回1,如果不是,则什么也不做,并且返回0。
其中,如果命令为quit,则直接退出;如果命令是内置的jobs命令,则调用listjobs()函数,打印job列表;如果是fg或是bg两条内置命令,则调用do_bgfg()函数来处理即可。
要点分析:因为jobs是全局变量,为了防止其被修改,需要阻塞全部信号,过程大致为(后面函数阻塞全部信号的做法与此基本一致):
sigset_t mask,prev_mask;
sigfillset(&mask);
…
sigprocmask(SIG_BLOCK,&mask,&prev_mask);
sigprocmask(SIG_SETMASK,&prev_mask,NULL);
3. 1.3 void do_bgfg(char **argv) 函数(5分)
函数功能:执行内置bg和fg命令
参 数:形参 传入的参数数组argv 。实参argv[1] , argv[1][0] , jobs , pid , argv[1] , argv[0]
处理流程:
第一步:先判断fg或bg后是否有参数,如果没有,则忽略命令。
第二步:如果fg或bg后面只是数字,说明取的是进程号,获取该进程号后,使用getjobpid(jobs, pid)得到job;如果fg或bg后面是%加上数字的形式,说明%后面是任务号(第几个任务),此时获取jid后,可以使用getjobjid(jobs, jid)得到job 。
第三步:比较区分argv[0]是“bg”还是“fg”。如果是后台进程,则发送SIGCONT信号给进程组PID的每个进程,并且设置任务的状态为BG,打印任务的jid,pid和命令行;如果是前台进程,则发送SIGCONT信号给进程组PID的每个进程,并且设置任务的状态为FG,调用waitfg(jobp->pid),等待前台进程结束。
要点分析:
1.函数主要是先判断fg后面是%+数字还是只有数字的形式,从而根据进程号pid或是工作组号jid来获取结构体job;然后在根据前台和后台进程的不同,执行相应的操作。
2. isdigit()函数判断是否为数字,不是数字返回0
3. atoi()函数把字符串转化为整型数
4. SIGCONT信号对应事件为:继续进程如果该进程停止。
3. 1.4 void waitfg(pid_t pid) 函数(5分)
函数功能:阻止直到进程pid不再是前台进程
参 数:形参 前台进程pid 。实参mask , jobs 。
处理流程:
函数主体是while循环语句,判断传入的pid是否为一个前台进程的pid,如果是,则一直循环,如果不是,则跳出循环。其中while循环内部使用sigsuspend()函数,暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,选择运行一个处理程序或者终止该进程。
要点分析:
1.在while内部,如果使用的只是pause()函数,那么程序必须等待相当长的一段时间才会再次检查循环的终止条件,如果使用向nanosleep这样的高精度休眠函数也是不可接受的,因为没有很好的办法来确定休眠的间隔。
2.在while循环语句之前,初始化mask结合为空,在while内部用SIG_SETMASK使block=mask,这样sigsuspend()才不会因为收不到SIGCHLD信号而永远睡眠。
3. 1.5 void sigchld_handler(int sig) 函数(10分)
函数功能:只要子作业终止(变为僵尸),或者因为收到SIGSTOP或SIGTSTP信号而停止,内核就会向shell发送SIGCHLD。处理程序收集所有可用的僵尸子节点,但不等待任何其他当前正在运行的子节点终止。
参 数:形参 信号sig 。 实参 status , WNOHANG|WUNTRACED , SIG_BLOCK , mask , prev_mask , SIG_SETMASK
处理流程:
第一步:把每个信号都添加到mask阻塞集合中,设置olderrno = errno 。
第二步:在while循环中使用waitpid(-1,&status,WNOHANG|WUNTRACED)),其中,目的是尽可能回收子进程,其中WNOHANG | WUNTRACED表示立即返回,如果等待集合中没有进程被中止或停止返回0,否则孩子返回进程的pid。
第三步:在循环中阻塞信号,并且使用getjobpid()函数,通过pid找到job 。
第四步:通过waitpid在status中放上的返回子进程的状态信息,判断子进程的退出状态。如果引起返回的子进程当前是停止的,那么WIFSTOPPED(status)就返回真,此时只需要将pid找到的job的状态改为ST,并且按照示例程序输出的信息,将job的jid,pid以及导致子进程停止的信号的编号输出即可。如果子进程是因为一个未被捕获的信号终止的,那么WIFSIGNALED(status)就返回真,此时同样按照示例程序输出的信息,将job的jid,pid以及导致子进程终止的信息的编号输出即可,因为此时进程是中止的的进程,所以还需要deletejob()将发出SIGCHLD信号的将其直接回收。
第五步:清空缓冲区,解除阻塞,恢复errno 。
要点分析:
1.while循环来避免信号阻塞的问题,循环中使用waitpid()函数,以尽可能多的回收僵尸进程。
2.调用deletejob()函数时,因为jobs是全局变量,因此需要阻塞信号。
3. 通过waitpid在status中放上的返回子进程的状态信息,判断子进程的退出状态。
WIFSIGNALED判断子进程是否因为一个未被捕获的信号中止的,WIFSTOPPED判断引起返回地子进程当前是否为停止的。
3.2 程序实现(tsh.c的全部内容)(10分)
重点检查代码风格:
- 用较好的代码注释说明——5分
- 检查每个系统调用的返回值——5分
/*
* tsh - A tiny shell program with job control
*
* <Put your name and login ID here>
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
/* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/
/* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
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 */
/* End global variables */
/* Function prototypes */
/* Here are the functions that you will implement */
void eval(char *cmdline);
int builtin_cmd(char **argv);
void do_bgfg(char **argv);
void waitfg(pid_t pid);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv);
void sigquit_handler(int sig);
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);
pid_t fgpid(struct job_t *jobs);
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);
struct job_t *getjobjid(struct job_t *jobs, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t *jobs);
void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
/*
* main - The shell's main routine
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);
/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
/* Install the signal handlers */
/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);
/* Initialize the job list */
initjobs(jobs);
/* Execute the shell's read/eval loop */
while (1) {
/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
/* Evaluate the command line */
eval(cmdline);
fflush(stdout); //清空缓冲区并输出
fflush(stdout);
}
exit(0); /* control never reaches here */
}
/*
* 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;
}
/*
* parseline - Parse the command line and build the argv array.
*
* Characters enclosed in single quotes are treated as a single
* argument. Return true if the user has requested a BG job, false if
* the user has requested a FG job.
*/
int parseline(const char *cmdline, char **argv)
{
static char array[MAXLINE]; /* holds local copy of command line */
char *buf = array; /* ptr that traverses command line */
char *delim; /* points to first space delimiter */
int argc; /* number of args */
int bg; /* background job? */
strcpy(buf, cmdline);
buf[strlen(buf)-1] = ' '; /* replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* ignore leading spaces */
buf++;
/* Build the argv list */
argc = 0;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
while (delim) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* ignore spaces */
buf++;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
}
argv[argc] = NULL;
if (argc == 0) /* ignore blank line */
return 1;
/* should the job run in the background? */
if ((bg = (*argv[argc-1] == '&')) != 0) {
argv[--argc] = NULL;
}
return bg;
}
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
builtin_cmd - 如果用户键入了内置命令,则立即执行。
*/
int builtin_cmd(char **argv)
{
sigset_t mask,prev_mask;
sigfillset(&mask);
if(!strcmp(argv[0],"quit")) //退出命令
exit(0);
else if(!strcmp(argv[0],"&")) //忽略单独的&
return 1;
else if(!strcmp(argv[0],"jobs")) //输出作业列表中所有作业信息
{
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //访问全局变量,阻塞所有信号
listjobs(jobs);
sigprocmask(SIG_SETMASK,&prev_mask,NULL);
return 1;
}
else if(!strcmp(argv[0],"bg")||!strcmp(argv[0],"fg")) //实现bg和fg两条内置命令
{
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
/*
* do_bgfg - Execute the builtin bg and fg commands
执行内置bg和fg命令
*/
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])) //判断串1的第0位是否为数字
{
pid_t pid = atoi(argv[1]); //atoi把字符串转化为整型数
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 - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid) //传入的是一个前台进程的pid
{
sigset_t mask;
sigemptyset(&mask); //初始化mask为空集合
while(pid==fgpid(jobs))
{
sigsuspend(&mask); //暂时挂起
}
}
/*****************
* 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.
sigchld_handler - 只要子作业终止(变为僵尸),
内核就会向shell发送SIGCHLD,或者因为收到SIGSTOP或SIGTSTP信号而停止。
处理程序收集所有可用的僵尸子节点,但不等待任何其他当前正在运行的子节点终止。
*/
void sigchld_handler(int sig)
{
struct job_t *job1; //新建结构体
int olderrno = errno,status;
sigset_t mask, prev_mask;
pid_t pid;
sigfillset(&mask);
while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)
{
/*尽可能回收子进程,WNOHANG | WUNTRACED表示立即返回,
如果等待集合中没有进程被中止或停止返回0,否则孩子返回进程的pid*/
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //需要deletejob,阻塞所有信号
job1 = getjobpid(jobs,pid); //通过pid找到job
if(WIFSTOPPED(status)) //子进程停止引起的waitpid函数返回
{
job1->state = ST;
printf("Job [%d] (%d) terminated by signal %d\n",job1->jid,job1->pid,WSTOPSIG(status));
}
else
{
if(WIFSIGNALED(status) //子进程终止引起的返回
printf("Job [%d] (%d) terminated by signal %d\n",job1->jid,job1->pid,WTERMSIG(status));
deletejob(jobs, pid); //终止的进程直接回收
}
fflush(stdout);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
}
errno = olderrno;
}
/*
* 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.
sigint handler - 只要用户在键盘上键入ctrl-c,
内核就会向shell发送一个SIGINT。 抓住它并将其发送到 前台作业。
*/
void sigint_handler(int sig)
{
pid_t pid ;
sigset_t mask, prev_mask;
int olderrno=errno;
sigfillset(&mask);
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //需要获取前台进程pid,阻塞所有信号
pid = fgpid(jobs); //获取前台作业pid
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
if(pid!=0) //只处理前台作业
kill(-pid,SIGINT);
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.
sigtstp_handler - 只要用户在键盘上键入ctrl-z,
内核就会向shell发送SIGTSTP。 通过向它发送SIGTSTP来捕获它并暂停前台作业。
*/
void sigtstp_handler(int sig)
{
pid_t pid;
sigset_t mask,prev_mask;
int olderrno = errno;
sigfillset(&mask);
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //需要获取前台进程pid,阻塞所有信号
pid = fgpid(jobs);
sigprocmask(SIG_SETMASK,&prev_mask,NULL);
if(pid!=0)
kill(-pid,SIGTSTP);
errno = olderrno;
return;
}
/*********************
* End signal handlers
*********************/
/***********************************************
* Helper routines that manipulate the job list
**********************************************/
/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job) {
job->pid = 0;
job->jid = 0;
job->state = UNDEF;
job->cmdline[0] = '\0';
}
/* initjobs - Initialize the job list */
void initjobs(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
clearjob(&jobs[i]);
}
/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *jobs)
{
int i, max=0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid > max)
max = jobs[i].jid;
return max;
}
/* addjob - Add a job to the job list */
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == 0) {
jobs[i].pid = pid;
jobs[i].state = state;
jobs[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = 1;
strcpy(jobs[i].cmdline, cmdline);
if(verbose){
printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
}
return 1;
}
}
printf("Tried to create too many jobs\n");
return 0;
}
/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *jobs, pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == pid) {
clearjob(&jobs[i]);
nextjid = maxjid(jobs)+1;
return 1;
}
}
return 0;
}
/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].state == FG)
return jobs[i].pid;
return 0;
}
/* getjobpid - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *jobs, pid_t pid) {
int i;
if (pid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid)
return &jobs[i];
return NULL;
}
/* getjobjid - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *jobs, int jid)
{
int i;
if (jid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid == jid)
return &jobs[i];
return NULL;
}
/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid) {
return jobs[i].jid;
}
return 0;
}
/* 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);
}
}
}
/******************************
* end job list helper routines
******************************/
/***********************
* Other helper routines
***********************/
/*
* usage - print a help message
*/
void usage(void)
{
printf("Usage: shell [-hvp]\n");
printf(" -h print this message\n");
printf(" -v print additional diagnostic information\n");
printf(" -p do not emit a command prompt\n");
exit(1);
}
/*
* unix_error - unix-style error routine
*/
void unix_error(char *msg)
{
fprintf(stdout, "%s: %s\n", msg, strerror(errno));
exit(1);
}
/*
* app_error - application-style error routine
*/
void app_error(char *msg)
{
fprintf(stdout, "%s\n", msg);
exit(1);
}
/*
* Signal - wrapper for the sigaction function
*/
handler_t *Signal(int signum, handler_t *handler)
{
struct sigaction action, old_action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */
if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}
/*
* sigquit_handler - The driver program can gracefully terminate the
* child shell by sending it a SIGQUIT signal.
*/
void sigquit_handler(int sig)
{
printf("Terminating after receipt of SIGQUIT signal\n");
exit(1);
}
第4章 TinyShell测试
总分15分
4.1 测试方法
针对tsh和参考shell程序tshref,完成测试项目4.1-4.15的对比测试,并将测试结果截图或者通过重定向保存到文本文件(例如:./sdriver.pl -t trace01.txt -s ./tsh -a "-p" > tshresult01.txt)。
4.2 测试结果评价
tsh与tshref的输出在一下两个方面可以不同:
(1)PID
(2)测试文件trace11.txt, trace12.txt和trace13.txt中的/bin/ps命令,每次运行的输出都会不同,但每个mysplit进程的运行状态应该相同。
除了上述两方面允许的差异,tsh与tshref的输出相同则判为正确,如不同则给出原因分析。
4.3 自测试结果
4.3.1测试用例trace01.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.2测试用例trace02.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.3测试用例trace03.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.4测试用例trace04.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.5测试用例trace05.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.6测试用例trace06.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.7测试用例trace07.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.8测试用例trace08.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.9测试用例trace09.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.10测试用例trace10.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.11测试用例trace11.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.12测试用例trace12.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.13测试用例trace13.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.14测试用例trace14.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.3.15测试用例trace15.txt的输出截图(1分)
tsh测试结果 | tshref测试结果 | |
|
| |
测试结论 | 相同/不同,原因分析如下: | |
4.4 自测试评分
根据节4.3的自测试结果,程序的测试评分为: 15 。
/*
* tsh - A tiny shell program with job control
*
* <Put your name and login ID here>
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
/* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/
/* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
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 */
/* End global variables */
/* Function prototypes */
/* Here are the functions that you will implement */
void eval(char *cmdline);
int builtin_cmd(char **argv);
void do_bgfg(char **argv);
void waitfg(pid_t pid);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv);
void sigquit_handler(int sig);
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);
pid_t fgpid(struct job_t *jobs);
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);
struct job_t *getjobjid(struct job_t *jobs, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t *jobs);
void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
/*
* main - The shell's main routine
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);
/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
/* Install the signal handlers */
/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);
/* Initialize the job list */
initjobs(jobs);
/* Execute the shell's read/eval loop */
while (1) {
/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
/* Evaluate the command line */
eval(cmdline);
fflush(stdout); //清空缓冲区并输出
fflush(stdout);
}
exit(0); /* control never reaches here */
}
/*
* 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;
}
/*
* parseline - Parse the command line and build the argv array.
*
* Characters enclosed in single quotes are treated as a single
* argument. Return true if the user has requested a BG job, false if
* the user has requested a FG job.
*/
int parseline(const char *cmdline, char **argv)
{
static char array[MAXLINE]; /* holds local copy of command line */
char *buf = array; /* ptr that traverses command line */
char *delim; /* points to first space delimiter */
int argc; /* number of args */
int bg; /* background job? */
strcpy(buf, cmdline);
buf[strlen(buf)-1] = ' '; /* replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* ignore leading spaces */
buf++;
/* Build the argv list */
argc = 0;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
while (delim) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* ignore spaces */
buf++;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
}
argv[argc] = NULL;
if (argc == 0) /* ignore blank line */
return 1;
/* should the job run in the background? */
if ((bg = (*argv[argc-1] == '&')) != 0) {
argv[--argc] = NULL;
}
return bg;
}
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
builtin_cmd - 如果用户键入了内置命令,则立即执行。
*/
int builtin_cmd(char **argv)
{
sigset_t mask,prev_mask;
sigfillset(&mask);
if(!strcmp(argv[0],"quit")) //退出命令
exit(0);
else if(!strcmp(argv[0],"&")) //忽略单独的&
return 1;
else if(!strcmp(argv[0],"jobs")) //输出作业列表中所有作业信息
{
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //访问全局变量,阻塞所有信号
listjobs(jobs);
sigprocmask(SIG_SETMASK,&prev_mask,NULL);
return 1;
}
else if(!strcmp(argv[0],"bg")||!strcmp(argv[0],"fg")) //实现bg和fg两条内置命令
{
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
/*
* do_bgfg - Execute the builtin bg and fg commands
执行内置bg和fg命令
*/
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])) //判断串1的第0位是否为数字
{
pid_t pid = atoi(argv[1]); //atoi把字符串转化为整型数
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 - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid) //传入的是一个前台进程的pid
{
sigset_t mask;
sigemptyset(&mask); //初始化mask为空集合
while(pid==fgpid(jobs))
{
sigsuspend(&mask); //暂时挂起
}
}
/*****************
* 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.
sigchld_handler - 只要子作业终止(变为僵尸),
内核就会向shell发送SIGCHLD,或者因为收到SIGSTOP或SIGTSTP信号而停止。
处理程序收集所有可用的僵尸子节点,但不等待任何其他当前正在运行的子节点终止。
*/
void sigchld_handler(int sig)
{
struct job_t *job1; //新建结构体
int olderrno = errno,status;
sigset_t mask, prev_mask;
pid_t pid;
sigfillset(&mask);
while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)
{
/*尽可能回收子进程,WNOHANG | WUNTRACED表示立即返回,
如果等待集合中没有进程被中止或停止返回0,否则孩子返回进程的pid*/
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //需要deletejob,阻塞所有信号
job1 = getjobpid(jobs,pid); //通过pid找到job
if(WIFSTOPPED(status)) //子进程停止引起的waitpid函数返回
{
job1->state = ST;
printf("Job [%d] (%d) terminated by signal %d\n",job1->jid,job1->pid,WSTOPSIG(status));
}
else
{
if(WIFSIGNALED(status)) //子进程终止引起的返回
printf("Job [%d] (%d) terminated by signal %d\n",job1->jid,job1->pid,WTERMSIG(status));
deletejob(jobs, pid); //终止的进程直接回收
}
fflush(stdout);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
}
errno = olderrno;
}
/*
* 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.
sigint handler - 只要用户在键盘上键入ctrl-c,
内核就会向shell发送一个SIGINT。 抓住它并将其发送到 前台作业。
*/
void sigint_handler(int sig)
{
pid_t pid ;
sigset_t mask, prev_mask;
int olderrno=errno;
sigfillset(&mask);
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //需要获取前台进程pid,阻塞所有信号
pid = fgpid(jobs); //获取前台作业pid
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
if(pid!=0) //只处理前台作业
kill(-pid,SIGINT);
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.
sigtstp_handler - 只要用户在键盘上键入ctrl-z,
内核就会向shell发送SIGTSTP。 通过向它发送SIGTSTP来捕获它并暂停前台作业。
*/
void sigtstp_handler(int sig)
{
pid_t pid;
sigset_t mask,prev_mask;
int olderrno = errno;
sigfillset(&mask);
sigprocmask(SIG_BLOCK,&mask,&prev_mask); //需要获取前台进程pid,阻塞所有信号
pid = fgpid(jobs);
sigprocmask(SIG_SETMASK,&prev_mask,NULL);
if(pid!=0)
kill(-pid,SIGTSTP);
errno = olderrno;
return;
}
/*********************
* End signal handlers
*********************/
/***********************************************
* Helper routines that manipulate the job list
**********************************************/
/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job) {
job->pid = 0;
job->jid = 0;
job->state = UNDEF;
job->cmdline[0] = '\0';
}
/* initjobs - Initialize the job list */
void initjobs(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
clearjob(&jobs[i]);
}
/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *jobs)
{
int i, max=0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid > max)
max = jobs[i].jid;
return max;
}
/* addjob - Add a job to the job list */
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == 0) {
jobs[i].pid = pid;
jobs[i].state = state;
jobs[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = 1;
strcpy(jobs[i].cmdline, cmdline);
if(verbose){
printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
}
return 1;
}
}
printf("Tried to create too many jobs\n");
return 0;
}
/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *jobs, pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == pid) {
clearjob(&jobs[i]);
nextjid = maxjid(jobs)+1;
return 1;
}
}
return 0;
}
/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].state == FG)
return jobs[i].pid;
return 0;
}
/* getjobpid - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *jobs, pid_t pid) {
int i;
if (pid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid)
return &jobs[i];
return NULL;
}
/* getjobjid - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *jobs, int jid)
{
int i;
if (jid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid == jid)
return &jobs[i];
return NULL;
}
/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid) {
return jobs[i].jid;
}
return 0;
}
/* 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);
}
}
}
/******************************
* end job list helper routines
******************************/
/***********************
* Other helper routines
***********************/
/*
* usage - print a help message
*/
void usage(void)
{
printf("Usage: shell [-hvp]\n");
printf(" -h print this message\n");
printf(" -v print additional diagnostic information\n");
printf(" -p do not emit a command prompt\n");
exit(1);
}
/*
* unix_error - unix-style error routine
*/
void unix_error(char *msg)
{
fprintf(stdout, "%s: %s\n", msg, strerror(errno));
exit(1);
}
/*
* app_error - application-style error routine
*/
void app_error(char *msg)
{
fprintf(stdout, "%s\n", msg);
exit(1);
}
/*
* Signal - wrapper for the sigaction function
*/
handler_t *Signal(int signum, handler_t *handler)
{
struct sigaction action, old_action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */
if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}
/*
* sigquit_handler - The driver program can gracefully terminate the
* child shell by sending it a SIGQUIT signal.
*/
void sigquit_handler(int sig)
{
printf("Terminating after receipt of SIGQUIT signal\n");
exit(1);
}