收发信号思想是 Linux 程序设计特性之一,一个信号可以认为是一种软中断,通过用来向进程通知异步事件。

        本文讲述的 信号处理内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解信号编程。


signal

1.库

标准 c 库,libc, -lc

2.头文件

<signal.h>

3.接口定义

       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

4.接口描述

       注意:signal() 的行为会根据不同的 UNIX 版本而变化,同样它也会根据 Linux 版本的不同而不同。考虑到程序的可移植性,尽量避免 signal() 的使用,而是使用 sigacton(2) 代替,可以参考下面移植性部分。

        signal() 设置 signum 信号的处理函数为 handler,处理函数可以是SIG_IGN、SIG_DFL,也可以是程序设计人员定义的函数地址。

        如果 signum 被分发给到一个进程,那么会发生以下行为:

  • 如果处理函数设置成了 SIG_IGN,那么信号会被忽略。
  • 如果处理函数设置成了 SIG_DFL,那么信号的默认关联行为会发生(可以参考 signal(7))。
  • 如果处理函数设置成了一个函数,那么首先会将默认处理函数复位为 SIG_DFL,或者信号被阻塞(参考下面可移植性部分),然后会调用 handler 函数并传递 signum 参数。如果处理函数调用导致了该信号处理阻塞,那么在处理函数返回后,该信号会重新被 unblock。

        SIGKILL 和 SIGSTOP 两个信号不能被捕捉或者忽略。

5.返回值

        signal() 返回信号之前 的处理函数值。发生错误时,signal() 会返回 SIG_ERR,并设置 errno 来提示具体错误。

        错误值定义如下:

6.版本

       sighandler_t 是一个 GNU 扩展,它会在 _GNU_SOURCE 定义时暴漏出来。如果定义了_BSD_SOURCE(glibc 2.19 或低版本) 或者 _DEFAULT_SOURCE(glibc 2.19 或高版本),glibc 也定义了 sig_t。不使用这些定义的情况下,signal() 的声明就会有些晦涩:

           void ( *signal(int signum, void (*handler)(int)) ) (int);

可移植性 

        signal() 只有在将 handler 设置为 SIG_DFL/SIG_IGN 时,才具有移植性。使用 signal() 建立信号处理函数的语义随着系统的不同而不同,POSIX.1 明确允许这些不同的行为。所以不要使用它于此目的。

        POSIX.1 通过 sigaction(2) 接口解决了这种移植上的混乱,sigaction(2)  提供了信号处理调用的明确语义定义。所以,使用 sigaction(2) 来代替 signal()。

7.历史

        C89,POSIX.1-2001

        在原来的 UNIX 系统中,当使用 signal() 建立的信号处理函数被调用时,信号的处理会被设置成 SIG_DFT,并且系统不会阻塞该信号通往其他进程的发布。这相当于调用了 sigaction(2),附带以下标记:

           sa.sa_flags = SA_RESETHAND | SA_NODEFER;

         System V 已提供了 signal() 的语义,不过这个定义有点差劲,因为在处理函数重新建立连接前,可能会连续收到两个信号。更严重的,同一个信号的频繁分发会导致处理函数的递归调用。

        BSD 对此进行了改善,但不幸的是这种改善却改变了现存 signal() 接口的语义。在 BSD 系统上,当一个处理函数调用时,信号处置并没有被重新设置,后面发生的该信号的实例因该处理函数正在执行无法进行分发。更严重的,一些阻塞系统调用会在信号处理函数打断后自动重启。BSD 语义相当于使用下面标记调用 sigaction(2):

           sa.sa_flags = SA_RESTART;

         Linux 上的情景如下:

  • 内核的 signal() 系统调用提供了 System V 语义
  • 默认情况下,glibc 2 及更高版本的 signal() 封装并没有调用内核系统调用,而是调用了 sigaction(2),提供 BSD 语义的标记。只要提供合适的宏定义,就可以提供以上默认行为:glibc 2.19 或低版本的 _BSD_SOURCE 或者 2.19 或更高版本的 _DEFAULT_SOURCE。(默认情况下,这些宏是定义了的,参考 feature_test_macros(7) )。如果这些测试宏没有开启,那么 signal) 提供的是 System V 语义。 

 8.注意

        signal() 在进程的多线程场景下的副作用是未定义的。

        根据 POSIX 定义,进程忽略非 kill(2)/raise(3) 产生的 SIGFPE/SIGILL/SIGSEGV 信号后的行为是未定义的。整数除以 0 是未定义的结果,在一些架构上它会产生 SIGFPE 信号(同样使用 -1 除最大负整数也可能产生 SIGFPE。)忽略这些信号可能会导致无限循环。

        参考 sigaction(2) 获取更多关于将 SIGCHLD 信号的处置设置为 SIGIGN 的信息。

        参考 signal-safety(7) 来查看一些可以在信号处理函数内部调用的异步信号安全的函数。

9.代码

        下面是一个捕捉 CTRL + C 信号的程序。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void sighandler(int);

int main()
{
   signal(SIGINT, sighandler);

   while(1) 
   {
      printf("开始休眠一秒钟...\n");
      sleep(1);
   }

   return(0);
}

void sighandler(int signum)
{
   printf("捕获信号 %d,跳出...\n", signum);
   exit(1);
}
09-18 20:48