- shlab
- 1. 框架代码分析
- 2. 实验难点
- 3. 实现综述
- 4. 总结
本次实验主要是运用课本第八章讲授的job control
在框架代码的基础上实现一个简单的shell
。正好最近上的OS课也讲了shell和job control,就简单地练练手。
本次实验的框架代码大多已经给出,要填空的部分为:
eval
:解析执行命令行builtin_cmd
:识别并解释执行内部命令,如:quit,fg,bg,jobsdo_bgfg
:在上述函数识别的基础上执行fg,bgsigchld_handler
:捕获处理SIGCHILD
信号sigint_handler
:捕获处理SIGINT
信号sigstp_handler
:捕获处理SIGSTP
信号
整体来看,一次shell处理的控制流如下:
main
函数不断获取输入,获取后传给eval
执行
eval
调用parse_line
解析命令行,并根据命令类型调用相应函数处理。
由于这次lab不要求实现重定向、管道等复合命令,所以命令行不用解析为树型结构。(复合命令shell的一个简单实现可以参考xv6)
框架代码使用一个全局的job
数组维护信息,并通过信号机制实际控制各job的状态
这次实验的难点主要在于信号处理,具体地有:
-
竞争问题
- 如果信号处理程序访问全局数据,那么需要避免信号处理程序和主程序之间、信号处理程序和信号处理程序之间发生数据竞争。
- 具体地,在每一次访问全局数据时显示的利用
sigprocmask
阻塞可能发生数据竞争的信号处理
sigprocmask(SIG_BLOCK, &mask_all, NULL); //暂时阻塞全部信号 addjob(jobs, pid, FG, buf); //全局数据 sigprocmask(SIG_SETMASK, &mask_all, NULL); //恢复
-
同步问题
-
创建子进程的正常步骤是
fork()
- 父进程更新
job
- 子进程
execve
这个流程能工作的一个前提是:父进程先更新job,子进程再execve。因为只有这样,子进程exit后父进程的信号处理程序才能在已经更新的job上删除这个进程信息。
但如果子进程先execve到exit,此时job中还没有子进程的信息,信号处理程序不做任何事就返回,之后切换到父进程,它更新了job...再也不可能被删除
- 父进程更新
-
为了解决这个同步问题,依然可以通过阻塞信号处理,我们在fork前显式阻塞child信号,父进程更新job后再解除即可同步
-
-
如何实现进程的前后台执行?
- 前台进程需要在shell中调用
waitpid
显式等待,后台程序则在创建后不等待,按原执行流进行 - 所有子进程结束后都在信号处理程序中回收
这其实也是官方手册的参考实现————将回收僵尸进程的工作集中到信号处理程序中进行
- 前台进程需要在shell中调用
-
信号转发
在实现Ctrl+C
和Ctrl+Z
时,我们需要了解终端和进程组的概念:
- 简单来讲,终端是一类特殊的虚拟设备。我们对着黑框输入实际上是将数据传送给了终端,应用程序(如shell)能通过
stdin
从终端读取输入的数据(默认)。 - 如上图所示,终端控制着一个session,当我们在终端按下
ctrl+c
时终端就会给session的前台进程组发送SIGINT信号。在这个实验中,我们的shell运行在bash中作为bash的前台进程,所以所有的SIGINT都会发送给shell,所以为了在我们的shell运行其它进程时通过ctrl+c只终止该进程,我们首先需要将shell fork()出的进程放在另外的进程组里,然后再每次把SIGINT等信号转发给shell管理的前台进程组(框架代码里的job提供了这样的机制)
-
eval
- 调用
parse_line
解析命令行,之后调用builtin_cmd
尝试解析内部命令,成功则返回等待下一次输入;失败后将命令行作为其它进程用execve
执行 - 如果是前台执行,则在fork+正确更新
jobs
后显式调用waitfg()
等待进程结束;如果是后台执行则在fork+更新后等待输入
- 调用
void eval(char *cmdline)
{
int olderrno = errno; //save errno
char *argv[MAXARGS] = {NULL};
char buf[MAXLINE];
pid_t pid = 0;
strcpy(buf, cmdline);
int bg = parseline(buf, argv);
if (argv[0] == NULL) {
return; //ignore empty line.
}
sigset_t mask, prev, mask_all;
sigfillset(&mask_all);
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
if (builtin_cmd(argv) == 0)
{
sigprocmask(SIG_BLOCK, &mask, &prev);
if ((pid = fork()) == 0) {
sigprocmask(SIG_SETMASK, &prev, NULL);
setpgid(0, 0); //create a new process group.
if (execve(argv[0], argv, environ) < 0)
{
//failed to execute, should exit the child process.
printf("command not found.\n");
exit(0);
}
}
if (!bg) {
//front process
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, FG, buf);
sigprocmask(SIG_SETMASK, &prev, NULL);
waitfg(pid);
} else {
//back process
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, BG, buf);
sigprocmask(SIG_SETMASK, &prev, NULL);
printf("[%d] (%d) %s", pid2jid(pid), pid, buf);
}
}
errno = olderrno;
return;
}
-
builtin_cmd/do_bgfg
- 内部命令主要借助
jobs
提供的接口实现,都比较简单 - 但需要注意鲁棒性,包括命令不合语法、jid/pid不存在等
- 内部命令主要借助
int builtin_cmd(char **argv)
{
if (strcmp(argv[0], "quit") == 0) {
exit(0);
} else if (strcmp(argv[0], "jobs") == 0) {
listjobs(jobs);
return 1;
} else if (strcmp(argv[0], "bg") == 0 || strcmp(argv[0], "fg") == 0) {
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
void do_bgfg(char **argv)
{
char *argstr = argv[1];
int is_jid = 0;
struct job_t *job = NULL;
pid_t pid;
int jid;
char *cmdline = NULL;
sigset_t mask_all, prev;
sigfillset(&mask_all);
if (argstr == NULL)
{
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (argv[1][0] == '%')
{
argstr = argv[1] + 1;
is_jid = 1;
}
else
{
argstr = argv[1];
}
for (char *itr = argstr; *itr; itr++)
{
if (!isdigit(*itr)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
}
if (is_jid) {
jid = atoi(argstr);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
job = getjobjid(jobs, jid);
if (job == NULL)
{
printf("%%%d: No Such job\n", jid);
sigprocmask(SIG_SETMASK, &prev, NULL);
return;
}
pid = job->pid;
cmdline = job->cmdline;
sigprocmask(SIG_SETMASK, &prev, NULL);
}
else
{
pid = atoi(argstr);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
job = getjobpid(jobs, pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
if (job == NULL) {
printf("(%d): No Such process\n", pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
return;
}
jid = job->jid;
cmdline = job->cmdline;
sigprocmask(SIG_SETMASK, &prev, NULL);
}
if (strcmp(argv[0], "bg") == 0)
{
printf("[%d] (%d) %s", jid, pid, cmdline);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
job->state = BG;
sigprocmask(SIG_SETMASK, &prev, NULL);
kill(-(pid), SIGCONT);
}
else if (strcmp(argv[0], "fg") == 0)
{
printf("%s", cmdline);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
job->state = FG;
sigprocmask(SIG_SETMASK, &prev, NULL);
kill(-pid, SIGCONT);
waitfg(pid);
}
return;
}
waitfg
void waitfg(pid_t pid)
{
sigset_t mask, prev;
sigfillset(&mask);
sigprocmask(SIG_BLOCK, &mask, &prev);
struct job_t *job = getjobpid(jobs, pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
while (job != NULL && job->state == FG)
{
sigfillset(&mask);
sigprocmask(SIG_BLOCK, &mask, &prev);
job = getjobpid(jobs, pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
}
return;
}
-
sigchld_handler
-
接收到SIGCHILD信号后说明一定至少有一个子进程终止或暂停,可以通过
waitpid(-1, &status, WNOHANG|WUNTRACED)
获取这些进程号,然后正确更新jobs
(i.e.回收僵尸进程) -
值得注意的是,这里的信号处理可能被其它信号处理程序中断,因此只能使用信号安全函数, 此外因为要访问全局数据
jobs
,需要在访问时阻塞同样访问该数据的其它信号(相当于上锁) -
同时,为了避免连续发生多个SIGINT信号时,由于阻塞的表现形式导致丢失(阻塞用一个二进制位实现,因此同一个信号连续超过2个就会被丢弃),最好如下:
while (waitpid(-1, &status, WNOHANG|WUNTRACED) > 0) { .... }
- 但实验讲义中说只需调用一次waitpid... 我暂时还没想通只调用一次如何避免上述问题。由于测试用例比较弱,两种写法没出现问题
-
void sigchld_handler(int sig)
{
pid_t pid;
int status;
sigset_t mask_all, prev;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
{
if (WIFSTOPPED(status)) {
getjobpid(jobs, pid)->state = ST;
} else {
deletejob(jobs, pid);
}
}
sigprocmask(SIG_SETMASK, &prev, NULL);
}
-
sigint_handler|sigstp_handler
- 主要就是完成第2节中所说的信号转发和全局数据更新.注意这里如果要向控制台打印信息,不能使用
printf
。所以我的实现相当冗长...
- 主要就是完成第2节中所说的信号转发和全局数据更新.注意这里如果要向控制台打印信息,不能使用
void sigint_handler(int sig)
{
pid_t fpid = fgpid(jobs);
if (fpid == 0) {
return;
}
char buf[MAXLINE] = {'\0'};
strcpy(buf, "Job [");
strcatNum(buf, pid2jid(fpid));
strcat(buf, "] (");
strcatNum(buf, fpid);
strcat(buf, ") terminated by signal ");
strcatNum(buf, sig);
strcat(buf, "\n");
if (write(STDOUT_FILENO, buf, strlen(buf)) < 0) {
exit(0);
}
kill(-fpid, sig);
return;
}
void sigtstp_handler(int sig)
{
pid_t fpid = fgpid(jobs);
if (fpid == 0) {
return;
}
char buf[MAXLINE] = {'\0'};
strcpy(buf, "Job [");
strcatNum(buf, pid2jid(fpid));
strcat(buf, "] (");
strcatNum(buf, fpid);
strcat(buf, ") stopped by signal ");
strcatNum(buf, sig);
strcat(buf, "\n");
if (write(STDOUT_FILENO, buf, strlen(buf)) < 0) {
exit(0);
}
kill(-fpid, sig);
return;
}
4. 总结
-
本次实验的绝大部分内容都在课本和实验讲义中有所涉及,因此实验过程中多次有重温教材的感觉。这是一个比较合适的难度梯度!
-
虽然在大一寒假的时候读过csapp,但由于基础不牢当时没能坚持下来,效果也不太理想。现在有了计算机系统、操作系统方面的基础,或许csapp已经不再是一本很难的书,但它仍然是一本值得反复阅读的参考书。最近我也会结合操作系统课、网络课回味一遍这本神书...(if time)