shlab这节是要求写个支持任务(job)功能的简易shell,主要考察了linux信号机制的相关内容。难度上如果熟读了《CSAPP》的“异常控制流”一章,应该是可以不算困难的写出来。但如果读书不仔细,或者实践的时候忘记了部分细节,那就可能完全不知道怎么下手,或者得改bug改到吐了。我自己写了大概八个小时,其中仅一半的时间都在处理收到SIGTSTP后莫名卡死的问题,最后才发现是课本没看仔细,子进程停止后也会向父进程发送SIGCHLD。
在实验中我们需要实现job、fg、bg、kill四个内建命令和对执行本地程序的支持,并且还要处理好SIGCHLD、SIGINT、SIGTSTP这几个信号。关键要点都在课本的534页有说过了:
处理程序尽可能简单
处理程序中只用异步信号安全的函数
保存恢复errno
访问共享全局变量时阻塞所有信号
volatile声明全局变量
sig_atiomic_t声明标志
验收标准这一块因为是在实际操作系统上跑的,不能保证进程号相同,但要保证处理进程号意外所有指令的顺序和信息都要与参考程序的输出完全相同。这点可以用linux上的各种diff工具进行结果比较。
eval函数在课本P525页有一个缺陷版,我们要做的就是以此为蓝本加上点信号处理。
void eval(char* cmdline) { 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; }waitfg负责等待前台进程结束。每次都调用fgpid有点低效了,我们直接用一个全局标志fg_child_flag代表前台进程是否异常,默认为0,如果切换到停止或退出状态就置1。
void waitfg(pid_t pid) { sigset_t mask_empty; sigemptyset(&mask_empty); fg_child_flag = 0; while(!fg_child_flag){ // 参考课本545页,挂起进程直到任意信号到达 sigsuspend(&mask_empty); } return; }要注意子进程终止或停止都可能触发SIGCHLD,所以我们得分类讨论。
void sigchld_handler(int sig) { int olderrno=errno; sigset_t mask_all, prev_mask; pid_t pid; int status; sigfillset(&mask_all); // 这里一定要设置成WUNTRACED,否则在子进程处于停止状态时会卡死 if((pid=waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){ // 涉及到对全局变量jobs的访问,阻塞所有信号 sigprocmask(SIG_SETMASK, &mask_all, &prev_mask); struct job_t *job = getjobpid(jobs, pid); if(job->state == FG){ // 子进程为前台进程,打开标志 fg_child_flag=1; } if(WIFEXITED(status)){ // 正常退出,删除任务即可 deletejob(jobs, pid); } else if(WIFSIGNALED(status)){ // 收到信号非正常退出,打印消息后删除任务 printf("Job [%d] (%d) terminated by signal %d\n", job->jid, pid, WTERMSIG(status)); deletejob(jobs, pid); } else if(WIFSTOPPED(status)){ // 子进程处于停止状态,切换对应的任务状态 job->state = ST; printf("Job [%d] (%d) stopped by signal %d\n", job->jid ,pid, WSTOPSIG(status)); } sigprocmask(SIG_SETMASK, &prev_mask, NULL); } errno=olderrno; return; }因为进程在终止时会自动向父进程发送SIGCHLD信号,所以部分逻辑放在了sigchld_handler,这里只要对子进程发出SIGINT信号就行
void sigint_handler(int sig) { sigset_t mask_all, prev_mask; sigfillset(&mask_all); // 访问全局变量,阻塞所有信号 sigprocmask(SIG_SETMASK, &mask_all, &prev_mask); int pid = fgpid(jobs); sigprocmask(SIG_SETMASK, &prev_mask, NULL); if(pid > 0){ kill(-pid, SIGINT); // 对子进程及其后代发送,故加负号 } return; }设计思路同上
void sigtstp_handler(int sig) { sigset_t mask_all, prev_mask; sigfillset(&mask_all); // 访问全局变量,阻塞所有信号 sigprocmask(SIG_SETMASK, &mask_all, &prev_mask); int pid = fgpid(jobs); sigprocmask(SIG_SETMASK, &prev_mask, NULL); if(pid > 0){ kill(-pid, SIGTSTP); // 对子进程及其后代发送,故加负号 } return; }仍旧参考课本525页,挨个命令strcmp就行
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], "kill")){ do_bgfg(argv); return 1; } if(!strcmp(argv[0], "&")){ return 1; } return 0; /* not a builtin command */ }这个也没啥难度,对着参考输出慢慢地添加判断细节就行
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; }到这所有的实现都捋完一遍了。做这个实验的缘由是在看数据库网课,讲到缓存管理的时候老师说这一块儿知识和你们学操作系统文件系统管理的知识一个样,只不过我们为了效率得另写一套。然后我发现这块知识快忘光了,得补补操作系统,刚好CSAPP还剩下几个实验当初不屑做,干脆一块搞了吧。