《UNIX环境高级编程》实验五 信号处理,本科生跟研究生做的实验是不同的。本科生做的是带时间限制的 myshell,研究生做的是实现与 UNIX 的 sleep 函数一样的 mysleep。
信号这部分的实验,特别是本科生的实验,之所以难,是因为大多连书上的内容都没去看明白,又怎么可能做得出来,给你代码看你都看不出个所以然。所以,先老老实实把书上相关的内容看明白再说。研究生的实验倒真的是难,要考虑的情况比较多。这两个实验的代码我都放上来了,mysleep 的代码在文章后头。
实验描述
信号处理,学习和掌握信号的处理方法,特别是 sigaction,alarm,sigpending,sigsetjmp 和 siglongjmp 等函数的使用。
要求:
1、编制具有简单执行时间限制功能的shell:
myshell [ -t
这个测试程序的功能类似实验1,但是具有系统shell (在cs8服务器上是bash)的全部功能。<time>是测试程序允许用户命令执行的时间限制,默认值为无限制。当用户命令的执行时间限制到达时,测试程序终止用户命令的执行,转而接收下一个用户命令。
2、myshell只在前台运行。
3、按Ctrl-\键不是中断myshell程序的运行,而是中断当前用户命令的接收或终止当前用户命令的执行,转而接收下一个用户命令。
4、注意信号SIGALRM和SIGQUIT之间嵌套关系的处理。
实验思路
实验可在代码 1-8 的基础上进行修改,代码 1-8 就是能处理信号(ctrl+c)的简单 shell。
《UNIX环境高级编程实验指导.doc》里将思路介绍得差不多了,这个实验只要考虑 SIGALRM 和 SIGQUIT,所以核心要点就是:无论上述两个信号哪个先处理,另一个未决信号就应该忽略(清除未决信号);在处理其中一个信号时,屏蔽另一个信号(如果发生,就是未决信号)。
谈到屏蔽信号,可以通过 sigprocmask (程序10-11) 实现,也可以通过 sigaction (程序10-12) 实现,在这里是推荐使用 sigaction,在绑定信号处理函数的同时,可以进行信号的屏蔽。
前面说到,在处理其中一个信号时,要屏蔽另一个信号,在程序 10-12 的基础上修改下,可以方便的实现这一点:
/* Reliable version of signal(), using POSIX sigaction(). */
Sigfunc *
signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
/* 在处理其中一个信号时,屏蔽另一个信号 */
if (signo == SIGALRM) {
sigaddset(&act.sa_mask, SIGQUIT);
} else if (signo == SIGQUIT) {
sigaddset(&act.sa_mask, SIGALRM);
}
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
对于这两个信号,在信号处理函数之后,要返回继续接收用户的命令输入,这点可以通过非局部转移机制来实现信号处理完成之后的跳转,可参考书中解决低速输入的那个代码段。
非局部转移机制其实就类似于 Goto 语句,很好理解,比如下面这一个简单的程序:
#include "apue.h"
#include <setjmp.h>
int main() {
static sigjmp_buf jmpbuf;
sigsetjmp(jmpbuf, 1); /* 设置跳转点 */
char str[100];
printf("input:\n");
gets(str);
siglongjmp(jmpbuf, 1); /* 进行跳转 */
}
此程序运行后,会不断执行 printf 和 gets。
至于对信号的具体处理,主要就包括三部分:
- 杀死正在执行的子进程(如果有)
- 对屏蔽的信号进行处理,即处理未决信号
- 进行跳转
当然,还得注意一些细节的处理,如对 SIGQUIT 信号的处理中,应有 alarm(0)
,取消之前所设置的闹钟。更多的细节就不详说了,还是那句话,先把书上内容看明白。
另外,网上的代码有的是错的:在等待用户输入时就开始计时,也就是若用户在时间限制内没有输入/执行命令,也会提示超时,这点是不必要的,不符合实验要求。实验要求的是“用户命令执行的时间限制”,是命令的执行时间,并不算上命令的输入时间。
至于长时间的执行命令,可以通过命令 sleep <time> 来模拟。
实验讲解PPT
完整代码
可执行 ./myshell -t5
,然后简单的测试如下情况验证程序的正确性:
- 输入 ls等命令能正确执行;
- 执行 sleep 10,大约5秒后能提示 timeout 并退出 sleep;
- 再次执行 sleep 10,然后按 ctrl + \ ,能提示 quit 并退出 sleep,且
大约5秒后不会提示 timeout; - 在等待输入命令时,按 ctrl + \,能提示 quit,但不退出 myshell;
- 打乱上述顺序,能正确执行。
#include "apue.h"
#include <setjmp.h>
#include <sys/wait.h>
static volatile pid_t pid; /* 存放子进程ID,非0表示正在执行用户命令 */
static sigjmp_buf jmpbuf;
static void sig_alrm(int); /* our signal-catching function */
static void sig_quit(int); /* our signal-catching function */
Sigfunc *signal(int, Sigfunc *); /* Reliable version of signal() */
int
main(int argc, char *argv[])
{
char buf[MAXLINE]; /* from apue.h */
int status;
int time = 0;
/* 处理输入 */
if (argc != 1 && argc != 3 )
err_quit("usage: myshell [-t <time>] ");
if (argc == 3 && strcmp(argv[1], "-t") != 0) {
err_quit("usage: myshell [-t <time>] ");
} else {
time = atoi(argv[2]);
}
/* 注册信号 */
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
err_sys("signal error");
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
err_sys("signal error");
sigsetjmp(jmpbuf, 1); /* 设置跳转点 */
printf("%% "); /* print prompt (printf requires %% to print %) */
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if (time) alarm(time); /* 在用户命令执行前设置闹钟 */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
execlp("/bin/sh", "sh", "-c", buf, (char *)0); /* 使用系统shell */
err_ret("couldn't execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
if (time) alarm(0); /* 在用户命令执行完成后应清理闹钟 */
printf("%% ");
}
exit(0);
}
/* Reliable version of signal(), using POSIX sigaction(). */
Sigfunc *
signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
/* 在处理其中一个信号时,屏蔽另一个信号 */
if (signo == SIGALRM) {
sigaddset(&act.sa_mask, SIGQUIT);
} else if (signo == SIGQUIT) {
sigaddset(&act.sa_mask, SIGALRM);
}
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
void
sig_alrm(int signo)
{
if (pid > 0) {
kill(pid, SIGKILL);
pid = 0;
}
printf("\n *** TIMEOUT ***\n");
sigset_t pendmask;
if (sigemptyset(&pendmask) < 0)
err_sys("sigemptyset error!");
if (sigpending(&pendmask) < 0)
err_sys("sigpending error!");
if (sigismember(&pendmask, SIGQUIT)) { /* 存在未决信号 */
signal(SIGQUIT, SIG_IGN); /* 清除未决信号 */
signal(SIGQUIT, sig_quit); /* 恢复之前的处理方式 */
}
siglongjmp(jmpbuf, 1);
}
void
sig_quit(int signo)
{
if (pid > 0) {
kill(pid, SIGKILL);
pid = 0;
}
printf("\n*** QUIT *** \n");
alarm(0); /* 清除闹钟 */
sigset_t pendmask;
if (sigemptyset(&pendmask) < 0)
err_sys("sigemptyset error!");
if (sigpending(&pendmask) < 0)
err_sys("sigpending error!");
if (sigismember(&pendmask, SIGALRM)) { /* 存在未决信号 */
signal(SIGALRM, SIG_IGN); /* 清除未决信号 */
signal(SIGALRM, sig_alrm); /* 恢复之前的处理方式 */
}
siglongjmp(jmpbuf, 1);
}
研究生的实验:实现 mysleep 函数
函数名字和原型:
unsigned int mysleep(unsigned int);
该函数的功能要求与UNIX的sleep函数一样。
要求:
1、使用alarm函数实现定时。
2、必须正确处理mysleep函数中的闹钟与调用者可能设置的闹钟之间的关系。例如,如何解决不同的信号处理函数的保存和恢复?如何处理调用者设置的闹钟比mysleep函数中的闹钟早响的问题?如何处理调用进程屏蔽SIGALRM信号?
3、不允许出现任何竟态条件(时间窗口)。
4、总之,mysleep的实现细节应当对调用者透明,也就是说,不论在实现mysleep函数时是否使用了alarm函数,对调用者是否以及如何使用alarm函数均不应有任何影响。
实验实现
具体实现以书本P281的程序10-21 sleep的可靠实现为基础,主要处理的是之前设置的闹钟的交互,实现不同的信号处理函数的保存和恢复。
如果调用程序设置了计时器,应检查之前调用alarm的返回值,如果小于本次调用alarm的秒数,则应等到调用程序设置的计时器超时触发;如果大于本次调用alarm的秒数,则应在mysleep函数返回前复位本次闹钟,使得调用程序设置的计时器能再次超时触发。
在具体实现中,主要考虑以下这几种情况的处理:
1, 调用程序中已设置了Alarm():
通过alarm(0)可以获得调用程序设置的计时器的未睡足时间。如果小于本次sleep时间,则应先等到调用程序设置的计时器超时。否则应该在完成sleep的功能后,复原计时器。
2,调用进程阻塞了SIGALRM:
通过sigprocmask(0, NULL, &oldmask)和sigismember(&oldmask, SIGALRM)来判断SIGALRM是否被阻塞了。如果被阻塞,且设置了计时器,则应该在mysleep返回前给调用进程发送SIGALRM信号(如果sleep时间大于计时器未睡足时间,通过kill()发送)或者复原计时器(如果sleep时间小于计时器未睡足时间)。
3,调用sleep时,有未决的SIGALRM信号:
如果未决的SIGALRM信号,则应在sleep前先处理,否则通过sigsuspend进入sleep时会立即返回(会接收到该未决信号)。可以在进入sleep前先使用sigpending判断是否有未决SIGALRM信号,然后使用sigsuspend立即捕获该信号,即处理掉了该未决信号。同样,在mysleep返回前也要复原该未决信号(通过kill()向调用进程发送)。
完整代码
老师给了测试用例 testsleep.c ,反正结果输出跟老师给的不同的话就是有问题。我的代码 mysleep.c:
#include "apue.h"
static void sig_alrm(int signo) {
/* just return */
}
unsigned int mysleep(unsigned int nsecs) {
// return sleep(nsecs);
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask, pendingmask;
unsigned int unslept, oldalrm, slept;
int isblock = 0, ispending = 0;
/* set handler, save previous information */
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
/* has alarm? */
oldalrm = alarm(0);
/* is block? */
sigprocmask(0, NULL, &oldmask);
if (sigismember(&oldmask, SIGALRM)) {
isblock = 1;
}
/* is pending? */
if (sigpending(&pendingmask) < 0) {
err_sys("sigpending error\n");
}
if (sigismember(&pendingmask, SIGALRM)) { /* handle the pending signal */
ispending = 1;
pendingmask = oldmask;
sigdelset(&pendingmask, SIGALRM);
sigsuspend(&pendingmask);
}
/* block SIGALRM and save current signal mask */
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
/* sleep */
if (oldalrm && (oldalrm < nsecs) && !isblock) { /* if has set alarm */
alarm(oldalrm);
} else {
alarm(nsecs);
}
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM); /* make sure SIGALRM isn't blocked */
sigsuspend(&suspmask); /* wait for any signal to be caught */
/* signal caught */
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL); /* reset previous action */
/* reset signal mask, which unblocks SIGALRM */
sigprocmask(SIG_SETMASK, &oldmask, NULL);
/* cal the slept time and return the new unslept time */
if (oldalrm && (oldalrm < nsecs) && !isblock) {
slept = oldalrm - unslept;
} else {
slept = nsecs - unslept;
}
/* handle previous interaction */
if ((oldalrm && (slept >= oldalrm)) || (isblock && oldalrm) || ispending) {
kill(getpid(), SIGALRM);
}
if (slept < oldalrm) { /* reset previous alarm */
alarm(oldalrm - slept);
}
return(nsecs - slept);
}