不得不说,CMU的15213课程比SEU的计组和操作系统课强太多了(不过SEU的课也给我打下了一些基础,还是有用的)。布置的所有实验都有详细友好的指导手册,会提供程序的框架,不需要从零构建程序,让学生更专注于课程所学内容。同时还有完善的测试用例,学生在实验过程中就能知道自己写的程序是否正确。学习曲线很缓和,对学生很友好!
进程(process)是一个运行着的应用程序(program)实例。
进程提供了两个关键的抽象:
上下文 = 地址空间 + 寄存器
这个函数的作用是解析tsh接受的命令行。需要注意的是,在fork出子进程之前,需要先block掉SIG_CHLD信号。这是因为子进程结束的时候,内核会给父进程发送SIG_CHLD,进而父进程执行sigchld_handler,而sigchld_handler会执行deletejob操作,如果子进程先于父进程执行addjob之前结束,就会出现错误。addjob完成之后就可以unblock了。
父进程block掉SIG_CHLD信号,子进程会继承这一结果。如果子进程创建它自己的子进程,当子子进程结束的时候,子进程将收不到SIG_CHLD,所以在execve之前,需要进行unblock操作。
从标准 Unix shell 运行 tsh时,tsh 正在前台进程组中运行。 如果tsh创建一个子进程,默认情况下该子进程也将是前台进程组的成员。由于键入 ctrl-c 会向前台组中的每个进程发送一个 SIGINT,因此键入 ctrl-c 将向tsh以及tsh创建的每个进程发送一个 SIGINT,这显然是不正确的。所以需要为子进程设置新的进程组号。在fork和execve之间执行setpgid(0, 0);即可。
void eval(char *cmdline) { char *argv[MAXARGS]; int bg; pid_t pid; sigset_t mask_one, mask_all, prev_mask; sigfillset(&mask_all); sigemptyset(&mask_one); sigaddset(&mask_one, SIGCHLD); bg = parseline(cmdline, argv); // bg=1时有两种情况,一是argv[0]为NULL,二是有& if (!argv[0]) { return; } if (!builtin_cmd(argv)) { sigprocmask(SIG_BLOCK, &mask_one, &prev_mask); // block sigchld if ((pid = fork()) == 0) // child process { setpgid(0, 0); //将当前进程的组号设置成其进程号,避免使用与tsh相同的gpid sigprocmask(SIG_SETMASK, &prev_mask, NULL); //子进程unblock sigchld,因为子进程需要处理自己的子进程 if (execve(argv[0], argv, environ) < 0) //注意:exec后子进程会清空所有自定义的sighandler { printf("%s: Command not found.\n", argv[0]); exit(0); //结束子进程 } } //父进程等待子进程返回 sigprocmask(SIG_BLOCK, &mask_all, NULL); addjob(jobs, pid, bg ? BG : FG, cmdline); sigprocmask(SIG_SETMASK, &prev_mask, NULL); if (!bg) { waitfg(pid); //一定要解除对SIGCHLD的block之后再waitfg,不然会死锁 } else { printf("[%d] (%d) %s", jobs->jid, pid, cmdline); } } else // buildin commands { switch (argv[0][0]) { case 'q': exit(0); break; case 'f': case 'b': do_bgfg(argv); break; case 'j': listjobs(jobs); break; default: break; } } return; }
int builtin_cmd(char **argv) { if (strcmp("jobs", argv[0]) == 0 || strcmp("bg", argv[0]) == 0 || strcmp("fg", argv[0]) == 0 || strcmp("quit", argv[0]) == 0) return 1; /* not a builtin command */ else return 0; }
需要给指定进程组的所有进程都发送SIGCONT,并注意waitfg的使用即可。
void do_bgfg(char **argv) { struct job_t *job; if (!argv[1]) { printf("bg command requires PID or %%jobid argument\n"); return; } if (argv[1][0] == '%') { int jid = atoi(argv[1] + 1); if (!jid) { printf("bg: argument must be a PID or %%jobid\n"); return; } job = getjobjid(jobs, jid); if (!job) { printf("%%%d: No such job\n", jid); return; } } else { pid_t pid = atoi(argv[1]); if (!pid) { printf("bg: argument must be a PID or %%jobid\n"); return; } job = getjobpid(jobs, pid); if (!job) { printf("(%d): No such process\n", pid); return; } } kill(-job->pid, SIGCONT); //这里需要给组内所有进程都发送信号 if (argv[0][0] == 'f') // fg { job->state = FG; waitfg(job->pid); } else { job->state = BG; printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline); } return; }
sleep函数在进程收到信号时会立即返回,所以这里每次sleep一秒也没关系。
void waitfg(pid_t pid) { while (fgpid(jobs) == pid) { sleep(1); } return; }
子进程结束(收到SIGINT或程序流程结束)后,内核会立即给父进程发送SIGCHLD。 而且,子进程stops(收到来自shell转发的SIGTSTP或其他进程发送的SIGTSTP)后,内核也会立即发送SIGCHLD给shell。
当options参数为0时,waitpid会等待指定的进程结束,如果指定的进程是一个zombie,则立即返回,否则阻塞。在options里添加WNOHANG,则waitpid不会阻塞,如果指定进程未结束(不是zombie),则返回错误-1。WUNTRACED选项让waitpid同时等待子进程进入STOP状态。通过宏WIFSTOPPED和WIFSIGNALED解析状态status来判断返回的进程是处于终止状态还是暂停状态。由于SIG_CHLD信号是没有队列的,因此进入sigchld_handler时,后续收到的SIG_CHLD会被丢弃,所以需要在while来reap或者处理暂停的进程。
这个handler统一对joblist处理,另外两个handler只需要转发tsh的信号就可以了。这样可以规避子进程无法继承父进程的handler所带来的问题。
void sigchld_handler(int sig) { pid_t pid; int status; while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) { /* returns true if the child process was stopped by delivery of a signal; this is possible only if the call was done using WUNTRACED or when the child is being traced (see ptrace(2)). */ pid_t pid = fgpid(jobs); struct job_t *job = getjobpid(jobs, pid); if (WIFSTOPPED(status)) { job->state = ST; printf("Job [%d] (%d) stopped by signal 20\n", job->jid, job->pid); } else { if (WIFSIGNALED(status)) { printf("Job [%d] (%d) terminated by signal 2\n", job->jid, job->pid); } deletejob(jobs, pid); } } return; }
将tsh收到的来自ctrl+c的SIG_INT转发给前台进程的子进程组
void sigint_handler(int sig) { pid_t pid = fgpid(jobs); struct job_t *job = getjobpid(jobs, pid); if (job) { // kill(-pid,sig)会给pid组的所有进程发送信号 kill(-job->pid, sig); } return; }
将tsh收到的来自ctrl+z的SIG_STP转发给前台进程的子进程组
void sigtstp_handler(int sig) { pid_t pid = fgpid(jobs); struct job_t *job = getjobpid(jobs, pid); if (job) { kill(-job->pid, sig); } return; }