34-招待你的“客人”

对于进程来说,信号就像它的客人。客人来了,进程没好好执行它,那么客人会很生气,后果很严重。

比如你发送信号 1 信号 2 给进程,直接导致进程退出。客人来你家了,如果你不理会,导致的就是你家被毁(^_^客人还是挺牛逼的)。

本篇学习一个ANSI C 规定的函数 signal,这个函数,可以帮助我们招待指定的客人。

1. 如何招待客人

  • 按照规则编写信号处理函数
  • 使用 signal 函数安装你刚刚编写的信号处理函数,同时指定该信号函数可以处理哪个信号

上面是编写信号处理函数的基本规则。

2. signal 函数

  • 函数原型
#include <signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
  • 参数 handler

这个函数可能是你见过有史以来最复杂的函数了,原因在于它的第二个参数是函数指针。一般来说,参数里有函数指针的函数,称之为“注册”函数,而指针指向的那个函数,称之为“回调函数”。

所以 signal 函数可以称它为信号注册函数。你注册了你指定的函数,你的函数就可以被回调了。回调的意思是指不需要你亲自调用,而是由别人(一般来说是操作系统)调用。这是一种常见的编程技巧。

你也可以编写一些工具库,提供注册函数给别人注册他们自己的函数,如此一来即使别人的函数还没写好(你根本不知道谁会使用,什么时候使用你的工具库),你就可以事先在你的工具库里调用这个还没写好的函数啦,是不是有点未卜先知的意思?

系统为我们事先提供好的两个宏,分别是 SIG_DFL (default) 和 SIG_IGN (ignore)。如果 handler 被指定为 SIG_DFL,系统将为该信号指定默认的信号处理函数,如果 handler 被指定为 SIG_IGN,系统将忽略该信号。实际上在程序启动时,所有信号的处理函数都被指定为默认或者忽略。

注意:如果你需要指定自己编写的信号处理函数,你的函数格式必须为 void func(int) 这种形式,函数的名字可以随便,但是参数和返回值不能随便改。

  • 参数 signum

signal 的第一个参数指示了你需要捕捉哪个信号,这很简单。后面用例子讲解。

  • 返回值

signal 的返回值表示旧的信号处理函数。如果返回值等于 SIG_ERR 说明注册失败。

3. 招待你的客人吧

下面这个例子的功能很简单,捕捉了SIGUSR1, SIGUSR2, SIGINT, SIGTSTP, SIGQUIT 以及 SIGSEGV 信号,sighandler 函数是自己编写的信号处理函数,这个函数必须以 void 返回,并且带一个 int 参数。
主函数主要做了三件事:

  1. 打印自己的进程 id 号
  2. 注册完所有你想捕捉的信号
  3. 每隔 10 秒打一个点。

  • 代码
// catchsignal.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void sighandler(int sig) {
  switch(sig) {
    case SIGUSR1:
      printf("hello SIGUSR1\n");break;
    case SIGUSR2:
      printf("hello SIGUSR2\n");break;
    case SIGINT:
      printf("休想干掉我!\n");break;
    case SIGTSTP:
      printf("不要停止我!\n");break;
    case SIGQUIT:
      printf("就是不退出!\n");break;
    case SIGSEGV:
      printf("呃!程序出 bug 了!\n");break;
    default:
      printf("hello, who are you %d?\n", sig);
  }
  sleep(2); // 删除这一行,再给程序发信号,看看 main 函数打点的情况。
}


int main() {
  printf("I'm %d\n", getpid());

  if (SIG_ERR == signal(SIGUSR1, sighandler)) {
    perror("signal SIGUSR1");
  }
  if (SIG_ERR == signal(SIGUSR2, sighandler)) {
    perror("signal SIGUSR2");
  }
  if (SIG_ERR == signal(SIGINT, sighandler)) {
    perror("signal SIGINT");
  }
  if (SIG_ERR == signal(SIGTSTP, sighandler)) {
    perror("signal SIGTSTP");
  }
  if (SIG_ERR == signal(SIGQUIT, sighandler)) {
    perror("signal SIGQUTI");
  }
  if (SIG_ERR == signal(SIGSEGV, sighandler)) {
    perror("signal SIGSEGV");
  }

  while(1) {
    write(STDOUT_FILENO, ".", 1); 
    sleep(10);
  }   
  return 0;
}
  • 编译
$ gcc catchsignal.c -o catchsignal
  • 运行
$ ./catchsignal

程序运行起来后,你可以试试快捷键 Ctrl + CCtrl + Z 以及 Ctrl + \,看看你的进程有何反应。

你也可以再打开一个终端,使用 kill 命令发送信号给你的进程。

代码我都给你写好了,请一定动手完成,看看发送不同的信号程序的反应。

提示:kill -9 pid 可以终结你的进程。所有信号 9 被称为——终结者。

4. 总结

  • 掌握信号注册函数 signal
  • 理解什么是注册函数,什么是回调函数
  • 1-31 号信号是不可靠的,可能会丢失(参考练习1)
  • 信号会打断某些(不支持自动重启的函数,后面会讲)正在阻塞的函数(参考练习2)
  • SIGKILL 和 SIGSTOP 信号无法被捕获(参考练习3)

练习

  1. 在上面的实验中,如果你连续发送相同的信号,会有什么结果?请仔细观察!
  2. 把信号处理函数的 sleep 语句删除,再重新编译运行。当程序捕捉到信号后,是否还要等待 10 秒才会打点?
  3. 最后,请你尝试捕捉 SIGKILL 和 SIGSTOP 信号,看看能否成功。(提示:有些客人招待下就没事了,有些客人很霸道,比如城管)。
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页