Signals

Table of Contents

1. Signals 基本概念

1.1. 什么是信号

A signal is a notification to a process that an event has occurred. Signals are sometimes described as software interrupts.

signal_delivery_and_handler_execution.gif

Figure 1: Signal delivery and handler execution

1.2. 谁产生信号

  • One process can (if it has suitable permissions) send a signal to another process. In this use, signals can be employed as a synchronization technique, or even as a primitive form of interprocess communication (IPC).
  • It is also possible for a process to send a signal to itself.
  • However, the usual source of many signals sent to a process is the kernel.

1.3. 处理信号方式

内核在某个信号出现时,可按下面三种方式之一进行处理:

  1. 忽略此信号。 SIGKILL 和 SIGSTOP 两种信号不能被忽略。
  2. 捕捉信号,信号发生时调用一个用户指定的信号处理函数。 SIGKILL 和 SIGSTOP 两种信号不能被捕捉。
  3. 执行系统默认动作,每种信号都有系统默认动作。

注:进程接收到 SIGSTOP 信号后会暂停执行,可以发送 SIGCONT 信号使其恢复执行。

2. 信号的产生和投递

A signal is said to be generated by some event. Once generated, a signal is later delivered to a process, which then takes some action in response to the signal. Between the time it is generated and the time it is delivered, a signal is said to be pending.
Normally, a pending signal is delivered to a process as soon as it is next scheduled to run, or immediately if the process is already running (e.g., if the process sent a signal to itself). Sometimes, however, we need to ensure that a segment of code is not interrupted by the delivery of a signal. To do this, we can add a signal to the process’s signal mask—a set of signals whose delivery is currently blocked.

如果信号被设置为阻塞的(blocked),且该信号没有被忽略,那么这个信号产生后不会被投递,而是一直处于未决的(pending)状态,直到(a)把该信号更改为非阻塞(unblocked)的,或者(b)将该信号的动作更改为忽略。

说明:内核在投递一个原来被阻塞的信号给进程时(而不是在产生该信号时),才决定对它的处理方式。也就是说进程在信号投递给它之前仍可改变对该信号的处理动作。

2.1. 什么时候内核处理投递的信号

The kernel handles signals only when a process returns from kernel mode to user mode. Thus, a signal does not have an instant effect on a process running in kernel mode. If a process is running in user mode, and the kernel handles an interrupt that causes a signal to be sent to the process, the kernel will recognize and handle the signal when it returns from the interrupt. Thus, a process never executes in user mode before handling outstanding signals.

参考:UNIX 操作系统设计 7.2 软中断信号

3. Changing Signal Dispositions: signal(), sigaction()

使用 signal() , sigaction() 都可以绑定信号和其对应的处理函数。
由于历史原因,不同的 UNIX 实现中, signal()函数的语义可能不一样(尽管它是 C90 中的函数),要使程序有较好的移植性,应该避免使用 signal()函数。推荐使用系统调用 sigaction(),它比 signal()功能更多,但使用也更复杂。

3.1. signal()

#include <signal.h>
void ( *signal(int sig, void (*func)(int)) ) (int);

/* Returns previous disposition of signal on success, or SIG_ERR on error */

如果使用 typedef,则 signal 的声明可写为更易读的形式:

typedef void (*sighandler_t) (int);
sighandler_t signal(int sig, sighandler_t func);

signal 的第 1 个参数 sig 是信号名。
signal 的第 2 个参数可以是下面三个值:

  1. 常量 SIG_IGN。表示忽略这个信号。进程将不会接收到对应的信号,内核会直接把信号丢弃。
  2. 常量 SIG_DFL。重新设置信号的处理方式为默认行为。
  3. 接收到此信号后要调用的函数的地址。

signal 会返回一个函数地址,当调用 signal 成功后,会返回之前的信号处理函数;当调用 signal 失败后,会返回 SIG_ERR。
如果查看系统的头文件 signal.h,会找到类似下面的声明:

#define SIG_DFL         (void (*)(int))0
#define SIG_IGN         (void (*)(int))1
#define SIG_ERR         ((void (*)(int))-1)

3.2. signal()实例

#include <stdio.h>
#include <signal.h>
#include <unistd.h>   /* pause() */

static void sig_usr(int);   /* one handler for both signals */

int
main(void)
{
  if (signal(SIGUSR1, sig_usr) == SIG_ERR)
    fprintf(stderr, "can't catch SIGUSR1");
  if (signal(SIGUSR2, sig_usr) == SIG_ERR)
    fprintf(stderr, "can't catch SIGUSR2");
  for ( ; ; )
    pause();    /* waiting for a signal */
}

static void
sig_usr(int signo)  /* argument is signal number */
{
  if (signo == SIGUSR1)
    printf("received SIGUSR1\n");
  else if (signo == SIGUSR2)
    printf("received SIGUSR2\n");
  else
    fprintf(stderr, "received signal %d\n", signo);
}

运行上面程序,并向其发送 SIGUSR1 和 SIGUSR2 信号。

$ ./a.out &
[1] 1162
$ kill -USR1 1162
received SIGUSR1
$ kill -USR2 1162
received SIGUSR2
$ kill 1162
[1]+  Terminated: 15          ./a.out

3.3. sigaction()

系统调用 sigaction() 的功能是检查或修改与指定信号相关联的处理动作(或同时执行这两种操作)。

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

/* Returns 0 on success, or –1 on error */

其中,signo 是要检查或修改其具体动作的信号编号。若 act 指针非空,则要修改其关联的动作。如果 oldact 指针非空,则系统经由 oldact 指针返回该信号的上一个动作。

3.4. sigaction()实例

下面程序收到 SIGTERM 信号时打印消息。

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

static void myhandler(int signum, siginfo_t *siginfo, void *context) {
    printf("Signal caught: %d, PID: %ld, UID: %ld\n",
           signum, (long)siginfo->si_pid, (long)siginfo->si_uid);
    /* 并不是所有的信号都会设置si_pid,si_uid,参考 man sigaction */
}

int main() {
    struct sigaction act;

    /* The SA_SIGINFO flag tells sigaction() to use the sa_sigaction field, not
       sa_handler filed. See `man sigaction' */
    act.sa_flags = SA_SIGINFO;

    /* myhandler has 3 arguments, use sa_sigaction, rather than sa_handler. */
    act.sa_sigaction = myhandler;
    sigemptyset(&act.sa_mask);

    sigaction(SIGTERM, &act, NULL);

    pause();
    return 0;
}

测试结果:

$ ./a.out &
[1] 26218
$ kill 26218
Signal caught: 15, PID: 12433, UID: 5200

4. 执行系统调用时发生中断,系统调用是否重启

考虑下面情况:当进程正在执行一个低速系统调用(如 read/write)时,在低速系统调用的阻塞期间捕捉到一个信号,系统调用被中断而执行相应的信号处理函数,那么信号处理函数返回后,系统调用还会继续执行吗?

默认地,系统调用不会继续执行,会返回出错,其 errno 被设置为 EINTR
如果我们想要继续执行被中断的系统调用,我们需要显式地处理低速系统调用的出错返回。典型的处理代码如下:

again:
if ((n = read(fd, buf, BUFFSIZE)) < 0) {
    if (errno == EINTR)
        goto again;     /* just an interrupted system call */
    /* handle other errors */
}

除上面的方法外,我们还有其它更好的方法:在建立信号处理函数时设置 SA_RESTART 标记。
We can specify the SA_RESTART flag when establishing the signal handler with sigaction(), so that system calls are automatically restarted by the kernel on the process’s behalf. This means that we don’t need to handle a possible EINTR error return for these system calls.
The SA_RESTART flag is a per-signal setting. In other words, we can allow handlers for some signals to interrupt blocking system calls, while others permit automatic restarting of system calls.

注意:由于历史的原因,指定了 SA_RESTART 后,并不是所有的系统调用都会重启。
详情可参考:
Advanced Programming in the UNIX Environment, 10.5 Interrupted System Calls
The Linux Programming Interface, 21.5 Interruption and Restarting of System Calls

4.1. 有些系统调用不会被会中断

大部分系统调用在收到信号时可以被中断(可能返回 EINTR 错误)。也有一些系统调用(如 mkdir)在收到信号时不会被中断(一般情况下,这些系统调用会很快完成),当程序正在运行该类系统调用时,进程的状态被称为“Uninterruptible Sleep State”,用 ps 命令查看时其 STAT 列会显示字母 D,这类进程是无法立刻被“kill -9”掉的(因为它不会被信号中断)。

注:查看系统调用的 man 文档,如果它不会返回 EINTR 错误,则说明它不会被信号中断。

参考: man 2 mkdir

5. 可重入函数(即:异步信号安全函数)

进程捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回(例如没有调用 exit 或 longjmp),则继续执行在捕捉到信号时进行正在执行的正常指令序列(这类似于发生硬件中断时所做的)。 但在信号处理程序中,不能判断捕捉到信号时进程在何处执行。 如果进程正在执行 malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用 malloc,这时会发生什么?可能会对进程造成破坏,因为 malloc 通常为它所分配的存储区维护一个链接表,而插入执行信号处理程序时,进行可能正在更改此链接表。
若在信号处理程序中调用一个不可重入函数,则其结果是不可预见的。
摘自:APUE, 10.6 可重入函数

A function is said to be reentrant if it can safely be simultaneously executed by multiple threads of execution in the same process. In this context, "safe" means that the function achieves its expected result, regardless of the state of execution of any other thread of execution.
可重入函数符合下面条件:

  1. 不在函数内部使用静态或全局数据。
  2. 不返回静态或全局数据,所有数据都由函数的调用者提供。
  3. 不调用不可重入函数。

An AS-Safe (async-signal-safe) function is one that the implementation guarantees to be safe when called from a signal handler.
参考:The Linux Programming Interface, by Michael Kerrisk, 21.1 Designing Signal Handlers

异步信号安全函数和可重入函数可以理解为从不同的角度描述同一事情。

5.1. 信号处理函数不要调用异步信号不安全函数

信号处理函数中不要调用异步信号不安全的函数,可能会带来不可预见的结果。
信号处理函数要尽可能简单。
Signal handlers should be as concise as possible—ideally by unconditionally setting a flag and returning.

在“CERT C Coding Standard”中有个信号处理函数中调用异步信号不安全函数 longjmp()的反例及其对应的解决方法。

有问题的代码如下:

#include <setjmp.h>
#include <signal.h>
#include <stdlib.h>

enum { MAXLINE = 1024 };
static jmp_buf env;

void handler(int signum) {
  longjmp(env, 1);          /* Unsafe! */
}

void log_message(char *info1, char *info2) {
  static char *buf = NULL;
  static size_t bufsize;
  char buf0[MAXLINE];

  if (buf == NULL) {
    buf = buf0;
    bufsize = sizeof(buf0);
  }

  /*
   * Try to fit a message into buf, else reallocate
   * it on the heap and then log the message.
   */

  /* Program is vulnerable if SIGINT is raised here */

  if (buf == buf0) {
    buf = NULL;
  }
}

int main(void) {
  if (signal(SIGINT, handler) == SIG_ERR) {
    /* Handle error */
  }
  char *info1;
  char *info2;

  /* info1 and info2 are set by user input here */

  if (setjmp(env) == 0) {
    while (1) {
      /* Main loop program code */
      log_message(info1, info2);
      /* More program code */
    }
  } else {
    log_message(info1, info2);
  }

  return 0;
}

函数 longjmp()或 siglongjmp()都不是信号安全的函数,不应该在信号处理函数中被调用。
在上面的例子中,如果 SIGINT 信号恰好在运行 log_message()的第 2 个 if 语句前产生,那么 longjmp 执行,程序的执行流程会回到 main()中,log_message()被再次调用。但是这次 log_message()中的第 1 个 if 语句不会被执行(因为前一次调用时,buf 没有被设置为 NULL 就被信号中断了),这样程序会往一个非法的内存地址写东西。

解决的方法是不在信号处理函数中调用 longjmp(),信号处理函数尽可能简单化,比如仅设置一个标记。如下所示:

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

enum { MAXLINE = 1024 };
volatile sig_atomic_t eflag = 0;

void handler(int signum) {
  eflag = 1;
}

void log_message(char *info1, char *info2) {
  static char *buf = NULL;
  static size_t bufsize;
  char buf0[MAXLINE];

  if (buf == NULL) {
    buf = buf0;
    bufsize = sizeof(buf0);
  }

  /*
   * Try to fit a message into buf, else reallocate
   * it on the heap and then log the message.
   */
  if (buf == buf0) {
    buf = NULL;
  }
}

int main(void) {
  if (signal(SIGINT, handler) == SIG_ERR) {
    /* Handle error */
  }
  char *info1;
  char *info2;

  /* info1 and info2 are set by user input here */

  while (!eflag) {
    /* Main loop program code */
    log_message(info1, info2);
    /* More program code */
  }

  log_message(info1, info2);

  return 0;
}

参考:
https://www.securecoding.cert.org/confluence/display/c/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers

6. Sending a Signal

The following system calls and library functions allow the caller to send a signal:

Table 1: functions used for sending a signal
send a signal description
raise Sends a signal to the calling thread.
kill Sends a signal to a specified process, to all members of a specified process group, or to all processes on the system.
killpg Sends a signal to all of the members of a specified process group.
pthread_kill Sends a signal to a specified POSIX thread in the same process as the caller.
tgkill Sends a signal to a specified thread within a specific process. (This is the system call used to implement pthread_kill.)
sigqueue Sends a real-time signal with accompanying data to a specified process.

7. Signal Mask (Blocking Signal Delivery)

有时,我们想确保程序执行某段代码时不被信号中断,这可通过设置 Signal Mask 来实现。
系统内核为每一个进程维护了一个 Signal Mask,它是一个信号的集合,这个集合里的信号都被称为阻塞的(blocked)。如果有阻塞的信号发送到进程,则信号并不会马上投递,而是一直处理 pending 状态,直到(a)把信号从 Signal Mask 中去掉,或者(b)将该信号的动作更改为忽略。

特别注意:
In multithreaded process, the signal mask is actually a per-thread attribute, and that each thread in a multithreaded process can independently examine and modify its signal mask using the pthread_sigmask() function.

7.1. 信号如何加入到 Signal Mask 中

一个信号可能以下面形式加入到 Signal Mask 中:

  • 当某个信号处理函数被调用时,触发这个处理函数的信号被自动加入到 Signal Mask 中。这个默认的行为可以通过在建立信号处理函数时指定选项 SA_NODEFER 来改变。
  • sigaction() 建立信号处理函数时,可以指定当这个信号处理函数被调用时,加入哪些信号到 Signal Mask 中;
  • 使用函数 sigprocmask() 可以把信号加入 Signal Mask 中。

参考:The Linux Programming Interface, 20.10 The Signal Mask (Blocking Signal Delivery)

7.2. 阻塞期间同一信号发生多次时只会投递一次

如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,UNIX 内核只投递这种信号一次(除非使用 POSIX.1 实时扩展)。

7.3. 信号集合的操作

Table 2: Signal Sets
Function Description
int sigemptyset(sigset_t *set) initializes a signal set to contain no members.
int sigfillset(sigset_t *set) initializes a signal set to contain all signals (including all realtime signals).
int sigaddset(sigset_t *set, int sig) add signal into signal set.
int sigdelset(sigset_t *set, int sig) remove signal from signal set.
int sigismember(const sigset_t *set, int sig) 测试信号 sig 是否在集合 set 中

One of sigemptyset() or sigfillset() must be used to initialize a signal set.

7.4. sigprocmask()实例

可以通过 sigprocmask() 来改变一个进程的 Signal Mask。

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset);
                                      /* Returns 0 on success, or –1 on error */

/*
If oldset is not null, it is set to the previous value of the signal mask.
If set is null, the value of how is insignificant and the mask remains unset providing a way to examine the signal mask without modification.
If set is not null, the action of sigprocmask() depends on the value of the parameter how (SIG_BLOCK/SIG_UNBLOCK/SIG_SETMASK).
     SIG_BLOCK    The new mask is the union of the current mask and the specified set.
     SIG_UNBLOCK  The new mask is the intersection of the current mask and the complement of the specified set.
     SIG_SETMASK  The current mask is replaced by the specified set.
*/

sigprocmask()实例:

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

int main(void)
{
    sigset_t blockSet, prevMask;

    /* Initialize a signal set to contain SIGINT */
    sigemptyset(&blockSet);
    sigaddset(&blockSet, SIGINT);

    /* Block SIGINT, save previous signal mask */
    if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1) {
        fprintf(stderr, "sigprocmask error1");
        exit(EXIT_FAILURE);
    }

    /* ... Code that should not be interrupted by SIGINT ... */
    sleep(10);
    printf("Hit 1\n");

    /* Restore previous signal mask, unblocking SIGINT */
    if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1) {
        fprintf(stderr, "sigprocmask error2");
        exit(EXIT_FAILURE);
    }

    printf("Hit 2\n");
    return 0;
}

运行上面程序,并在 sleep 过程中向其发送 SIGINT 信号。

$ ./a.out &
[1] 1146
$ kill -s INT 1146                       # 在sleep 10秒中,向程序发送SIGINT信号
$ Hit 1                                  # 程序不会马上退出,能输出"Hit 1",因为SIGINT信号被暂时block
[1]+  Interrupt: 2            ./a.out    # 程序没有输出"Hit 2",SIGNIT信号被unblock而导致程序前提退出

8. Determine Pending Signals: sigpending()

If a process receives a signal that it is currently blocking, that signal is added to the process’s set of pending signals. When (and if) the signal is later unblocked, it is then delivered to the process. To determine which signals are pending for a process, we can call sigpending().

The sigpending() system call returns the set of signals that are pending for the calling process in the sigset_t structure pointed to by set.

#include <signal.h>
int sigpending(sigset_t *set);
                                      /* Returns 0 on success, or –1 on error */

9. Displaying Signal Descriptions: sys_siglist, strsignal, psignal

Each signal has an associated printable description. These descriptions are listed in the array sys_siglist. For example, we can refer to sys_siglist[SIGPIPE] to get the description for SIGPIPE (broken pipe). However, rather than using the sys_siglist array directly, the char *strsignal(int sig) function is preferable.
The void psignal(int sig, const char *msg) function displays (on standard error) the string given in its argument msg, followed by a colon, and then the signal description corresponding to sig.

#include<stdio.h>
#include<signal.h>
#include<string.h>   /* strsignal */

extern const char *const sys_siglist[];

int main(void)
{
    const char* msg="my_message";

    printf("...Test sys_siglist...\n");
    printf("%s\n", sys_siglist[SIGINT]);
    printf("%s\n", sys_siglist[SIGQUIT]);
    printf("...Test strsignal...\n");
    printf("%s\n", strsignal(SIGINT));
    printf("%s\n", strsignal(SIGQUIT));
    printf("...Test psignal...\n");
    psignal(SIGINT, msg);
    psignal(SIGQUIT, msg);
    return 0;
}

/* 运行上面程序会得到下面的输出
...Test sys_siglist...
Interrupt
Quit
...Test strsignal...
Interrupt: 2
Quit: 3
...Test psignal...
my_message: Interrupt
my_message: Quit
*/

10. Waiting for a Signal

10.1. Waiting for a Signal: pause()

Calling pause() suspends execution of the process until the call is interrupted by a signal handler (or until an unhandled signal terminates the process).

#include <unistd.h>

int pause(void);
                            /* Always returns -1 with errno set to EINTR */

10.2. Waiting for a Signal Using a Mask: sigsuspend()

如果我们想要完成下面的任务:
①. We temporarily block a signal so that the handler for the signal doesn’t interrupt the execution of some critical section of code.
②. We unblock the signal, and then suspend execution until the signal is delivered.

我们可能会这样做:

sigset_t prevMask, intMask;
struct sigaction sa;

sigemptyset(&intMask);
sigaddset(&intMask, SIGINT);

sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;

if (sigaction(SIGINT, &sa, NULL) == -1)
    errExit("sigaction");

/* Block SIGINT prior to executing critical section. (At this point we assume that SIGINT is not already blocked.) */

if (sigprocmask(SIG_BLOCK, &intMask, &prevMask) == -1)
    errExit("sigprocmask - SIG_BLOCK");

/* Critical section: do some work here that must not be interrupted by the SIGINT handler */

/* End of critical section - restore old mask to unblock SIGINT */
if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1)
    errExit("sigprocmask - SIG_SETMASK");

/* BUG: what if SIGINT arrives now... */

pause();                            /* Wait for SIGINT */

上面代码有下面的问题:
如果信号 SIGINT 在其被设置为 blocked 的时间段中发生,则 SIGINT 的投递被推迟直到对其解除阻塞。就好像信号发生在解除对 SIGINT 的阻塞和调用 pause 之间。如果发生了这种情况,或者在解除对 SIGINT 的阻塞和调用 pause 之间确实发生了 SIGINT 信号,那么就产生了问题:pause()将使程序永远阻塞下去,直到第 2 个 SIGINT 信号到来。第 1 个 SIGINT 信号好像丢失了一样,这违背了原程序的本意。

为了解决上面的问题,我们需要一个方法能原子地完成对信号解除阻塞,使进程进入休眠。这就是系统调用 sigsuspend() 的主要功能。

#include <signal.h>
int sigsuspend(const sigset_t *mask);
                             /* (Normally) returns –1 with errno set to EINTR */

sigsuspend() 将进程的信号屏蔽字设置为由 mask 指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则 sigsuspend 返回(总是返回-1),并且将该进程的信号屏蔽字设置为调用 sigsuspend 之前的值。

Calling sigsuspend() is equivalent to atomically performing these operations:

sigprocmask(SIG_SETMASK, &mask, &prevMask); /* Assign new mask */
pause();
sigprocmask(SIG_SETMASK, &prevMask, NULL);  /* Restore old mask */

10.2.1. sigsuspend 实例

sigsuspend 的一个常见应用是等待信号处理程序设置一个全局变量。
下面程序捕捉信号 SIGINT 和 SIGQUIT,但是仅当捕捉到信号 SIGQOUT 时,才唤醒主程序。

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

volatile sig_atomic_t   quitflag;   /* set nonzero by signal handler */

static void sig_int(int signo)  /* one signal handler for SIGINT and SIGQUIT */
{
    if (signo == SIGINT)
        printf("\ninterrupt\n");
    else if (signo == SIGQUIT)
        quitflag = 1;   /* set flag for main loop */
}

static void err_sys(const char * str)
{
    fprintf(stderr, "%s", str);
    exit(EXIT_FAILURE);
}

int main(void)
{
    sigset_t    newmask, oldmask, zeromask;
    if (signal(SIGINT, sig_int) == SIG_ERR)
        err_sys("signal(SIGINT) error");
    if (signal(SIGQUIT, sig_int) == SIG_ERR)
        err_sys("signal(SIGQUIT) error");
    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);

    /*
     * Block SIGQUIT and save current signal mask.
     */
    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
        err_sys("SIG_BLOCK error");
    while (quitflag == 0)
        sigsuspend(&zeromask);

    /*
     * SIGQUIT has been caught and is now blocked; do whatever.
     */
    quitflag = 0;

    /*
     * Reset signal mask which unblocks SIGQUIT.
     */
    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
        err_sys("SIG_SETMASK error");

    exit(0);
}

运行上面程序,按 Ctrl+C(发送 SIGINT 信号)时程序不会退出,按 Ctrl+\(发送 SIGQUIT 信号)时程序会退出。

$ ./a.out
^C
interrupt
^C
interrupt
^C
interrupt
^C
interrupt
^C
interrupt
^\ $

10.3. Synchronously Waiting for a Signal: sigwaitinfo(), sigtimedwait()

11. sigsetjmp() 和 siglongjmp()

C 标准库中已经有了 setjmp()longjmp() 用作非局部跳转,为什么还需要 sigsetjmp()siglongjmp()
因为 setjmp()longjmp() 的行为在不同的平台不一样。在有的平台中(如 Linux 和 Solaris)只会保存和恢复栈,并不会保存和恢复 Signal Mask;而在另外一些平台中(如 FreeBSD 和 Mac OS X)在保存和恢复栈的同时还会保存和恢复 Signal Mask。

#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
          /* Returns: 0 if called directly, nonzero if returning from a call to siglongjmp */

void siglongjmp(sigjmp_buf env, int val);

这两个函数与 setjmp 和 longjmp 之间的唯一区别是 sigsetjmp 增加了一个参数。如果 savemask 为非 0 值,则 sigsetjmp 在 env 中保存进程的当前 Signal Mask。调用 siglongjmp 时,如果 sigsetjmp 已经保存了 Signal Mask 到 env,则 siglongjmp 从中恢复 Signal Mask。

注: 这些函数都不是异步信号安全的。

12. 让信号处理函数运行在另外的栈上: sigaltstack()

Normally, when a signal handler is invoked, the kernel creates a frame for it on the process stack. It is possible to arrange that the signal handler uses an alternate stack, see sigaltstack() for a discussion of how to do this and when it might be useful.

13. Realtime signals

Signals fall into two broad categories. The first set constitutes the traditional or standard signals, which are used by the kernel to notify processes of events. On Linux, the standard signals are numbered from 1 to 31. We describe the standard signals in this chapter. The other set of signals consists of the realtime signals.

14. Signals and Threads

The UNIX signal model was designed with the UNIX process model in mind, and predated the arrival of Pthreads by a couple of decades. As a result, there are some significant conflicts between the signal and thread models. These conflicts arose primarily from the need to maintain the traditional signal semantics for singlethreaded processes.

The differences between the signal and thread models mean that combining signals and threads is complex, and should be avoided whenever possible. Nevertheless, sometimes we must deal with signals in a threaded program.

参考:"The Linux Programming Interface" 33.2 Threads and Signals

14.1. How the UNIX Signal Model Maps to Threads

The following list summarizes the key points:

  • Signal actions are process-wide. If any unhandled signal whose default action is stop or terminate is delivered to any thread in a process, then all of the threads in the process are stopped or terminated.
  • Signal dispositions are process-wide; all threads in a process share the same disposition for each signal. If one thread uses sigaction() to establish a handler for, say, SIGINT, then that handler may be invoked from any thread to which the SIGINT is delivered. Similarly, if one thread sets the disposition of a signal to ignore, then that signal is ignored by all threads.
  • A signal may be directed to either the process as a whole or to a specific thread. A signal is thread-directed if: ① it is generated as the direct result of the execution of a specific hardware instruction within the context of the thread; ② it is a SIGPIPE signal generated when the thread tried to write to a broken pipe; or ③ it is sent using pthread_kill() or pthread_sigqueue(), which are functions that allow one thread to send a signal to another thread within the same process. All signals generated by other mechanisms are process-directed. Examples are signals sent from another process using kill() or sigqueue(); signals such as SIGINT and SIGTSTP, generated when the user types one of the terminal special characters that generate a signal; and signals generated for software events such as the resizing of a terminal window (SIGWINCH) or the expiration of a timer (e.g., SIGALRM).
  • When a signal is delivered to a multithreaded process that has established a signal handler, the kernel arbitrarily selects one thread in the process to which to deliver the signal and invokes the handler in that thread. This behavior is consistent with maintaining the traditional signal semantics. It would not make sense for a process to perform the signal handling actions multiple times in response to a single signal.
  • The signal mask is per-thread. (There is no notion of a process-wide signal mask that governs all threads in a multithreaded process.) Threads can independently block or unblock different signals using pthread_sigmask(), a new function defined by the Pthreads API. By manipulating the per-thread signal masks, an application can control which thread(s) may handle a signal that is directed to the whole process.
  • The kernel maintains a record of the signals that are pending for the process as a whole, as well as a record of the signals that are pending for each thread. A call to sigpending() returns the union of the set of signals that are pending for the process and those that are pending for the calling thread. In a newly created thread, the per-thread set of pending signals is initially empty. A thread-directed signal can be delivered only to the target thread. If the thread is blocking the signal, it will remain pending until the thread unblocks the signal (or terminates).

15. Tips

15.1. 多个不同信号的处理函数可能嵌套执行

When multiple unblocked signals are awaiting delivery, if a switch between kernel mode and user mode occurs during the execution of a signal handler, then the execution of that handler will be interrupted by the invocation of a second signal handler (and so on).

signal_delivery_of_multiple_unblocked_signals.gif

Figure 2: Delivery of multiple unblocked signals

参考:
The Linux Programming Interface, 22.6 Timing and Order of Signal Delivery
http://stackoverflow.com/questions/29738969/how-does-linux-prioritize-custom-signal-handlers

Author: cig01

Created: <2015-03-15 Sun>

Last updated: <2020-11-05 Thu>

Creator: Emacs 27.1 (Org mode 9.4)