信号是软件中断。

信号提供了一种处理异步事件的方法。

信号用于大多数复杂的应用程序中。

Unix系统的早期版本就已经提供了信号机制,但不可靠。POSIX.1对可靠信号例程进行了标准化。

信号概述

每个信号都有一个名字,以SIG开头。

在头文件<signal.h>中,信号名被定义为正整数常量(信号编号)。

如果应用程序和内核都需使用同一定义,通常将有关信息放置在内核头文件中,然后用户级头文件在包含该内核头文件。

不存在编号为0的信号,POSIX.1将该信号编号值定义为空信号。

产生信号的条件

条件示例
1. 当用户按某些终端按键时,可引发终端产生信号。在终端上按Delete或Ctrl+C键,通常会产生信号SIGINT,可用于停止进程。
2. 硬件异常产生信号:除数为0、无效的内用引用等。硬件检测到条件通知内核,内核为当时运行的进程产生信号。无效内存引用时,内核为进程产生SIGSEGV信号。
3. 进程调用kill()函数发送任意信号给另一个进程或进程组。接收信号的进程与发送信号进程的所有者必须相同,或发送信号进程的所有者是超级用户。
4. 用户用kill命令信号给其他进程。此命令可以用来终止后台进程。
5. 当检测到软件条件发生时,有时也会产生信号通知有关进程。SIGURG带外数据、SIGPIPE管道读关闭,有进程写、SIGALRM进程定时器超时。

处理信号的方式

  1. 忽略此信号。(SIGKILLSIGTSOP信号不能被忽略,这两个信号是内核与超级用户停止进程的可靠方法。)
  2. 捕捉信号。(信号发生时,执行一个用户函数。)不能捕捉SIGKILLSIGSTOP信号
  3. 执行系统默认动作。(对于大多数信号,系统的默认动作是终止该进程。)

信号与默认动作

信号名说明默认动作
SIGABRT异常终止进程终止+core
SIGALRM定时器超时进程终止
SIGBUS硬件故障进程终止+core
SIGCANCEL线程库内部使用忽略
SIGCHLD子进程状态改变忽略
SIGCONT使暂停的进程继续进程继续或忽略
SIGEMT硬件故障进程终止+core
SIGFPE算数异常进程终止+core
SIGHUP连接断开进程终止
SIGILL非法硬件指令进程终止+core
SIGINFO键盘状态请求忽略
SIGINT终端中断符进程终止
SIGIO异步I/O进程终止或忽略
SIGIOT硬件故障进程终止或忽略
SIGKILL终止进程终止
SIGPIPE写无读进程的管道进程终止
SIGPOLL可轮询事件进程终止
SIGPWR电源失效或重启动进程终止或忽略
SIGQUIT终端退出符进程终止+core
SIGSEGV无效地址引用进程终止+core
SIGSTOP停止停止进程
SIGSYS无效系统调用进程终止+core
SIGTERM终止进程终止
SIGTSTP终端停止/挂起符停止进程
SIGTTIN后台读控制tty停止进程
SIGTTOU后台写控制tty停止进程
SIGURG紧急情况(套接字)忽略
SIGUSR1用户自定义信号进程终止
SIGUSR2用户自定义信号进程终止
SIGXCPU超出CPU限制进程终止(+core)
SIGXFSZ超出文件长度限制进程终止(+core)

Linux中,core文件名可通过/proc/sys/kernel/core_pattern进行配置。

SIGABRT

调用abort()函数产生此信号,用于表示进程异常终止。

SIGALRM

当调用alarm()函数设置的定时器超时会产生此信号,可用于超时限制。

SIGCHLD

当一个进程终止或停止时,会发送SIGCHLD信号给其父进程。

如果父进程想要获取其子进程的状态变化,可以捕捉该信号。信号捕捉函数中通常要调用一种wait函数以取得子进程ID与其终止状态。

SIGCONT

该作业信号发送给需要继续运行,但当前处于停止状态的进程。

如果接收此信号的进程不是处于停止状态,则忽略该信号。

SIGFPE

算术异常,比如除0、浮点溢出等。

SIGHUP

如果终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程)。

收到此信号的会话首进程也可能是在后台运行,与中断、退出以及挂起信号传递给前台进程组不同。

如果会话首进程终止,也会产生该信号,并且该信号会发送至前台进程组中的每一个进程。

通常用此信号通知守护进程再次读取其配置文件,因为守护进程不会有控制终端。

SIGINT

终端中断信号。

当用户按中断键(Delete或Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组的每一个进程。

通常用于前台进程失控,向屏幕输出大量信息时,用此信号终止该进程。

SIGKILL

不能被捕捉或忽略的两个信号之一。

用于向系统管理提供杀死进程的可靠方法。

SIGPIPE

当管道的读进程已经终止时,写管道就会产生该信号。

SIGQUIT

终端退出信号。

当用户在终端上按退出键(Ctrl+\)时,终端驱动程序产生此程序并发送给前台进程组中的每一个进程。

此信号会终止前台进程中的所有进程,也会产生一个core文件。

$ ./test
200809
^\Quit (core dumped)

SIGSEGV

进程引用无效内存或内存越界。

SIGSTOP

作业控制信号,用于停止一个进程。

类似于交互停止信号SIGTSTP,但该信号不能被捕捉或忽略。

SIGSYS

无效的系统调用。

如果用户编写了使用新系统调用的程序,却运行在不支持该系统调用的旧版系统上,则会出现该信号。

SIGTERM

kill命令发送的默认信号,用于终止程序。

该信号可被应用程序捕捉,因此可以让程序做好退出清理工作。

SIGTSTP

交互停止信号。

当用户在终端上按挂起键(Ctrl+Z)时,终端驱动程序会产生此信号,并发送给前台进程组的所有进程。

对于终端驱动程序来说,停止字符一般指的是Ctrl+S终止终端输出,Ctrl+Q启动终端输出。

通常终端驱动程序称产生交互停止信号的字符为挂起字符,而非停止字符。

SIGTTIN

后台进程组进程读取其控制终端时,终端驱动程序会产生该信号。

有两种情况不会产生该信号:

  1. 该进程忽略或者阻塞此信号。
  2. 该进程为孤儿进程组(后台进程组)成员,读操作会直接返回出错。

SIGTTOU

后台进程组写其控制终端时,终端驱动程序会产生该信号。

后台进程是否被允许写控制终端是可选择的。

如果不允许后台进程写控制终端,也有两种情况不会产生该信号,与SIGTTIN信号相同。

SIGURG

此信号用于通知另一个进程发生紧急情况。比如网络连接上收到带外数据,可选择此信号。

SIGUSR1

用户自定义信号,用法取决于应用程序。

SIGUSR2

该信号用法与SIGUSR1一致。

设置信号处理函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 返回值:成功返回以前的信号处理函数指针;出错返回SIG_ERR

typedef void (*__sighandler_t) (int);
#define     SIG_ERR  ((__sighandler_t) -1)  /* Error return.  */
#define     SIG_DFL  ((__sighandler_t)  0)  /* Default action.  */
#define     SIG_IGN  ((__sighandler_t)  1)  /* Ignore signal.  */

signum参数指的是信号名,handler参数值可以是SIG_IGNSIG_DFL或接收到信号后要执行的函数地址。

signal()的返回值是一个函数地址,该函数具有一个整型参数,是在此之前信号处理函数的指针。

该函数是由ISO C定义的,因此对于不同的系统具有不同的实现方式。该函数主要用于向后兼容,新程序中建议使用sigaction函数替代。

示例

下面的示例使用signal()函数捕捉用户自定义信号SIGUSR1SIGUSR2

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

static void sig_user(int);

int main(int argc, char* argv[])
{
    if (signal(SIGUSR1, sig_user) == SIG_ERR)
        perror("signal SIGUSR1");
    else if (signal(SIGUSR2, sig_user) == SIG_ERR)
        perror("signal SIGUSR2");

    while (1)
        pause();
}

static void sig_user(int signo)
{
    if (signo == SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo == SIGUSR2)
        printf("received SIGUSR2\n");
    else
        printf("received signal %d\n", signo);
}

在后台运行该进程,并使用kill命令将信号发送给它。kill默认发送SIGTERM信号,如果不捕捉,默认动作是终止进程。

输出结果如下:

$ ./catch_user_signal  &
[1] 3572
$ kill -USR1 3572
$ received SIGUSR1

$ kill -USR2 3572
$ received SIGUSR2

$ kill  3572
$
[1]+  Terminated              ./catch_user_signal
$

默认信号处理动作

当执行一个程序时,所有信号的状态都是系统默认或者忽略。

通常所有信号都被设置为它们的默认动作,而忽略信号的原因一般是执行exec的进程设置忽略该信号

exec函数会将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态不变(因为信号捕捉函数的地址在新程序中不可用)。

例如,对于非作业控制的shell,前台进程与后台进程同处于同一进程组,shell会在执行exec运行后台进程之前会将中断和退出信号的处理方式设置为忽略。否则,当按下中断字符时,前后台进程都会被终止。

捕捉中断和退出信号的方式(先检测信号是否被忽略,未忽略则捕捉):

void sig_int(int), sig_quit(int);
if (signal(SIGINT, SIG_IGN) != SIG_IGN)
    signal(SIGINT, sig_int);
if(signal(SIGQUIT, SIG_IGN) != SIG_IGN)
    signal(SIGQUIT, sig_quit);

由此可见,signal()函数的缺点,不改变信号的处理方式就不能确定当前的处理方式。

当一个进程调用fork时,子进程继承其父进程的信号处理方式。因为信号捕捉函数在子进程中是有意义的。

作业控制信号

有6个信号与作业控制相关,分别是:

信号作用
SIGCHLD子进程已停止或终止时产生,用于通知父进程。
SIGCONT如果进程已停止,收到该信号会继续执行。
SIGSTOP停止进程,不能被捕捉或忽略,相当于强制停止。
SIGTSTP交互式停止进程,可通过终端按键(Ctrl+Z)产生。
SIGTTIN后台进程组成员读控制终端时产生,默认停止该进程,然后作业控制shell通知用户。
SIGTTOU后台进程组成员写控制终端时产生,默认停止该进程,然后作业控制shell通知用户。

除了SIGCHLD信号外,其他信号一般都有作业控制的交互式shell进程来处理的。

我们可以使用shell的作业控制命令fgbg来将作业转入前台或后台运行,这两个命令同时会发送SIGCONT信号给该进程组。

例如,在打开vim之后,按键Ctrl+Z可以停止vim进程并将其转入后台,然后执行其他动作,最后可以输入fg %1%1是作业编号)将vim进程转入前台并发送SIGCONT信号让其继续运行。

在上面的例子中,vim进程需要在接收到SIGTSTP信号停止运行之前重新绘制终端屏幕,在执行fg %1转入前台并接收到SIGCONT时也要重新绘制屏幕。

这种情况的处理流程如示例所示:

#include <signal.h>
#include <unistd.h>
#define BUFFSIZE 1024

static void sig_tstp(int signo)
{
    // 1. 收到SIGTSTP信号之后,需要将光标移到左下角,重置tty模式
    // 2. 解除阻塞SIGTSTP信号
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTSTP);
    sigprocmask(SIG_UNBLOCK, &mask, NULL);
    // 3. 恢复SIGTSTP信号为默认动作
    signal(SIGTSTP, SIG_DFL);
    // 4. 向自己发送SIGTSTP信号,让系统执行默认动作
    // 正常情况下不会返回,因为kill会等信号递送完才会返回,SIGTSTP信号递送完进程就会转入后台休眠
    kill(getpid(), SIGTSTP);
    // 当执行fg %1等命令之后,交互式shell会向该进程发送SIGCONT信号,进程继续执行,才会返回到这里
    // 5. 进程恢复之后,重新设置SIGTSTP信号为sig_tstp信号捕捉程序
    signal(SIGTSTP, sig_tstp);
    // 6. 重新设置tty模式,渲染屏幕
}
int main()
{
    int n;
    char buf[BUFFSIZE];

    // 仅在支持作业控制的shell中捕捉SIGTSTP信号
    // 如何判断是否支持作业控制呢
    // shell进程是由init或者systemd进程拉起的,如果shell不支持作业控制,这两个进程会将SIGTSTP的动作设置为忽略,否则为默认
    if (signal(SIGTSTP, SIG_IGN) == SIG_DFL)
    {
        signal(SIGTSTP, sig_tstp);
    }
    // 其他动作
}

信号名与编号

函数psignal可以打印与信号编号对应的字符串,类似于perror函数。

#include <signal.h>

void psignal(int sig, const char *s);

函数strsignal可以返回与信号编号对应的字符串,类似于strerror函数。

#include <string.h>
char *strsignal(int sig);
// 返回值:失败返回NULL

示例如下

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

int main()
{
    psignal(SIGINT, "signal");
    printf("%s\n", strsignal(SIGINT));
}
// $ ./signo2str
// signal: Interrupt
// Interrupt