壳实验,对应于csapp第8章,异常控制流;根据其提示可知,该实验要求编写一个完整的简单的壳;
在完成之后,有许多的检测关卡等待壳的成果;
实际上,此实验已经将大部分内容编写完毕,只要求完成7个函数的构造来完整壳即可;
这7个函数都是有关于信号,以及异常处理的;
现在来先说说这7个函数的大致功能以及目的:
eval:解析和解释命令行的主例程;
builtin_cmd:识别并解释内置命令;
do_bgfg:执行bg和fg指令;
waitfg:等待前台作业完成;
sigchld_handler:SIGCHLD(子程序退出)信号处理;
sigint_handler:SIGINT(中断)信号处理;
sigtstp_handler:SIGTSTP(暂停)信号处理;
辅助的已有函数:
parseline:解析命令行构建argv列表;
clearjob:清除job结构体中的内容;
initjobs:初始化job列表;
maxjid:返回允许的最大job ID;
addjob:添加一个作业到job列表;
deletejob:从job列表中删除pid的作业;
fgpid:返回前台job的pid;
getjobpid:根据pid从job列表中找到作业;
getjobjid:根据job ID从job列表中找到作业;
pid2jid:根据pid返回对应jid;
listjobs:显示job列表;
之后有经典的实验约束规则:
提示符为:tsh>
用户键入的命令行应包含一个名称和零个或多个参数,所有参数均由一个或多个空格分隔。 如果名称是内置命令,则shell应该立即处理它并等待下一个命令行;否则,shell应该假定名称是可执行文件的路径,它在初始子进程的上下文中加载并运行;
shell不用支持管道或I/O重定向;
输入 ctrl-c 导致 SIGINT (输入 ctrl-z 导致 SIGTSTP)发送到当前前台作业以及该作业的任何后代,如果没有前台作业,那么信号没有效果;
如果命令行以 & 结束,则shell应该在后台运行作业,否则它将在前台运行该作业;
每个作业都可以通过进程ID(PID)或作业ID(JID)进行标识,该ID是tsh分配的正整数;
shell支持内置命令;
shell应该回收所有僵死子进程,如果任何作业由于接收到未捕获到的信号而终止,则shell应该识别此事件并打印一条消息,其中包含该作业的PID和有问题的信号的描述;
先来看Shell的主函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 int main (int argc, char **argv) { char c; char cmdline[MAXLINE]; int emit_prompt = 1 ; 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(); } } Signal(SIGINT, sigint_handler); Signal(SIGTSTP, sigtstp_handler); Signal(SIGCHLD, sigchld_handler); Signal(SIGQUIT, sigquit_handler); initjobs(jobs); while (1 ) { if (emit_prompt) { printf ("%s" , prompt); fflush(stdout ); } if ((fgets(cmdline, MAXLINE, stdin ) == NULL ) && ferror(stdin )) app_error("fgets error" ); if (feof(stdin )) { fflush(stdout ); exit (0 ); } eval(cmdline); fflush(stdout ); fflush(stdout ); } exit (0 ); }
第一个while是在选择模式;输入h参数显示提示,输入v参数发出附加诊断信息,输入p不显示命令行;
之后需要捕获信号,就需要通过Signal函数将信号和对应处理函数绑定,然后进入第二个while使用eval一条一条地重复解析输入的内容;
I . Eval 第一解析命令行,可以套用parseline函数来帮忙,并根据结尾符号是否为 & 来判断前后台关系;
第二要做到的,查看解析出的 argv[0] 是否为内置命令,是,则转交给builtin_cmd函数,不是则创建子进程来运行;之后在shell中通过 addjob 来添加作业,如果是前台作业,就等待前台作业运行完毕,如果是后台作业,就执行解析下一条命令;
由此:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 void eval (char *cmdline) { static char array [MAXLINE]; char *buf = array ; char *argv[MAXARGS]; int bg; pid_t pid; sigset_t mask, prev, all; strcpy (buf,cmdline); bg = parseline(buf,argv); if (argv[0 ]==NULL ) return ; if (!builtin_cmd(argv)) { sigemptyset(&mask); sigaddset(&mask,SIGCHLD); sigfillset(&all); sigprocmask(SIG_BLOCK,&mask,&prev) if ((pid = fork()) == 0 ) { fflush(sdout); setpgid(0 ,0 ); sigprocmask(SIG_SETMASK,&prev,NULL ) if (execve(argv[0 ],agrv,environ) < 0 ) { printf ("%s: Command not found\n" , argv[0 ]); exit (0 ); } } sigprocmask(SIG_BLOCK,&all,NULL ) addjob(jobs,pid,bg?BG:FG,buf); sigprocmask(SIG_SETMASK,&prev,NULL ) if (bg) printf ("[%d] (%d) %s" , pid2jid(pid), pid, buf); else waitfg(pid); } }
需要注意的有两点:
阻塞SIGCHLD信号以防止addjob与deletejob竞争;
访问全局数据jobs列表前阻塞所有信号;
为什么要在fork创建进程之前阻塞SIGCHLD呢?因为fork的进程可能在任意时刻暂停或终止;使得Shell跳转通过对应信号去处理程序,并在信号处理中对该作业进行修改;如果在 addjob 之前跳转,则会由于未保存该作业而导致错误,所以需要在fork之前阻塞,并在子进程中取消阻塞;
设置独立的进程组与Shell分开,以免Shell接收到的其他例如 ctrl-c 之类的信号而导致进程受到影响;
其次,从安全信号处理的角度,在修改读取jobs时如果不阻塞所有信号,则会有可能中断而导致jobs的各部分状态不同;
II . builtin_cmd 第一,前面可以知道用这个函数套用在eval里,使得分辨是否为内置命令,所以让内置命令返回1,而非内置命令返回0;
第二,已知这个函数会用于bg和fg的内置命令,所以可以套用do_bgfg的函数;
那么代码就可知了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int builtin_cmd (char **argv) { if (!strcmp (argv[0 ],"quit" )) exit (0 ); if (!strcmp (argv[0 ],"jobs" )) { listjobs(jobs); return 1 ; } if (!strcmp (argv[0 ],"&" )) return 1 ; if ((!strcmp (argv[0 ],"bg" )) || (!strcmp (argv[0 ],"fg" ))) { do_bgfg(argv); return 1 ; } return 0 ; }
III . do_bgfg 根据之前提到的,需要用用到SIGCONT信号,那么也需要使用kill来发送给整个进程组;
在这之前需要修改job结构的状态;
而在修改前台或者后台的再之前,需要寻找到这个工作的ID或者pid;
由此代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 void do_bgfg (char **argv) { int jid; pid_t pid; struct job_t *job ; sigset_t mask,prev; if (argv[1 ] == NULL ) { printf ("%s command requires PID or %%jobid argument\n" ,argv[0 ]); return ; } if (sscanf (argv[1 ],"%%%d" ,&jid) > 0 ) { job = getjobjid(jobs,jid); if (job == NULL || job->state == UNDEF) { printf ("%s: No such job\n" , argv[1 ]); return ; } } else if (sscanf (argv[1 ],"%d" ,&pid) > 0 ) { job = getjobpid(jobs, pid); if (job == NULL || job->state == UNDEF) { printf ("(%s): No such process\n" , argv[1 ]); return ; } } else { printf ("%s: argument must be a PID or %%jobid\n" , argv[0 ]); return ; } sigfillset(&mask); sigprocmask(SIG_BLOCK,&mask,&prev); if (!strcmp (argv[0 ], "fg" )) job->state = FG; else job->state = BG; sigprocmask(SIG_SETMASK,&prev,NULL ); pid = job->pid; kill(-pid,SIGCONT); if (!strcmp (argv[0 ], "fg" )) waitfg(pid); else printf ("[%d] (%d) %s" , job->jid, pid, job->cmdline); }
IV . waitfg 等待前台作业完成就使用sleep挂起:
1 2 3 4 5 void waitfg (pid_t pid) { while (pid == fgpid(jobs)) sleep(1 ); }
V . sigint_handler 当使用 Ctrl+c ,内核发送中断信号给这个Shell程序,而Shell程序通过kill发送信号给子进程,而停止信号也同理;
1 2 3 4 5 6 7 8 void sigint_handler (int sig) { int old_errno = errno; pid_t pid = fgpid(jobs); if (pid!=0 ) kill(-pid,sig); errno = old_errno; }
保存之前的errno并在返回时重新赋值,是为了防止它被改变;
VI . sigstp_handler 1 2 3 4 5 6 7 8 void sigtstp_handler (int sig) { int old_errno = errno; pid_t pid = fgpid(jobs); if (pid!=0 ) kill(-pid,sig); errno = old_errno; }
VII . sigchld_handler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void sigchld_handler (int sig) { int old_errno = errno; pid_t pid; sigset_t mask, prev; int state; struct job_t *job ; sigfillset(&mask); while ((pid = waitpid(-1 , &state, WNOHANG | WUNTRACED)) > 0 ) { sigprocmask(SIG_BLOCK, &mask, &prev); if (WIFEXITED(state)) { deletejob(jobs, pid); } else if (WIFSIGNALED(state)) { printf ("Job [%d] (%d) terminated by signal %d\n" , pid2jid(pid), pid, WTERMSIG(state)); deletejob(jobs, pid); } else if (WIFSTOPPED(state)) { job = getjobpid(jobs, pid); job->state = ST; printf ("Job [%d] (%d) stopped by signal %d\n" , job->jid, pid, WSTOPSIG(state)); } sigprocmask(SIG_SETMASK, &prev, NULL ); } errno = old_errno; }
总结: 这个实验主要是考察了安全信号处理内容,以及竞争关系;
帮助疏通了对Shell的理解:接受指令,处理指令,以及增加进程和如何回收进程;
对于小方向的话便是细节的考虑,阻塞顺序以及分类情况;