一、准备工作
Hints
• 这是基于CSAPP教材第八章的配套实验。
• 使用跟踪文件来指导 shell 的开发。从 trace01.txt 开始,确保 shell 产生与reference shell 相同的输出。然后继续跟踪文件 trace02.txt,以此类推。
• waitpid、kill、fork、execve、setpgid 和 sigprocmaskfunctions将会被用到。waitpid 的WUNTRACED 和 WNOHANG 选项也会被用到。
• 当我们实现信号处理程序时,请确保向整个前台进程组发送 SIGINT 和 SIGTSTP 信号,在kill 函数的参数中使用“-pid”而不是“pid”。sdriver.pl 程序可以测试此错误。
• 这项任务的棘手部分之一是决定 waitfg和 sigchldhandler 函数之间的工作分配。我们建议采用以下方法:
– In waitfg, use a busy loop around the sleep function.
– In sigchld handler, use exactly one call to waitpid.
• 在 eval中,父进程必须在分叉子进程之前使用 sigprocmask来阻塞 SIGCHLD信号,然后取消阻塞这些信号,在通过调用 addjob 将子进程添加到作业列表之后,再次使用 sigprocmask。因为孩子继承了他们父母被阻止的向量,所以在执行新程序之前,孩子必须确保解除对SIGCHLD 信号的阻止。父进程需要以这种方式阻塞 SIGCHLD 信号,以避免子进程在父进程调用 addjob 之前被sigchldhandler 捕获(并因此从作业列表中删除)的竞争情况。
• more, less, vi 和 emacs等程序会对终端设置做一些奇怪的事情。不要从 shell 运行这些程序。坚持使用简单的基于文本的程序,如/bin/ls、/bin/ps 和/bin/echo。
• 当从标准的 Unix shell 运行您的 shell 时,我们的 shell 正在前台进程组中运行。如果我们的 shell创建了一个子进程,默认情况下,该子进程也是前台进程组的成员。由于键入 ctrl-c 会向前台组中的每个进程发送一个 SIGINT,因此键入 ctrl-c 会向我们的 shell 以及 shell 创建的每个进程发送一个 SIGINT,这显然是不正确的。
解决方法是:在 fork 之后,但在 execve 之前,子进程应该调用 set GID(0,0),这将子进程放在一个新的进程组中,该进程组的组标识与子进程的组标识相同。这确保前台进程组中只有一个进程,即 shell。当键入 ctrl-c 时,shell 应该捕获结果 SIGINT,然后将其转发到适当的前台作业(或者更准确地说,包含前台作业的进程组)。
实验介绍
首先输入命令tar xvf shlab-handout.tar来扩展目标文件。不要解压之后再上传到服务器,否则会出现下图所示的情况。
然后输入make 命令来编译tsh.c文件,如果程序被修改则需要先输入make clean指令。
- eval:读取指令并产生子进程执行。
- builtin_cmd:执行内置命令,bg、fg、quit、jobs。 do_bgfg:处理fg和bg操作。
- waitfg:等待前台进程结束。
- sigchld_handler:捕捉SIGCHILD信号,给子进程收尸,避免产生僵尸进程。
- sigint_handler:捕捉SIGINT(ctrl-c)信号,将SIGINT信号发给前台进程组。
- sigtstp_handler:捕捉SIGTSTP(ctrl-z)信号,将SIGTSTP信号发给前台进程组。
- 使用make testXX和make rtestXX指令比较traceXX.txt文件在编写的shell和reference shell的运行结果;或者也可以使用”./sdriver.pl -t traceXX.txt -s ./tsh -a “-p”和”./sdriver.pl -t traceXX.txt -s ./tshref -a “-p”
跟系统任务相关的几个命令:fg、bg、jobs、&、ctrl+z。
- & :这个用在一个命令的最后,可以把这个命令放到后台执行;
- ctrl + z:可以将一个正在前台执行的命令放到后台,并且暂停;
- jobs:查看当前有多少在后台运行的命令,列举出后台作业信息([作业号] 运行状态 作业名称);
- fg:将后台中的命令调至前台继续运行,如果后台中有多个命令,可以用 fg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid);
- bg:将一个在后台暂停的命令,变成继续执行,如果后台中有多个命令,可以用bg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid)。
二、对实验的一些研究
Step1
输入make rtest01和make test01;因为trace01.txt中只有CLOSE和WAIT命令,在EOF上正常终止。
Step2
输入make rtest02和make test02;tshref能够正常退出,而tsh因为quit的内置命令还没有写,所以不能正常退出。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210129081532266.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTk3NTU3NQ==,size_16,color_FFFFFF,t_70
Step3
trace03.txt中tsh>quit是打开bin目录下的echo可执行文件,在foreground开启一个子进程运行它(如果末尾有&,则说明是在background运行)。
运行echo进程时,通过tsh>quit命令。调用tsh并执行内置命令quit,退出子进程。
最后在tsh中执行内置命令quit,退出tsh进程,回到终端。
Step4
了解tsh.c中作业表job struct和操作管理函数(如addjob())
(1)文件符号:
空格:用来分隔命令和参数或者参数与参数;
&:如果一个命令以&结尾,shell应该在后台运行它,否则在前台运行;
#:以 # 开头的行就是注释,会被解释器忽略。
2)命令:
包括内建命令和外部命令。内置命令包括quit、jobs、bg、fg等。可以使用type来确定一个命令是否是内置命令。
用户程序myspin:使用myspin 指令可将进程挂起n秒
int main(int argc, char **argv) {
int i, secs;
pid_t pid;
//判断命令长度是否为2,若不为2则输出错误信息并终止程序
if (argc != 2) { fprintf(stderr, "Usage: %s <n>\n", argv[0]); exit(0);
}
secs = atoi(argv[1]); //将字符串转换为整型
for (i=0; i < secs; i++) //将进程挂起secs秒
sleep(1);
pid = getpid();
if (kill(pid, SIGINT) < 0)
fprintf(stderr, "kill (int) error");
exit(0);
}
Step5
编程实现jobs内建命令,使用trace05验证。在原有builtin_cmd函数中添加一个判断函数,如果参数是jobs,则执行listjobs函数的功能。
if(!strcmp(argv[0],"jobs"))
{
listjobs(jobs);
return 1;
}
/* listjobs - Print the job list */
void listjobs(struct job_t *jobs)
{
int i;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid != 0) {
printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
switch (jobs[i].state) {
case BG:
printf("Running ");
break;
case FG:
printf("Foreground ");
break;
case ST:
printf("Stopped ");
break;
default:
printf("listjobs: Internal error: job[%d].state=%d ",
i, jobs[i].state);
}
printf("%s", jobs[i].cmdline);
}
}
}
定义一个作业的结构体,该结构体具有的属性有作业的作业号、进程号、状态、命令行参数
struct job_t { /* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
下列函数进行清空、初始化、最大作业号、添加、删除功能。
void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs);
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *jobs, pid_t pid);
Step6
编程实现sigint_handler()、waitfg(),sigchld_handler(),验证trace06~07.
了解接收信号、信号处理、信号阻塞概念
- 接收信号:当目的进程被内核强迫以方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理 程序的用户层函数捕获这个信号。
- 信号处理:signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
①如果handler是SIG_IGN,那么忽略类型为signum的信号;
②如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认模式;
③否则,handler就是用户定义的函数的地址,这个函数称为信号处理程序;- 当一个程序要捕获多个信号时,会有以下问题:
待处理信号被阻塞;待处理信号不会排队等待;系统调用可以被中断。- 信号阻塞:Unix信号处理程序通常会阻塞当前处理程序正在处理类型的待处理信号。
Step7
编程实现sigtstp_handler捕获TSTP响应
Step8
编程实现内置命令bg和fg的do_bgfg()处理函数;比较trace09~10执行不同结果,在trace09中执行完bg %2命令后会将作业2放到后台运行,所以最终【2】后面会显示running;在trace10中执行fg %1会将后台的作业1停止之后添加到前台运行。
Step9
Trace11:./mysplit 4创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGINT 信号使子进程终止。
Trace12:./mysplit 4 创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGTSTP 信号使子进程停止直到下一个 SINCONT,因此执行 jobs 指令,可以看到子进程处 于 Stopped 状态,用 ps a 指令查看。
Trace13:./mysplit 4 创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGTSTP 信号使子进程停止直到接收一个 SINCONT 信号,因此执行 jobs 指令,可以看到子进程处于 Stopped 状态,用 ps a 指令查看;然后执行 fg %1 指令, 将后台停止的作业 1 切换至前台运行,再次使用 ps a 指令查看。
Step10
Trace14:处理输入未实现的命令,fg 和 bg 参数不正确等错误情况。
Step11
因为没有实现命令 bogus,所以执行./bogus 命令会报错。执行./myspin 10 命令,挂起10秒,但在挂起2秒后被SIGINT信号终止。./myspin 3 &和./myspin 4 &分别在后台执行./myspin 3 和./myspin 4 命令,且前者作业号为1,后者作业号为2。此时使用 jobs 命令查看,两者都在后台运行。使用 fg %1 命令将作业1切换至前台运行,挂起2秒后,发送 SIGTSTP 信号使其停止直到接收一个SINCONT 信号。此时再次使用 jobs 命令查 看,发现作业1已经处于Stopped状态,而作业2仍然处于 Running 状态。因为没有作业3,所以执行bg %3 命令会报错。然后执行 bg %1命令将已经在后台停止的作业1切换至运行状态,即重启作业1。此时使用jobs命令查看,作业1和作业2都处于后台运行状态。然后执行fg %1 命令将作业从后台运行状态切换至前台运行状态。
三、代码
1、eval
void eval(char *cmdline)
{
char *argv[MAXARGS];
char buf[MAXLINE];
int bg;
sigset_t prev;
sigset_t mask;
pid_t pid;
sigprocmask(SIG_BLOCK,NULL,&mask);
sigaddset(&mask,SIGCHLD);
strcpy(buf,cmdline);
bg=parseline(buf,argv);//内置函数,前台任务返回0,后台任务返回1
if(argv[0]==NULL) return;
if(!builtin_cmd(argv))//非内置指令时执行
{
sigprocmask(SIG_BLOCK,&mask,&prev);//屏蔽SIGCHLD
if((pid=fork())==0)//子进程
{
sigprocmask(SIG_SETMASK,&prev,NULL);
setpgid(0,0);//每个任务单独开一个进程组,方便信号处理中使用kill
if(execve(argv[0],argv,environ)<0)
{
printf("%s:Command not found.\n",argv[0]);
exit(0);
}
}
else//父进程
{
if(bg)//后台任务
addjob(jobs,pid,BG,cmdline);
else//前台任务
addjob(jobs,pid,FG,cmdline);
sigprocmask(SIG_SETMASK,&prev,NULL);
if(bg)//后台任务不等待任务结束
printf("[%d](%d)%s",pid2jid(pid),pid,cmdline);
else//前台任务,等待任务结束
waitfg(pid);
}
}
return;
}
2、builtin_cmd
int builtin_cmd(char** argv)
{
if (!strcmp(argv[0], "quit")) {
exit(0);
}
if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0], "jobs")) {
//访问全局变量,阻塞所有信号
//sigset_t mask_all, prev_mask;
//sigfillset(&mask_all);
//sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
listjobs(jobs);
//sigprocmask(SIG_SETMASK, &prev_mask, NULL);
return 1;
}
if(!strcmp(argv[0], "&")){
return 1;
}
return 0; /* not a builtin command */
}
char* argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid;
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL) {
return;
}
if (!builtin_cmd(argv)) {
sigset_t mask_chld, prev_mask, mask_all;
sigemptyset(&mask_chld);
sigaddset(&mask_chld, SIGCHLD);
sigfillset(&mask_all);
/*因为子进程可能在addjob前就结束并调用deleltejob,所以我们要先阻塞掉SIGCHLD,
保证addjob操作成功*/
sigprocmask(SIG_BLOCK, &mask_chld, &prev_mask);
if ((pid = fork()) == 0) {
//子进程默认继承父进程的mask,所以这里要恢复一下
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
setpgid(0, 0); //令进程组号等于进程号
if (execve(argv[0], argv, environ) <= 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
// addjob涉及到全局变量的操作,需要保证操作的原子性,故这里阻塞掉所有信号
sigprocmask(SIG_SETMASK, &mask_all, NULL);
addjob(jobs, pid, bg?BG:FG, cmdline);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
// 在线程终止前需要打印些相关信息,所以addjob完还要阻塞一会儿SIGCHLD
sigprocmask(SIG_BLOCK, &mask_chld, NULL);
if (!bg) {
waitfg(pid);
} else {
// 同上,操作全局变量时阻塞
sigprocmask(SIG_SETMASK, &mask_all, NULL);
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
// 操作结束后解除阻塞
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
}
return;
3、do_bgfg
void do_bgfg(char **argv)
{
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 访问全局变量jobs,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
struct job_t *job;
int pid;
if(argv[1] == NULL){
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
else if(argv[1][0] == '%'){
int jid = atoi(argv[1] + 1);
job = getjobjid(jobs, jid);
if(job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
pid = job->pid;
}
else {
pid = atoi(argv[1]);
if(pid <= 0){
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
job = getjobpid(jobs, pid);
if(job == NULL){
printf("(%d): No such process\n", pid);
return;
}
}
if(!strcmp(argv[0], "bg")){
job->state = BG;
printf("[%d] (%d) %s", job->jid, pid, job->cmdline);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
return;
}
else if(!strcmp(argv[0], "fg")){
job->state = FG;
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
waitfg(pid); // 子进程切换到了前台,故要等待它执行完
return;
}
else if(!strcmp(argv[0], "kill")){
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid,SIGQUIT); // 对子进程及其后代发送,故加负号
return;
}
return;
}
4、waitfg
void waitfg(pid_t pid)
{
sigset_t m;
sigemptyset(&m);
while(pid==fgpid(jobs))
sigsuspend(&m);//有信号时被唤醒检查前台进程pid是否变化,变化则说明前台进程结束。
return;
}
5、sigchld_handler
void sigchld_handler(int sig)
{
pid_t pid;
int status;
while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)
{
if(WIFEXITED(status))//正常结束
{
deletejob(jobs,pid);
}
if(WIFSTOPPED(status))//任务挂起时
{
struct job_t *job=getjobpid(jobs,pid);
int jid=pid2jid(pid);
printf("Job [%d] (%d) stopped by signal %d\n",jid,pid,WSTOPSIG(status));
job->state=ST;
}
if(WIFSIGNALED(status))//任务被终止
{
int jid=pid2jid(pid);
printf("Job [%d] (%d) terminated by signal %d\n",jid,pid,WTERMSIG(status));
deletejob(jobs,pid);
}
}
return;
}
6、sigint_handler
void sigint_handler(int sig)
{
pid_t pid=fgpid(jobs);
if(pid!=0)
{
kill(-pid,sig);
}
return;
}
7、sigtstp_handler
void sigtstp_handler(int sig)
{
pid_t pid=fgpid(jobs);
if(pid>0)
{
kill(-pid,sig);
}
return;
}