csapp-shelllab
这个lab现成的东西比较多,要写的其实比较少,而且相比于其他几个lab,shelllab的文档里的hint明显写的更加详细,所以很多问题和写法直接看文档就可以解决了。
比较重要的是理解信号的阻塞机制,各种信号相关的函数的使用,剩下的就是照着文档和tshref.out里给出的参考来写相应的输出格式。
信号阻塞方式
信号阻塞几乎出现在每个函数内,所以放在最前面讲。
这里会用到的命令包括:
sigfillset(&set) 将set填充为所有信号的集合
sigemptyset(&set) 将set集合清空
sigaddset(&set, sig) 将sig信号加入set集合中
sigprocmask(how, &set, &prev) 根据how参数,设置信号阻塞情况,prev记录的是调用这个函数前被阻塞的信号集合
how参数有以下三种:
SIG_BLOCK: 将set集合中的信号设置为阻塞
SIG_UNBLOCK: 将set集合中和信号设置为非阻塞
SIG_SETMASK: 将被阻塞的信号集合设置为set(即set之外的信号将不被阻塞)
所以以下代码可以实现阻塞所有信号,执行任务,并在执行结束后将阻塞状态还原
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
...(执行任务)
sigprocmask(SIG_SETMASK, &prev, NULL);
信号处理
首先是要给出INT,TSTP和CHLD三种信号的响应函数sigint_handler(),sigtstip_handler()和sigchld_handler()
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
其中SIGINT和SIGTSTP的响应比较简单,直接用kill发送信号给当前前端进程。
/*
* sigint_handler - The kernel sends a SIGINT to the shell whenver the
* user types ctrl-c at the keyboard. Catch it and send it along
* to the foreground job.
*/
void sigint_handler(int sig)
{
int olderrno = errno;
pid_t pid = fgpid(jobs);
if (pid != 0)
kill(-pid, sig);
errno = olderrno;
return;
}
/*
* sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
* the user types ctrl-z at the keyboard. Catch it and suspend the
* foreground job by sending it a SIGTSTP.
*/
void sigtstp_handler(int sig)
{
int olderrno = errno;
pid_t pid = fgpid(jobs);
if (pid != 0)
kill(-pid, sig);
errno = olderrno;
return;
}
kill用法为
kill(pid_t pid, int sig)
第一个参数pid大于零时,信号会发送到pid对应的进程,而pid小于零时,信号会发送到-pid对应的进程组。
至于为什么要发送到进程组呢,文档里是这样说的,大概是为了把信号发送到整个前端进程组,改成正数的pid似乎也正确,所以具体为什么我也太清楚:
SIGCHLD相对来说麻烦一点,要分几种情况
void sigchld_handler(int sig)
{
int olderrno = errno;
pid_t pid;
int status;
sigset_t mask_all, prev;
sigfillset(&mask_all);
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
{
if (WIFEXITED(status))
{
sigprocmask(SIG_BLOCK, &mask_all, &prev);
deletejob(jobs, pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
}
else if (WIFSIGNALED(status))
{
struct job_t* job = getjobpid(jobs, pid);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
printf("Job [%d] (%d) terminated by signal %d\n", job->jid, job->pid, WTERMSIG(status));
deletejob(jobs, pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
}
else
{
struct job_t* job = getjobpid(jobs, pid);
sigprocmask(SIG_BLOCK, &mask_all, &prev);
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, job->pid, WSTOPSIG(status));
job->state= ST;
sigprocmask(SIG_SETMASK, &prev, NULL);
}
}
errno = olderrno;
return;
}
其中利用waitpid(-1, &status, WNOHANG | WUNTRACED)来使waitpid立刻返回,并将捕获到的信息放在status里进一步判断信号退出的原因。
原因分成三种,子进程正常退出(此时为僵尸进程),子进程被终止,和子进程被停止,捕获到信号后做出响应。这个文档里也有提示:
命令解析
也就是eval()函数,首先调用parseline()解析参数。
接着这里命令可以分成两种,一种是内置命令,包括quit,jobs,bg和fg,对于内置命令会直接做出响应,分别是终止进程,列出所有进程和状态,以及进程的前后端切换。
另一种是非内置命令,此时会fork出一个子进程,然后让子进程执行非内置命令。
需要注意的是在fork前要先阻塞SIGCHLD信号,原因是为了避免子进程在被加入到进程集合(addjob函数)之前就已经结束了:
并且在添加或删除任务前要阻塞所有信号,防止addjob和deletejob竞争。
fork后根据文档要求要用setpgid(0, 0)来设置PID,这是为了让子进程和父进程不处于同一个进程组,让子进程不受父进程接受的信号影响:
前后端切换就直接用kill发送信号,设置状态,并按照格式给出输出即可。
void do_bgfg(char **argv)
{
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid arugment\n", argv[0]);
return;
}
struct job_t *job;
int id;
if (sscanf(argv[1], "%%%d", &id) > 0) {
job = getjobjid(jobs, id);
if (job == NULL) {
printf("%%%d: No such job\n", id);
return;
}
} else if (sscanf(argv[1], "%d", &id) > 0) {
job = getjobpid(jobs, id);
if (job == NULL) {
printf("(%d): No such process\n", id);
return;
}
} else {
printf("%s: argument must e a PID or %%jobid\n", argv[0]);
return;
}
if (!strcmp(argv[0], "bg")) {
kill(-(job->pid), SIGCONT);
job->state = BG;
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else {
kill(-(job->pid), SIGCONT);
job->state = FG;
waitfg(job->pid);
}
return;
}
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "quit"))
exit(0);
else if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
} else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
} else if (!strcmp(argv[0], "&")) {
return 1;
}
return 0; /* not a builtin command */
}
void eval(char *cmdline)
{
char *argv[MAXARGS];
char buf[MAXLINE];
int bg;
int state;
pid_t pid;
sigset_t mask_all, mask_one, prev;
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL)
return ;
if (!builtin_cmd(argv)) {
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask_one, &prev);
if ((pid = fork()) == 0) {
sigprocmask(SIG_SETMASK, &prev, NULL);
if (setpgid(0, 0) < 0) {
perror("SETPGID ERROR");
exit(0);
}
if (execve(argv[0], argv, environ) < 0) {
printf("%s : Command not found\n", argv[0]);
exit(0);
}
} else {
state = bg ? BG : FG;
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, state, cmdline);
sigprocmask(SIG_SETMASK, &prev, NULL);
}
if (!bg)
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
return;
}
这里还需要一个waitfg函数等待前端进程停止
void waitfg(pid_t pid)
{
sigset_t mask_temp;
sigemptyset(&mask_temp);
while (fgpid(jobs) > 0)
sigsuspend(&mask_temp);
return;
}
利用了sigsuspend来等待任意信号(因为前端任务停止时肯定会有信号产生),当然也可以用sleep(1)代替。
在等待信号时文档建议在waitfg中采用忙等,在sigchld_handler中使用waitpid等到信号:
但是好像也没有解释原因,只是说这样子不容易出错。
个人觉得是因为可能受到信号和当前前端进程改变这两件事可能存在时间间隔,即收到了SIG_CHLD信号时,还没有把进程状态从FG切换到ST,这使得fgpid(jobs)>0的判断仍然成立,必须等待下一个信号才能结束使waitfg返回。