不得不说,CMU的15213课程比SEU的计组和操作系统课强太多了(不过SEU的课也给我打下了一些基础,还是有用的)。布置的所有实验都有详细友好的指导手册,会提供程序的框架,不需要从零构建程序,让学生更专注于课程所学内容。同时还有完善的测试用例,学生在实验过程中就能知道自己写的程序是否正确。学习曲线很缓和,对学生很友好!
知识整理
进程(process)是一个运行着的应用程序(program)实例。
进程提供了两个关键的抽象:
- 每个进程仿佛独占着CPU(上下文切换)
- 每个进程仿佛独占着内存(虚拟内存)
上下文 = 地址空间 + 寄存器
代码
eval函数
这个函数的作用是解析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;
}
buildin_cmd
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;
}
do_bgfg
需要给指定进程组的所有进程都发送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;
}
waitfg
sleep函数在进程收到信号时会立即返回,所以这里每次sleep一秒也没关系。
void waitfg(pid_t pid)
{
while (fgpid(jobs) == pid)
{
sleep(1);
}
return;
}
sigchld_handler
子进程结束(收到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;
}
sigint_handler
将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;
}
sigstp_handler
将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;
}
原创文章,作者:bd101bd101,如若转载,请注明出处:https://blog.ytso.com/244986.html