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

2019年1月20日寒假大数据师资培训班

《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。

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

  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);
}