给力星

Web Developer

实验五 信号处理_Unix环境高级编程

《UNIX环境高级编程》实验五 信号处理,本科生跟研究生做的实验是不同的。本科生做的是带时间限制的 myshell,研究生做的是实现与 UNIX 的 sleep 函数一样的 mysleep。

信号这部分的实验,特别是本科生的实验,之所以难,是因为大多连书上的内容都没去看明白,又怎么可能做得出来,给你代码看你都看不出个所以然。所以,先老老实实把书上相关的内容看明白再说。研究生的实验倒真的是难,要考虑的情况比较多。这两个实验的代码我都放上来了,mysleep 的代码在文章后头。

实验描述

信号处理,学习和掌握信号的处理方法,特别是 sigaction,alarm,sigpending,sigsetjmp 和 siglongjmp 等函数的使用。

要求:

1、编制具有简单执行时间限制功能的shell:

myshell [ -t <time> ]

这个测试程序的功能类似实验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。

至于对信号的具体处理,主要就包括三部分:

  1. 杀死正在执行的子进程(如果有)
  2. 对屏蔽的信号进行处理,即处理未决信号
  3. 进行跳转

当然,还得注意一些细节的处理,如对 SIGQUIT 信号的处理中,应有 alarm(0),取消之前所设置的闹钟。更多的细节就不详说了,还是那句话,先把书上内容看明白。

另外,网上的代码有的是错的:在等待用户输入时就开始计时,也就是若用户在时间限制内没有输入/执行命令,也会提示超时,这点是不必要的,不符合实验要求。实验要求的是“用户命令执行的时间限制”,是命令的执行时间,并不算上命令的输入时间。

至于长时间的执行命令,可以通过命令 sleep <time> 来模拟。

实验讲解PPT

UNIX环境高级编程实验五 讲解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);
}