计科210X wolf 202108010XXX
作为一份实验报告,我希望阅读者能够比较好地看到这份报告有价值的部分。私以为更为有价值的部分体现在:
此外希望阅读者能够看到,若能如此,也算如愿了。
我们拿到手上的文件包解压之后有三个文件
第一个是老师为我们准备的贴心提示,特别友好地使用中文列出了我们需要干的事情,这也是这个实验中我们唯一能见到中文的地方。
//readme first-shell-lab.txt
这个实验是大家在本课程第一次体验系统级编程,涉及过程,过程控制和信号的相关知识。
1.你需要干什么?
你需要构建一个简单的类Unix/Linux Shell。基于已经提供的“微Shell”框架tsh.c,完成部分函数和信号处理函数的编写工作。使用sdriver.pl可以评估你所完成的shell的相关功能。
2. 准备工作
使用命令tar xvf shelab-handout.tar 解压缩文件;
使用命令 make 去编译和链接一些测试例程;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
你要实现的重要函数列出如下:
eval 主例程,用以分析和解释命令行(好消息:该函数原型在教材一第8章8.4节中可以找到!);
builtin_cmd 执行bg和fg内置命令;
waitfg 等待前台作业执行;
sigchld_handler 响应处理SIGCHILD信号
sigint_handler 响应处理SIGINT(ctrl-c)信号
sigtstp_handler 相应处理SIGSTP(ctrl-z)信号
3.注意
每次修改了tsh.c文件,都需要make它以重新编译。在你的Linux终端中直接运行tsh(./tsh)就可以进入你所编写完成的tiny shell tsh>了。
4. 如何证明你完成了这个实验
在你的Linux终端运行./tshref 这个已经实现的shell,将其输出结果与你所实现的./tsh 输出结果比较,是否一致。
相关比较命令行,参见shelab-overview文件。
5.请在实验报告体现你解决本实验目标的详细过程,仅仅贴图(图中只有代码)可能会导致“无个人工作,仅仅是复制粘贴”的极低分判定。
Love & Peace!
第二个是我们实验的环境以及材料,但需要解压。
使用tar指令解压包
tar xvf shlab-handout.tar
解压完成之后我们可以看到实验的材料
wolf@wolf-VirtualBox:~/LAB4-shelllab$ ls
Makefile README trace04.txt trace09.txt trace14.txt tshref.out
myint.c sdriver.pl trace05.txt trace10.txt trace15.txt
myspin.c trace01.txt trace06.txt trace11.txt trace16.txt
mysplit.c trace02.txt trace07.txt trace12.txt tsh.c
mystop.c trace03.txt trace08.txt trace13.txt tshref
打开这里的README,它会解释这些文件的作用。
################
CS:APP Shell Lab
################
Files:
Makefile # Compiles your shell program and runs the tests
README # This file
tsh.c # The shell program that you will write and hand in(这是我们要完善的程序)
tshref # The reference shell binary.
# The remaining files are used to test your shell
sdriver.pl # The trace-driven shell driver
trace*.txt # The 15 trace files that control the shell driver
tshref.out # Example output of the reference shell on all 15 traces
# Little C programs that are called by the trace files
myspin.c # Takes argument and spins for seconds
mysplit.c # Forks a child that spins for seconds
mystop.c # Spins for seconds and sends SIGTSTP to itself
myint.c # Spins for seconds and sends SIGINT to itself
接下来我们查看makefile文件
# Makefile for the CS:APP Shell Lab
TEAM = NOBODY
VERSION = 1
HANDINDIR = /afs/cs/academic/class/15213-f02/L5/handin
DRIVER = ./sdriver.pl
TSH = ./tsh
TSHREF = ./tshref
TSHARGS = "-p"
CC = gcc
CFLAGS = -Wall -O2
FILES = $(TSH) ./myspin ./mysplit ./mystop ./myint
all: $(FILES)
##################
# Handin your work
##################
handin:
cp tsh.c $(HANDINDIR)/$(TEAM)-$(VERSION)-tsh.c
##################
# Regression tests
##################
# Run tests using the student's shell program
test01:
$(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)
test02:
$(DRIVER) -t trace02.txt -s $(TSH) -a $(TSHARGS)
test03:
$(DRIVER) -t trace03.txt -s $(TSH) -a $(TSHARGS)
test04:
$(DRIVER) -t trace04.txt -s $(TSH) -a $(TSHARGS)
test05:
$(DRIVER) -t trace05.txt -s $(TSH) -a $(TSHARGS)
test06:
$(DRIVER) -t trace06.txt -s $(TSH) -a $(TSHARGS)
test07:
$(DRIVER) -t trace07.txt -s $(TSH) -a $(TSHARGS)
test08:
$(DRIVER) -t trace08.txt -s $(TSH) -a $(TSHARGS)
test09:
$(DRIVER) -t trace09.txt -s $(TSH) -a $(TSHARGS)
test10:
$(DRIVER) -t trace10.txt -s $(TSH) -a $(TSHARGS)
test11:
$(DRIVER) -t trace11.txt -s $(TSH) -a $(TSHARGS)
test12:
$(DRIVER) -t trace12.txt -s $(TSH) -a $(TSHARGS)
test13:
$(DRIVER) -t trace13.txt -s $(TSH) -a $(TSHARGS)
test14:
$(DRIVER) -t trace14.txt -s $(TSH) -a $(TSHARGS)
test15:
$(DRIVER) -t trace15.txt -s $(TSH) -a $(TSHARGS)
test16:
$(DRIVER) -t trace16.txt -s $(TSH) -a $(TSHARGS)
# Run the tests using the reference shell program
rtest01:
$(DRIVER) -t trace01.txt -s $(TSHREF) -a $(TSHARGS)
rtest02:
$(DRIVER) -t trace02.txt -s $(TSHREF) -a $(TSHARGS)
rtest03:
$(DRIVER) -t trace03.txt -s $(TSHREF) -a $(TSHARGS)
rtest04:
$(DRIVER) -t trace04.txt -s $(TSHREF) -a $(TSHARGS)
rtest05:
$(DRIVER) -t trace05.txt -s $(TSHREF) -a $(TSHARGS)
rtest06:
$(DRIVER) -t trace06.txt -s $(TSHREF) -a $(TSHARGS)
rtest07:
$(DRIVER) -t trace07.txt -s $(TSHREF) -a $(TSHARGS)
rtest08:
$(DRIVER) -t trace08.txt -s $(TSHREF) -a $(TSHARGS)
rtest09:
$(DRIVER) -t trace09.txt -s $(TSHREF) -a $(TSHARGS)
rtest10:
$(DRIVER) -t trace10.txt -s $(TSHREF) -a $(TSHARGS)
rtest11:
$(DRIVER) -t trace11.txt -s $(TSHREF) -a $(TSHARGS)
rtest12:
$(DRIVER) -t trace12.txt -s $(TSHREF) -a $(TSHARGS)
rtest13:
$(DRIVER) -t trace13.txt -s $(TSHREF) -a $(TSHARGS)
rtest14:
$(DRIVER) -t trace14.txt -s $(TSHREF) -a $(TSHARGS)
rtest15:
$(DRIVER) -t trace15.txt -s $(TSHREF) -a $(TSHARGS)
rtest16:
$(DRIVER) -t trace16.txt -s $(TSHREF) -a $(TSHARGS)
# clean up
clean:
rm -f $(FILES) *.o *~
可以看到makefile内帮我们写好了相应的命令。由于是线下验收,故不需要考虑all和handin指令。
这里比较重要的是test*
指令,rtest*
指令和clean
指令。(*表示01到16的其中一个数)
使用test*
指令可以用自己写的程序运行tinyshell,使用rtest*
指令可以使用参考的可执行文件看看生成了什么,用这个可以跟我们自己生成的进行比对,从而检验我们是不是写对了。每次改动tsh.c之后都要进行make clean重新生成。
随便打开一个trace文件。
#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &
看到这实际上是一个指令的集合,用于测试我们的tinyshell。
至此,我们要干的事情以已经十分明了了。
在tsh.c中有一些函数(7个)待实现,其他函数已经实现了。
我们要实现这7个函数,然后有16个测试指令集分别对我们的tinyshell进行功能验证。
在编写完函数后,在终端中使用如下方式验证
#这里的*要被换成相应的数字
make test* #这是运行我们自己的tinyshell
make rtest* #这是使用参考的tinyshell
#如果两者输出一致,基本可以认为这个测试点通过了
在我们使用make test*
后,make会调用相应的路径启动相关的测试指令集并完成该测试点的测试。
全部测试点通过,我们的tinyshell可以被认为基本搭建正确。
进程的概念、状态以及控制进程的几个函数(fork,waitpid,execve)。
信号的概念,会编写正确安全的信号处理程序。
shell的概念,理解shell程序是如何利用进程管理和信号去执行一个命令行语句。
shell lab主要目的是为了熟悉进程控制和信号。实现一个简单的shell,能够处理前后台运行程序、能够处理ctrl+z、ctrl+c等信号,实现七个函数:
void eval(char *cmdline); //分析命令,并派生子进程执行 主要功能是解析cmdline并运行
int builtin_cmd(char **argv); //解析和执行bulidin命令,包括 quit, fg, bg, and jobs
void do_bgfg(char **argv); //执行bg和fg命令
void waitfg(pid_t pid); //实现阻塞等待前台程序运行结束
void sigchld_handler(int sig); //SIGCHID信号处理函数
void sigint_handler(int sig); //信号处理函数,响应 SIGINT (ctrl-c) 信号
void sigtstp_handler(int sig); //信号处理函数,响应 SIGTSTP (ctrl-z) 信号
ubuntu12.04 (32位)环境
定义一些宏
/* 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 */
定义4种工作状态
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
定义job_t的任务的类,创建jobs[]数组,这是全局变量
/*
* 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 */
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 */
定义其他的全局变量
/* 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 */
/* End global variables */
接下来开始定义函数
/* Function prototypes */
定义我们需要补全的函数
/* Here are the functions that you will implement */
void eval(char *cmdline); //分析命令,并派生子进程执行 主要功能是解析cmdline并运行
int builtin_cmd(char **argv); //解析和执行bulidin命令,包括 quit, fg, bg, and jobs
void do_bgfg(char **argv); //执行bg和fg命令
void waitfg(pid_t pid); //实现阻塞等待前台程序运行结束
void sigchld_handler(int sig); //SIGCHID信号处理函数
void sigint_handler(int sig); //信号处理函数,响应 SIGINT (ctrl-c) 信号
void sigtstp_handler(int sig); //信号处理函数,响应 SIGTSTP (ctrl-z) 信号
定义已经实现并提供给我们的函数
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv); //获取参数列表,返回是否为后台运行命令
void sigquit_handler(int sig);//处理SIGQUIT信号
void clearjob(struct job_t *job);//清除job结构体
void initjobs(struct job_t *jobs);//初始化任务jobs[]
int maxjid(struct job_t *jobs); //返回jobs链表中最大的jid号。
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);//向jobs[]添加一个任务
int deletejob(struct job_t *jobs, pid_t pid);//在jobs[]中删除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);//打印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函数
/*
* main - The shell's main routine
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* 将stderr重定向到stdout(这样驱动程序将获得连接到stdout的管道上的所有输出) */
dup2(1, 2);
/* 解析命令行 */
while ((c = getopt(argc, argv, "hvp")) != EOF)
{
switch (c)
{
case 'h': /* 打印提示信息 */
usage();
break;
case 'v': /* 发出附加诊断信息 */
verbose = 1;
break;
case 'p': /* 不打印提示 */
emit_prompt = 0; /* 便于自动测试 */
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);
/* 初始化 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 */
}
mian函数,作用是在文件中逐行获取命令,并且判断是不是文件结束(EOF),将命令cmdline送入eval函数进行解析。
可以预见,若没有结束命令,这个main函数将持续进行,不断对用户输入做出反应。
注意:每个子进程都必须有一个唯一的进程组ID,这样当我们在键盘上键入ctrl-c(ctrl-z)时,我们的后台子进程就不会从内核接收SIGINT(SIGTSTP)。
void eval(char *cmdline)
{
char *argv[MAXARGS]; // execve()函数的参数
int state = UNDEF; // 工作状态,FG或BG
sigset_t set;
pid_t pid; // 进程id
// 处理输入的数据
if (parseline(cmdline, argv) == 1) // 解析命令行,返回给argv数组
state = BG;
else
state = FG;
if (argv[0] == NULL) // 若命令行为空,parseline也会返回1,但对argv[0]判定后,eval在这里直接返回
return;
// 如果不是内置命令
if (!builtin_cmd(argv)) // 若是内置命令,builtin_cmd(argv)会执行,若该函数返回0,则表示非内置命令
{
// 初始化信号集set并把SIGINT SIGTSTP SIGCHLD三个信号放入信号集中,方便管理
if (sigemptyset(&set) < 0)
unix_error("sigemptyset error");
if (sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)
unix_error("sigaddset error");
// 阻塞SIGCHLD信号,具体原因在书P541有解释
// 主要是防止fork之后调度执行子进程并在addjob之前结束子进程,
// 此时SIGCHLD信号使父进程将子进程回收,这会导致 addjob 和 deletejob 函数执行错位
// 结果是删除一个不存在的进程号,添加一个不存在且永不会被删除的进程号
if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if ((pid = fork()) < 0) // fork创建子进程失败
unix_error("fork error");
else if (pid == 0) // fork创建子进程
{
// 子进程控制流从这里开始
if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) // 解除阻塞
unix_error("sigprocmask error");
// 函数原型 int setpgid(pid_t pid,pid_t pgid);
// 将pid进程的进程组ID设置成pgid
// 如果参数pid为0,则会用来设置该进程的组识别码,
// 如果参数pgid为0,则由pid指定的进程ID将用作进程组ID
// 一个进程只能为它自己或它的子进程设置进程组ID,不能为其父进程设置ID。
if (setpgid(0, 0) < 0) // 设置子进程id(实际上并没有分进程组,因为一人一组)
unix_error("setpgid error");
if (execve(argv[0], argv, environ) < 0)
{
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// 父进程控制流从这里开始
addjob(jobs, pid, state, cmdline); // 将当前进程添加进jobs中,参数为当前进程pid,state,cmdline
// 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// 判断子进程类型并做处理
if (state == FG)
waitfg(pid); // 等待子进程的前台作业完成
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); // 将进程id映射到job id
}
return;
}
解析和执行bulidin命令,包括 quit, fg, bg, and jobs
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "quit")) // 如果命令是quit,退出
exit(0);
else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // 如果是bg或者fg命令,执行do_fgbg函数
do_bgfg(argv);
else if (!strcmp(argv[0], "jobs")) // 如果命令是jobs,列出正在运行和停止的后台作业
listjobs(jobs);
else
return 0; /* 不是内置命令,以0返回eval */
return 1;
}
完成内置指令bg或fg的切换进程前后台状态的操作。
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// 没有参数的fg/bg不符合规定
if (!argv[1])
{ // 命令行为空
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
// strtol函数原型:long int strtol(const char *nptr, char **endptr, int base);
// strtol函数会将参数nptr字符串根据参数base来转换成长整型数,参数base范围从2至36。
// 检测fg/bg参数,其中%开头的数字是JobID,纯数字的是PID
// 找到jobID或PID后通过这个找出job
if (argv[1][0] == '%')
{ // 解析jid
if ((num = strtol(&argv[1][1], NULL, 10)) <= 0) // 获取jid
{
printf("%s: argument must be a PID or %%jobid\n", argv[0]); // 失败,打印错误消息
return;
}
if ((job = getjobjid(jobs, num)) == NULL) // 根据jid获取job
{
printf("%%%d: No such job\n", num); // 没找到对应的job
return;
}
}
else
{ // 解析PID
if ((num = strtol(argv[1], NULL, 10)) <= 0) // 获取PID
{
printf("%s: argument must be a PID or %%jobid\n", argv[0]); // 失败,打印错误消息
return;
}
if ((job = getjobpid(jobs, num)) == NULL) // 根据PID获取job
{
printf("(%d): No such process\n", num); // 没找到对应的进程
return;
}
}
// kill函数原型: int kill(pid_t pid,int signo)
// pid > 0:将信号发送给进程 ID 为 pid 的进程。
// pid ==0:将信号发送给与发送进程属于同一进程组的所有进程。
// pid < 0:将信号发送给进程组 ID 等于 pid 的绝对值的所有进程。
// pid ==-1:将信号发送给系统中所有进程。
if (!strcmp(argv[0], "bg")) // 该进程需要在后台运行
{
// bg会启动子进程,并将其放置于后台执行
job->state = BG; // 设置状态BG
if (kill(-job->pid, SIGCONT) < 0) // 采用负数发送信号到进程组
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
}
else if (!strcmp(argv[0], "fg")) // 该进程需要在前台运行
{
job->state = FG; // 设置状态FG
if (kill(-job->pid, SIGCONT) < 0) // 采用负数发送信号到进程组
unix_error("kill error");
// 当一个进程被设置为前台执行时,当前tsh应该等待该子进程结束
waitfg(job->pid);
}
else // 指令出现异常
{
puts("do_bgfg: Internal error");
exit(0);
}
return;
}
实现阻塞等待前台程序运行结束。
job->state == FG
作为判定前台进程是否结束的依据依赖于我们实现的tinyshell中job的储存方式仅仅简单地使用了数组,并且在一开始的时候全部初始化为0,而且删除也仅仅是将其置为0而不是删除掉该片空间。若存储job的方式更改为链表或者使用malloc和free这样进程结束就找不到地方的方法,我们再写这一句话会导致段错误。if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0){err_sys("SIG_SETMASK error");} //------------>此处正好接受并处理SIGINT信号(与内核如何实现信号有关) pause();
上述代码试图用sigprocmask函数和pause函数来达成改变掩码并进入休眠的效果,
但它万万没想到倘若在这两句本该被认为一起执行的函数之间若进入了别的信号,此时函数pause将被永远阻塞,它试图达到的功能将不能被达到。
此时我们迫切地需要一个由操作系统支持的“恢复信号掩码”和“将进程放到sleep状态”能在单个原子操作内完成的操作。
void waitfg(pid_t pid)
{
// 通过pid获取该pid对应的job本体
struct job_t *job = getjobpid(jobs, pid);
if (!job)
return;
// 新设立一个wait信号集
sigset_t wait;
if (sigemptyset(&wait) < 0) // 清空wait信号集,该信号集为空
unix_error("sigemptyset &wait error");
// sigsuspend函数原型: int sigsuspend(const sigset_t *mask);
// 用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止
// 如果当前子进程的状态没有发生改变,则tsh继续休眠
while (job->state == FG)
// 一旦有信号改变,就判定是否
sigsuspend(&wait); // 使用空信号集替换信号掩码,即信号掩码为空,此时任何信号都会唤醒该进程
return;
}
sigchld信号处理例程-检测到sigchld信号就调用该程序处理僵尸子进程
僵尸进程的回收由内核完成,这里主要是在jobs中删除僵尸进程的信息并向终端输出操作信息
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if (verbose)
puts("sigchld_handler: entering"); // 输出额外信息
/*
waitpid函数原型: pid_t waitpid(pid_t pid , int *status , int options)
总体:
如果没有子进程或其它错误原因,则返回-1;
如果成功回收子进程,则返回回收的那个子进程的ID;
如果第三个参数为WNOHANG,且子进程都在运行,则返回0
参数:
pid:从参数的名字上可以看出来这是一个进程的ID。但是这里pid的值不同时,会有不同的意义。
1.pid > 0时,只等待进程ID等于pid的子进程,只要该子进程不结束,就会一直等待下去;
2.pid = -1时,等待任何一个子进程的退出,此时作用和wait相同;
3.pid = 0时,等待同一个进程组中的任何子进程;
4.pid < -1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
options:options提供了一些额外的选项来控制waitpid
WNOHANG : 若子进程仍然在运行,则返回0
(注意只有设置了这个标志,waitpid才有可能返回0)
WUNTRACED : 如果子进程由于传递信号而停止,则马上返回。
(只有设置了这个标志,waitpid返回时,其WIFSTOPPED(status)才有可能返回true)
&status参数:
WIFEXITED(status)
如果正常退出(exit)返回非零值;这时可以用WEXITSTATUS(status) 得到退出编号(exit的参数)
WIFSIGNALED(status)
如果异常退出 (子进程接受到退出信号) 返回非零值;使用WTERMSIG (status) 得到使子进程退出得信号编号
WIFSTOPPED(status)
如果是暂停进程返回的状态,返回非零值;使用WSTOPSIG(status) 得到使子进程暂停得信号编号
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) // 以非阻塞方式等待所有子进程,若成功回收了子进程,则返回这个子进程的PID,&status中返回其状态
{
// 如果当前这个子进程的job已经删除了,则表示有错误发生
if ((job = getjobpid(jobs, pid)) == NULL)
{
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
// 接下来判断三种状态
// 如果这个子进程收到了一个暂停信号(还没退出)
if (WIFSTOPPED(status))
{
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
// 使用WSTOPSIG(status) 得到使子进程暂停得信号编号
job->state = ST; // 状态设为挂起
}
// 如果子进程通过调用 exit 或者一个返回 (return) 正常终止
else if (WIFEXITED(status))
{
if (deletejob(jobs, pid))
if (verbose)
{
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
printf("sigchld_handler: Job [%d] (%d) terminates OK (status %d)\n", jid, pid, WEXITSTATUS(status));
// 用WEXITSTATUS(status) 得到退出编号(exit的参数)
}
}
// 如果子进程是因为一个未被捕获的信号终止的,例如SIGKILL
else
{
if (deletejob(jobs, pid))
{ // 清除进程
if (verbose)
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
}
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
// 使用WTERMSIG (status) 得到使子进程退出得信号编号
}
}
if (verbose)
puts("sigchld_handler: exiting");
return;
}
处理由终端输入ctrl-c引起的异常,将前台作业结束(实现由操作系统内核完成,这里的作用是以内核特权向进程发送SIGINT信号,这个是kill函数做到的)
void sigint_handler(int sig)
{
if (verbose)
puts("sigint_handler: entering");
pid_t pid = fgpid(jobs);//获取前台进程的pid
if (pid)
{
// 发送SIGINT给前台进程组里的所有进程
// 需要注意的是,前台进程组内的进程除了当前前台进程以外,还包括前台进程的子进程。
// 最多只能存在一个前台进程,但前台进程组内可以存在多个进程
if (kill(-pid, SIGINT) < 0) // 采用负数发送信号到进程组,使该进程结束
unix_error("kill (sigint) error");
if (verbose)
{
printf("sigint_handler: Job (%d) killed\n", pid);
}
}
if (verbose)
puts("sigint_handler: exiting");
return;
}
处理由终端输入ctrl-z引起的异常,将前台作业暂停(实现由操作系统内核完成,这里的作用是以内核特权向进程发送SIGTSTP信号,这个是kill函数做到的)
void sigtstp_handler(int sig)
{
if (verbose)
puts("sigstp_handler: entering");
pid_t pid = fgpid(jobs); // 获取前台进程的pid号
struct job_t *job = getjobpid(jobs, pid); // 获取前台进程的job本体
if (pid)
if (kill(-pid, SIGTSTP) < 0) // 采用负数发送信号到进程组,使该进程挂起
unix_error("kill (tstp) error");
if (verbose)
{
printf("sigstp_handler: Job [%d] (%d) stopped\n", job->jid, pid);
}
if (verbose)
puts("sigstp_handler: exiting");
return;
}
之前说过,我们可以单个地进行验证,逐个验证各个模块的正确性。
#
# trace01.txt - Properly terminate on EOF.
#
CLOSE
WAIT
分析:
调用linux命令close关闭文件并wait等待,在EOF上正常终止。最基础的测试。
运行截图:
#
# trace02.txt - Process builtin quit command.
#
quit
WAIT
分析:
针对输入的命令quit退出shell进程,需要解析cmdline,判断若为“quit”字符串则退出。该题需要完成eval函数和builtin_cmd函数。
运行截图:
#
# trace03.txt - Run a foreground job.
#
/bin/echo tsh> quit
quit
分析:
eval函数先通过builtin_cmd查询cmdline是不是内置命令如quit,如果是则当前进程执行命令
如果不是则创建一个子进程,在子进程中调用 execve()函数通过 argv[0]来寻找路径,并在子进程中运行路径中的可执行文件,如果找不到则说明命令为无效命令,输出命令无效,并用 exit(0)结束该子进程
/bin/echo就是打开bin目录下的echo文件,echo是一个常见的测试指令,功能是将其后面的内容当作字符串输出。
运行截图:
#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &
分析:
先在前台执行echo命令,等待程序执行完毕回收子进程。&代表是一个后台程序,myspin睡眠1秒,然后停止。因为成功实现了后台程序的运行,所以会输出后台程序的运行信息。
运行截图:
#
# trace05.txt - Process jobs builtin command.
#
/bin/echo -e tsh> ./myspin 2 \046
./myspin 2 &
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo tsh> jobs
jobs
分析:
分别运行了前台echo、后台myspin、前台echo、后台myspin,然后需要实现一个内置命令job,功能是显示目前任务列表中的所有任务的所有属性。
运行截图:
#
# trace06.txt - Forward SIGINT to foreground job.
#
/bin/echo -e tsh> ./myspin 4
./myspin 4
SLEEP 2
INT
分析:
接收到了中断信号SIGINT(即CTRL_C)那么结束前台进程
运行截图:
#
# trace07.txt - Forward SIGINT only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
INT
/bin/echo tsh> jobs
jobs
分析:
只将SIGINT转发给前台作业。测试方法是给出两个作业,一个在前台工作,另一个在后台工作,接下来传递SIGINT指令,然后调用内置指令jobs来查看此时的工作信息,来对比出是不是只将SIGINT转发给前台作业。
运行截图:
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
分析:
只将SIGTSTP转发给前台作业。根据这个信号的作用,也就是该进程会停止直到下一个SIGCONT也就是挂起,让别的程序继续运行。这里也就是运行了后台程序,然后使用jobs来打印出进程的信息,进行对比。
运行截图:
#
# trace09.txt - Process bg builtin command
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> bg %2
bg %2
/bin/echo tsh> jobs
jobs
分析:
这个题目的作用是测试bg命令能否让一个被挂起(暂停)的程序(进程2)继续在后台运行。在停止后,先输出进程信息之后,使用bg命令来唤醒进程2,也就是刚才被挂起的程序,接下来继续使用Jobs命令来输出结果。
运行截图:
#
# trace10.txt - Process fg builtin command.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
SLEEP 1
/bin/echo tsh> fg %1
fg %1
SLEEP 1
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> jobs
jobs
分析:
测试fg能否让一个被挂起的程序继续在前台运行。
运行截图:
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
INT
/bin/echo tsh> /bin/ps a
/bin/ps a
分析:
这里需要将SIGINT发给前台进程组中的每个进程。ps –a 显示所有进程,这里是有两个进程的,mysplit创建了一个子进程,接下来发送指令SIGINT,所以进程组中的所有进程都应该停止,接下来调用ps -a来查看该进程组中的每个进程是否都停止了。
运行截图:
#
# trace12.txt - Forward SIGTSTP to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> /bin/ps a
/bin/ps a
分析:
测试将SIGTSTP转发给前台进程组中的每个进程,测试方法跟上一题差不多。
运行截图:
#
# trace13.txt - Restart every stopped process in process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> /bin/ps a
/bin/ps a
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> /bin/ps a
/bin/ps a
分析:
测试fg是否能唤醒整个工作,使用ps -a来查看停止整个工作和唤醒整个工作的区别
运行截图:
#
# trace14.txt - Simple error handling
#
/bin/echo tsh> ./bogus
./bogus
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo tsh> fg
fg
/bin/echo tsh> bg
bg
/bin/echo tsh> fg a
fg a
/bin/echo tsh> bg a
bg a
/bin/echo tsh> fg 9999999
fg 9999999
/bin/echo tsh> bg 9999999
bg 9999999
/bin/echo tsh> fg %2
fg %2
/bin/echo tsh> fg %1
fg %1
SLEEP 2
TSTP
/bin/echo tsh> bg %2
bg %2
/bin/echo tsh> bg %1
bg %1
/bin/echo tsh> jobs
jobs
分析:
检测错误处理系统是否正确。
运行截图:
#
# trace15.txt - Putting it all together
#
/bin/echo tsh> ./bogus
./bogus
/bin/echo tsh> ./myspin 10
./myspin 10
SLEEP 2
INT
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> bg %3
bg %3
/bin/echo tsh> bg %1
bg %1
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> quit
quit
分析:
综合测试。
运行截图:
#
# trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT
# signals that come from other processes instead of the terminal.
#
/bin/echo tsh> ./mystop 2
./mystop 2
SLEEP 3
/bin/echo tsh> jobs
jobs
/bin/echo tsh> ./myint 2
./myint 2
分析:
测试shell是否可以处理来自其他进程而不是终端的SIGTSTP和SIGINT信号。
运行截图:
我们可以修改makefile,自己给它新增一个功能,使它能够一次性打印所有的测试点数据。跟参考输出的比对可以使用meld工具进行进行。
在原makefile文件下方加入以下代码
(本质是把之前单次运行的指令给一次性输出了)
testall:
$(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace02.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace03.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace04.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace05.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace06.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace07.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace08.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace09.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace10.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace11.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace12.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace13.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace14.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace15.txt -s $(TSH) -a $(TSHARGS)
$(DRIVER) -t trace16.txt -s $(TSH) -a $(TSHARGS)
rtestall:
$(DRIVER) -t trace01.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace02.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace03.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace04.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace05.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace06.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace07.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace08.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace09.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace10.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace11.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace12.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace13.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace14.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace15.txt -s $(TSHREF) -a $(TSHARGS)
$(DRIVER) -t trace16.txt -s $(TSHREF) -a $(TSHARGS)
★请等待一条指令运行完成后再执行下一条指令
★特别不要开两个终端同时并行操作,因为执行测试机时涉及到运行ps -a
,如果两个同时运行,会将另一个终端也显示进去,导致my_output和reference_output差距很大。
在终端依次输入以下指令
make testall > my_output.txt
make rtestall > reference_output.txt
从my_output.txt和reference_output.txt分别拿到结果。
这里使用的是ubuntu20.04,因为我们原本的ubuntu14.04太古老了,找不到meld包,没法安装meld。
通过meld比对可以发现,我们的和参考的只有进程编号上有区别,其余都是一致的。
进程编号上的区别是必然存在的,因为操作系统是Linux操作系统,我们依次生成的实际上都是子进程。这些在Linux下生成的子进程都对应着自己的PID,这是独有的,所以会不一样。
通过比对可以确认实验的结果基本正确。
进程的创建与销毁都是内核级的操作,这些操作由操作系统完成,一般来说用户是没有权限看到的。用户能做的只是请求创建进程、销毁进程、中断、结束等。Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件,它的作用是沟通用户与内核。我们的tsh(也就是tinyshell)所做的事情就是Shell的简化版,它简化到只有4个信号(SIGINT,SIGTSTP,SIGCHLD,SIGCONT),只有4个内置命令(bg,fg,jobs,quit)。它起到的作用也是沟通我们用户与内核,满足我们用户对于操作系统的要求,包括谁在前台运行,运行什么,谁在后台运行,运行什么等。
实际上,我认为tinyshell有两个视角去观察它。
其中带方框的为我们需要实现的部分。
从函数的视角理解,可以让我们将不同的函数与过程之间互相调用的关系看的更加透彻,能知道什么时候干什么事情。
每个进程都维护着两个集合。称为阻塞信号集(也称作屏蔽词集)和未决信号集。
阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于未决状态的信号的集合,这两个集合存储在内核的PCB中。
下面以SIGINT为例说明信号未决信号集和阻塞信号集的关系:
当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:
如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集 上该位置上的值保持为1,表示该信号处于未决状态;
如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。
当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。
事实上,main函数一开始就调用如下的signal函数,这些函数的作用是,一旦进程接收到这4个信号,就立刻调用相应的处理例程。实际上,其中的三个就是我们要实现的。
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
Signal(SIGQUIT, sigquit_handler);
然后,在函数的实现中,信号相关的函数也如影随形。
我们遇到了如下的信号设置函数,它们能够原子性地执行设置或修改信号集或者信号。
sigemptyset: int sigemptyset(sigset_t *set);
sigaddset: int sigaddset(sigset_t *set,int signum);
sigprocmask: int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset);
sigsuspend: int sigsuspend(const sigset_t *mask);
特别地,关注到对于信号的管理更倾向于使用set来进行整合与打包,将几个信号放入信号集里面一起操作。
比如void waitfg(pid_t pid)函数中的sigsuspend就可以原子性地将我们自定义的wait空信号集替换本进程的原有阻塞信号集,从而使得该进程可以被任何信号唤醒。
从信号的视角理解,可以让我们对于不同进程之间的通信有更深入的洞察。
这里存在两个并发问题
(1)jobs数组是一个临界缓冲区,addjob函数和deletejob函数的访问需要持有锁。
(2)sigchld处理函数处理SIGCHLD信号的时候,同时至多处理两个子进程的删除。此时需要用while信号处理同时并发的多个子进程
在完整代码中加入了自己的注释与理解。
/*
* tsh - A tiny shell program with job control
* 202108010206 ArcticWolf
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* 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> "; /*命令行提示符(DO NOT CHANGE) */
int verbose = 0; /*是否输出额外信息*/
int nextjid = 1; /*要分配的下一个作业ID*/
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 - shell的主函数
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* 将stderr重定向到stdout(这样驱动程序将获得连接到stdout的管道上的所有输出) */
dup2(1, 2);
/* 解析命令行 */
while ((c = getopt(argc, argv, "hvp")) != EOF)
{
switch (c)
{
case 'h': /* 打印提示信息 */
usage();
break;
case 'v': /* 发出附加诊断信息 */
verbose = 1;
break;
case 'p': /* 不打印提示 */
emit_prompt = 0; /* 便于自动测试 */
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);
/* 初始化 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 - 分析命令,并派生子进程执行 主要功能是解析cmdline并运行
如果用户请求了一个内置命令(quit、jobs、bg或fg),则立即执行。否则,派生一个子进程并在该子进程的上下文中运行作业。如果作业正在前台运行,请等待它终止,然后返回。注意:每个子进程都必须有一个唯一的进程组ID,这样当我们在键盘上键入ctrl-c(ctrl-z)时,我们的后台子进程就不会从内核接收SIGINT(SIGTSTP)。
*/
void eval(char *cmdline)
{
char *argv[MAXARGS]; // execve()函数的参数
int state = UNDEF; // 工作状态,FG或BG
sigset_t set;
pid_t pid; // 进程id
// 处理输入的数据
if (parseline(cmdline, argv) == 1) // 解析命令行,返回给argv数组
state = BG;
else
state = FG;
if (argv[0] == NULL) // 若命令行为空,parseline也会返回1,但对argv[0]判定后,eval在这里直接返回
return;
// 如果不是内置命令
if (!builtin_cmd(argv)) // 若是内置命令,builtin_cmd(argv)会执行,若该函数返回0,则表示非内置命令
{
// 初始化信号集set并把SIGINT SIGTSTP SIGCHLD三个信号放入信号集中,方便管理
if (sigemptyset(&set) < 0)
unix_error("sigemptyset error");
if (sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)
unix_error("sigaddset error");
// 阻塞SIGCHLD信号,具体原因在书P541有解释
// 主要是防止fork之后调度执行子进程并在addjob之前结束子进程,
// 此时SIGCHLD信号使父进程将子进程回收,这会导致 addjob 和 deletejob 函数执行错位
// 结果是删除一个不存在的进程号,添加一个不存在且永不会被删除的进程号
if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if ((pid = fork()) < 0) // fork创建子进程失败
unix_error("fork error");
else if (pid == 0) // fork创建子进程
{
// 子进程控制流从这里开始
if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) // 解除阻塞
unix_error("sigprocmask error");
// 函数原型 int setpgid(pid_t pid,pid_t pgid);
// 将pid进程的进程组ID设置成pgid
// 如果参数pid为0,则会用来设置该进程的组识别码,
// 如果参数pgid为0,则由pid指定的进程ID将用作进程组ID
// 一个进程只能为它自己或它的子进程设置进程组ID,不能为其父进程设置ID。
if (setpgid(0, 0) < 0) // 设置子进程id(实际上并没有分进程组,因为一人一组)
unix_error("setpgid error");
if (execve(argv[0], argv, environ) < 0)
{
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
// 父进程控制流从这里开始
addjob(jobs, pid, state, cmdline); // 将当前进程添加进jobs中,参数为当前进程pid,state,cmdline
// 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// 判断子进程类型并做处理
if (state == FG)
waitfg(pid); // 等待子进程的前台作业完成
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); // 将进程id映射到job id
}
return;
}
/*
* parceline - 分析命令行并构建argv数组
单引号中的字符被视为一个参数。如果用户已请求BG作业,则返回true;如果用户已申请FG作业,则为false
*/
int parseline(const char *cmdline, char **argv)
{
static char array[MAXLINE]; /* 创建cmd的备份 */
char *buf = array; /* 遍历命令行的ptr */
char *delim; /* 迭代指针(指向当前处理位置) */
int argc; /* argv向量个数 */
int bg; /* 是否后台运行标志 */
strcpy(buf, cmdline); // 在本地生成cmdline的副本buf
buf[strlen(buf) - 1] = ' '; /*将cmdline最后的'\n'替换为空格符*/
while (*buf && (*buf == ' ')) /*忽略前导空格*/
buf++;
/*生成参数向量(agrv)列表*/
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 == ' ')) /*忽略空格*/
buf++;
if (*buf == '\'')
{
buf++;
delim = strchr(buf, '\'');
}
else
{
delim = strchr(buf, ' ');
}
}
argv[argc] = NULL;
if (argc == 0) /*若cmdline是空的,返回1*/
return 1;
/*是否后台运行*/
if ((bg = (*argv[argc - 1] == '&')) != 0)
{
argv[--argc] = NULL; // 去掉&
}
return bg; // 若最后有&,则bg=1,parseline会返回1
}
/*
* builtin_cmd:解析和执行bulidin命令,包括 quit, fg, bg, jobs
*/
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "quit")) // 如果命令是quit,退出
exit(0);
else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // 如果是bg或者fg命令,执行do_fgbg函数
do_bgfg(argv);
else if (!strcmp(argv[0], "jobs")) // 如果命令是jobs,列出正在运行和停止的后台作业
listjobs(jobs);
else
return 0; /* 不是内置命令,以0返回eval */
return 1;
}
/*
* do_bgfg - 执行bg和fg命令
*/
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// 没有参数的fg/bg不符合规定
if (!argv[1])
{ // 命令行为空
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
// strtol函数原型:long int strtol(const char *nptr, char **endptr, int base);
// strtol函数会将参数nptr字符串根据参数base来转换成长整型数,参数base范围从2至36。
// 检测fg/bg参数,其中%开头的数字是JobID,纯数字的是PID
// 找到jobID或PID后通过这个找出job
if (argv[1][0] == '%')
{ // 解析jid
if ((num = strtol(&argv[1][1], NULL, 10)) <= 0) // 获取jid
{
printf("%s: argument must be a PID or %%jobid\n", argv[0]); // 失败,打印错误消息
return;
}
if ((job = getjobjid(jobs, num)) == NULL) // 根据jid获取job
{
printf("%%%d: No such job\n", num); // 没找到对应的job
return;
}
}
else
{ // 解析PID
if ((num = strtol(argv[1], NULL, 10)) <= 0) // 获取PID
{
printf("%s: argument must be a PID or %%jobid\n", argv[0]); // 失败,打印错误消息
return;
}
if ((job = getjobpid(jobs, num)) == NULL) // 根据PID获取job
{
printf("(%d): No such process\n", num); // 没找到对应的进程
return;
}
}
// kill函数原型: int kill(pid_t pid,int signo)
// pid > 0:将信号发送给进程 ID 为 pid 的进程。
// pid ==0:将信号发送给与发送进程属于同一进程组的所有进程。
// pid < 0:将信号发送给进程组 ID 等于 pid 的绝对值的所有进程。
// pid ==-1:将信号发送给系统中所有进程。
if (!strcmp(argv[0], "bg")) // 该进程需要在后台运行
{
// bg会启动子进程,并将其放置于后台执行
job->state = BG; // 设置状态BG
if (kill(-job->pid, SIGCONT) < 0) // 采用负数发送信号到进程组
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
}
else if (!strcmp(argv[0], "fg")) // 该进程需要在前台运行
{
job->state = FG; // 设置状态FG
if (kill(-job->pid, SIGCONT) < 0) // 采用负数发送信号到进程组
unix_error("kill error");
// 当一个进程被设置为前台执行时,当前tsh应该等待该子进程结束
waitfg(job->pid);
}
else // 指令出现异常
{
puts("do_bgfg: Internal error");
exit(0);
}
return;
}
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
// 通过pid获取该pid对应的job本体
struct job_t *job = getjobpid(jobs, pid);
if (!job)
return;
// 新设立一个wait信号集
sigset_t wait;
if (sigemptyset(&wait) < 0) // 清空wait信号集,该信号集为空
unix_error("sigemptyset &wait error");
// sigsuspend函数原型: int sigsuspend(const sigset_t *mask);
// 用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止
// 如果当前子进程的状态没有发生改变,则tsh继续休眠
while (job->state == FG)
// 一旦有信号改变,就判定是否
sigsuspend(&wait); // 使用空信号集替换信号掩码,即信号掩码为空,此时任何信号都会唤醒该进程
return;
}
/*****************
* Signal handlers
*****************/
/*
* sigchld_handler- 处理僵尸子进程例程
每当子作业终止(变成僵尸),或者因为接收到SIGSTOP或SIGTSTP信号而停止时,内核都会向shell发送sigchld。处理程序获取所有可用的僵尸子进程,但不等待任何其他当前正在运行的子进程终止。
*/
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if (verbose)
puts("sigchld_handler: entering"); // 输出额外信息
/*
waitpid函数原型: pid_t waitpid(pid_t pid , int *status , int options)
总体:
如果没有子进程或其它错误原因,则返回-1;
如果成功回收子进程,则返回回收的那个子进程的ID;
如果第三个参数为WNOHANG,且子进程都在运行,则返回0
参数:
pid:从参数的名字上可以看出来这是一个进程的ID。但是这里pid的值不同时,会有不同的意义。
1.pid > 0时,只等待进程ID等于pid的子进程,只要该子进程不结束,就会一直等待下去;
2.pid = -1时,等待任何一个子进程的退出,此时作用和wait相同;
3.pid = 0时,等待同一个进程组中的任何子进程;
4.pid < -1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
options:options提供了一些额外的选项来控制waitpid
WNOHANG : 若子进程仍然在运行,则返回0
(注意只有设置了这个标志,waitpid才有可能返回0)
WUNTRACED : 如果子进程由于传递信号而停止,则马上返回。
(只有设置了这个标志,waitpid返回时,其WIFSTOPPED(status)才有可能返回true)
&status参数:
WIFEXITED(status)
如果正常退出(exit)返回非零值;这时可以用WEXITSTATUS(status) 得到退出编号(exit的参数)
WIFSIGNALED(status)
如果异常退出 (子进程接受到退出信号) 返回非零值;使用WTERMSIG (status) 得到使子进程退出得信号编号
WIFSTOPPED(status)
如果是暂停进程返回的状态,返回非零值;使用WSTOPSIG(status) 得到使子进程暂停得信号编号
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) // 以非阻塞方式等待所有子进程,若成功回收了子进程,则返回这个子进程的PID,&status中返回其状态
{
// 如果当前这个子进程的job已经删除了,则表示有错误发生
if ((job = getjobpid(jobs, pid)) == NULL)
{
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
// 接下来判断三种状态
// 如果这个子进程收到了一个暂停信号(还没退出)
if (WIFSTOPPED(status))
{
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
// 使用WSTOPSIG(status) 得到使子进程暂停得信号编号
job->state = ST; // 状态设为挂起
}
// 如果子进程通过调用 exit 或者一个返回 (return) 正常终止
else if (WIFEXITED(status))
{
if (deletejob(jobs, pid))
if (verbose)
{
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
printf("sigchld_handler: Job [%d] (%d) terminates OK (status %d)\n", jid, pid, WEXITSTATUS(status));
// 用WEXITSTATUS(status) 得到退出编号(exit的参数)
}
}
// 如果子进程是因为一个未被捕获的信号终止的,例如SIGKILL
else
{
if (deletejob(jobs, pid))
{ // 清除进程
if (verbose)
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
}
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
// 使用WTERMSIG (status) 得到使子进程退出得信号编号
}
}
if (verbose)
puts("sigchld_handler: exiting");
return;
}
/*
* sigint_handler - 当用户在键盘上键入ctrl-c时,内核会向shell发送一个SIGINT。抓住它并将其发送到前台作业。
*/
void sigint_handler(int sig)
{
if (verbose)
puts("sigint_handler: entering");
pid_t pid = fgpid(jobs); // 获取前台进程的pid
if (pid)
{
// 发送SIGINT给前台进程组里的所有进程
// 需要注意的是,前台进程组内的进程除了当前前台进程以外,还包括前台进程的子进程。
// 最多只能存在一个前台进程,但前台进程组内可以存在多个进程
if (kill(-pid, SIGINT) < 0) // 采用负数发送信号到进程组,使该进程终止
unix_error("kill (sigint) error");
if (verbose)
{
printf("sigint_handler: Job (%d) killed\n", pid);
}
}
if (verbose)
puts("sigint_handler: exiting");
return;
}
/*
* sigtstp_handler - 每当用户在键盘上键入ctrl-z时,内核都会向shell发送一个sigtstp。捕获它并通过向它发送SIGTSTP来挂起前台作业。
*/
void sigtstp_handler(int sig)
{
if (verbose)
puts("sigstp_handler: entering");
pid_t pid = fgpid(jobs); // 获取前台进程的pid号
struct job_t *job = getjobpid(jobs, pid); // 获取前台进程的job本体
if (pid)
if (kill(-pid, SIGTSTP) < 0) // 采用负数发送信号到进程组,使该进程挂起
unix_error("kill (tstp) error");
if (verbose)
{
printf("sigstp_handler: Job [%d] (%d) stopped\n", job->jid, pid);
}
if (verbose)
puts("sigstp_handler: exiting");
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);
}
CSDN:
https://blog.csdn.net/qq_51684393/article/details/124888703
CSAPP:第八章-异常控制流