前言
  Linux多线程环境中的信号处理不同于进程的信号处理。一方面线程间信号处理函数的共享性使得信号处理更为复杂,另一方面普通异步信号又可转换为同步方式来简化处理。
  本文首先介绍信号处理在进程中和线程间的不同,然后描述相应的线程库函数,在此基础上给出一组示例代码,以讨论线程编程中信号处理的细节和注意事项。文中涉及的代码运行环境如下:

  本文通过sigwait()调用来“等待”信号,而通过signal()/sigaction()注册的信号处理函数来“捕获”信号,以体现其同步和异步的区别。
  一  概念
  1.1 进程与信号
  信号是向进程异步发送的软件通知,通知进程有事件发生。事件可为硬件异常(如除0)、软件条件(如闹钟超时)、控制终端发出的信号或调用kill()/raise()函数产生的用户逻辑信号。
  当信号产生时,内核通常在进程表中设置一个某种形式的标志,即向进程递送一个信号。在信号产生(generation)和递送(delivery)之间(可能相当长)的时间间隔内,该信号处于未决(pending)状态。已经生成但未递送的信号称为挂起(suspending)的信号。
  进程可选择阻塞(block)某个信号,此时若对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程(a)对此信号解除阻塞,或者(b)将对此信号的动作更改为忽略。内核为每个进程维护一个未决(未处理的)信号队列,信号产生时无论是否被阻塞,首先放入未决队列里。当时间片调度到当前进程时,内核检查未决队列中是否存在信号。若有信号且未被阻塞,则执行相应的操作并从队列中删除该信号;否则仍保留该信号。因此,进程在信号递送给它之前仍可改变对该信号的动作。进程调用sigpending()函数判定哪些信号设置为阻塞并处于未决状态。
  若在进程解除对某信号的阻塞之前,该信号发生多次,则未决队列仅保留相同不可靠信号中的一个,而可靠信号(实时扩展)会保留并递送多次,称为按顺序排队。
  每个进程都有一个信号屏蔽字(signal mask),规定当前要阻塞递送到该进程的信号集。对于每个可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则该信号当前被阻塞。
  应用程序处理信号前,需要注册信号处理函数(signal handler)。当信号异步发生时,会调用处理函数来处理信号。因为无法预料信号会在进程的哪个执行点到来,故信号处理函数中只能简单设置一个外部变量或调用异步信号安全(async-signal-safe)的函数。此外,某些库函数(如read)可被信号中断,调用时必须考虑中断后出错恢复处理。这使得基于进程的信号处理变得复杂和困难。
  1.2 线程与信号
  内核也为每个线程维护未决信号队列。当调用sigpending()时,返回整个进程未决信号队列与调用线程未决信号队列的并集。进程内创建线程时,新线程将继承进程(主线程)的信号屏蔽字,但新线程的未决信号集被清空(以防同一信号被多个线程处理)。线程的信号屏蔽字是私有的(定义当前线程要求阻塞的信号集),即线程可独立地屏蔽某些信号。这样,应用程序可控制哪些线程响应哪些信号。
  信号处理函数由进程内所有线程共享。这意味着尽管单个线程可阻止某些信号,但当线程修改某信号相关的处理行为后,所有线程都共享该处理行为的改变。这样,若某线程选择忽略某信号,而其他线程可恢复信号的默认处理行为或为信号设置新的处理函数,从而撤销原先的忽略行为。即对某个信号处理函数,以后一次注册的处理函数为准,从而保证同一信号被任意线程处理时行为相同。此外,若某信号的默认动作是停止或终止,则不管该信号发往哪个线程,整个进程都会停止或终止。
  若信号与硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV)或定时器超时相关,该信号会发往引起该事件的线程。其它信号除非显式指定目标线程,否则通常发往主线程(哪怕信号处理函数由其他线程注册),仅当主线程屏蔽该信号时才发往某个具有处理能力的线程。
  Linux系统C标准库提供两种线程实现,即LinuxThreads(已过时)和NPTL(Native POSIX Threads Library)。NPTL线程库依赖Linux 2.6内核,更加(但不完全)符合POSIX.1 threads(Pthreads)规范。两者的详细区别可以通过man 7 pthreads命令查看。
  NPTL线程库中每个线程拥有自己独立的线程号,并共享同一进程号,故应用程序可调用kill(getpid(), signo)将信号发送到整个进程;而LinuxThreads线程库中每个线程拥有自己独立的进程号,不同线程调用getpid()会得到不同的进程号,故应用程序无法通过调用kill()将信号发送到整个进程,而只会将信号发送到主线程中去。
  多线程中信号处理函数的共享性使得异步处理更为复杂,但通常可简化为同步处理。即创建一个专用线程来“同步等待”信号的到来,而其它线程则完全不会被该信号中断。这样可确知信号的到来时机,必然是在专用线程中的那个等待点。
  注意,线程库函数不是异步信号安全的,故信号处理函数中不应使用pthread相关函数。
  二  接口
  2.1 pthread_sigmask
  线程可调用pthread_sigmask()设置本线程的信号屏蔽字,以屏蔽该线程对某些信号的响应处理。
  #include <signal.h>
  int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
  该函数检查和(或)更改本线程的信号屏蔽字。若参数oset为非空指针,则该指针返回调用前本线程的信号屏蔽字。若参数set为非空指针,则参数how指示如何修改当前信号屏蔽字;否则不改变本线程信号屏蔽字,并忽略how值。该函数执行成功时返回0,否则返回错误编号(errno)。
  下表给出参数how可选用的值。其中,SIG_ BLOCK为“或”操作,而SIG_SETMASK为赋值操作。

  主线程调用pthread_sigmask()设置信号屏蔽字后,其创建的新线程将继承主线程的信号屏蔽字。然而,新线程对信号屏蔽字的更改不会影响创建者和其他线程。
  通常,被阻塞的信号将不能中断本线程的执行,除非该信号指示致命的程序错误(如SIGSEGV)。此外,不能被忽略处理的信号(SIGKILL 和SIGSTOP )无法被阻塞。
  注意,pthread_sigmask()与sigprocmask()函数功能类似。两者的区别在于,pthread_sigmask()是线程库函数,用于多线程进程,且失败时返回errno;而sigprocmask()针对单线程的进程,其行为在多线程的进程中没有定义,且失败时设置errno并返回-1。