在Linux中,信号是进程间通讯的一种方式,它采用的是异步机制。当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。

需要说明的是,信号只是用于通知进程发生了某个事件,除了信号本身的信息之外,并不具备传递用户数据的功能。

1 信号的响应动作

每个信号都有自己的响应动作,当接收到信号时,进程会根据信号的响应动作执行相应的操作,信号的响应动作有以下几种:

  • 中止进程(Term)
  • 忽略信号(Ign)
  • 中止进程并保存内存信息(Core)
  • 停止进程(Stop)
  • 继续运行进程(Cont)

用户可以通过signalsigaction函数修改信号的响应动作(也就是常说的“注册信号”,在文章的后面会举例说明)。另外,在多线程中,各线程的信号响应动作都是相同的,不能对某个线程设置独立的响应动作。

2 信号类型

Linux支持的信号类型可以参考下面给出的列表。

2.1 在POSIX.1-1990标准中的信号列表

信号 动作 说明
SIGHUP 1 Term 终端控制进程结束(终端连接断开)
SIGINT 2 Term 用户发送INTR字符(Ctrl+C)触发
SIGQUIT 3 Core 用户发送QUIT字符(Ctrl+/)触发
SIGILL 4 Core 非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT 6 Core 调用abort函数触发
SIGFPE 8 Core 算术运行错误(浮点运算错误、除数为零等)
SIGKILL 9 Term 无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV 11 Core 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
SIGPIPE 13 Term 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
SIGALRM 14 Term 时钟定时信号
SIGTERM 15 Term 结束程序(可以被捕获、阻塞或忽略)
SIGUSR1 30,10,16 Term 用户保留
SIGUSR2 31,12,17 Term 用户保留
SIGCHLD 20,17,18 Ign 子进程结束(由父进程接收)
SIGCONT 19,18,25 Cont 继续执行已经停止的进程(不能被阻塞)
SIGSTOP 17,19,23 Stop 停止进程(不能被捕获、阻塞或忽略)
SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略)
SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发
SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发

:其中SIGKILLSIGSTOP信号不能被捕获、阻塞或忽略。

2.2 在SUSv2和POSIX.1-2001标准中的信号列表

信号 动作 说明
SIGTRAP 5 Core Trap指令触发(如断点,在调试器中使用)
SIGBUS 0,7,10 Core 非法地址(内存地址对齐错误)
SIGPOLL Term Pollable event (Sys V). Synonym for SIGIO
SIGPROF 27,27,29 Term 性能时钟信号(包含系统调用时间和进程占用CPU的时间)
SIGSYS 12,31,12 Core 无效的系统调用(SVr4)
SIGURG 16,23,21 Ign 有紧急数据到达Socket(4.2BSD)
SIGVTALRM 26,26,28 Term 虚拟时钟信号(进程占用CPU的时间)(4.2BSD)
SIGXCPU 24,24,30 Core 超过CPU时间资源限制(4.2BSD)
SIGXFSZ 25,25,31 Core 超过文件大小资源限制(4.2BSD)

:在Linux 2.2版本之前,SIGSYSSIGXCPUSIGXFSZ以及SIGBUS的默认响应动作为Term,Linux 2.4版本之后这三个信号的默认响应动作改为Core。

2.3 其它信号

信号 动作 说明
SIGIOT 6 Core IOT捕获信号(同SIGABRT信号)
SIGEMT 7,-,7 Term 实时硬件发生错误
SIGSTKFLT -,16,- Term 协同处理器栈错误(未使用)
SIGIO 23,29,22 Term 文件描述符准备就绪(可以开始进行输入/输出操作)(4.2BSD)
SIGCLD -,-,18 Ign 子进程结束(由父进程接收)(同SIGCHLD信号)
SIGPWR 29,30,19 Term 电源错误(System V)
SIGINFO 29,-,- 电源错误(同SIGPWR信号)
SIGLOST -,-,- Term 文件锁丢失(未使用)
SIGWINCH 28,28,20 Ign 窗口大小改变时触发(4.3BSD, Sun)
SIGUNUSED -,31,- Core 无效的系统调用(同SIGSYS信号)

注意:列表中有的信号有三个值,这是因为部分信号的值和CPU架构有关,这些信号的值在不同架构的CPU中是不同的,三个值的排列顺序为:1,Alpha/Sparc;2,x86/ARM/Others;3,MIPS。

例如SIGSTOP这个信号,它有三种可能的值,分别是17、19、23,其中第一个值(17)是用在Alpha和Sparc架构中,第二个值(19)用在x86、ARM等其它架构中,第三个值(23)则是用在MIPS架构中的。

3 信号机制

文章的前面提到过,信号是异步的,这就涉及信号何时接收、何时处理的问题。

我们知道,函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换,过程可以先看一下下面的示意图:

信号处理机制示意图

接下来围绕示意图,将信号分成接收、检测和处理三个部分,逐一讲解每一步的处理流程。

3.1 信号的接收

接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。

注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。

3.2 信号的检测

进程陷入内核态后,有两种场景会对信号进行检测:

  • 进程从内核态返回到用户态前进行信号检测
  • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测

当发现有新信号时,便会进入下一步,信号的处理。

3.3 信号的处理

信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。

接下来进程返回到用户态中,执行相应的信号处理函数。

信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。

至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。

4 信号的使用

4.1 发送信号

用于发送信号的函数有raisekillkillpgpthread_killtgkillsigqueue,这几个函数的含义和用法都大同小异,这里主要介绍一下常用的raisekill函数。

raise函数:向进程本身发送信号

函数声明如下:

#include <signal.h>

int raise(int sig);

函数功能是向当前程序(自身)发送信号,其中参数sig为信号值。

kill函数:向指定进程发送信号

函数声明如下:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

函数功能是向特定的进程发送信号,其中参数pid为进程号,sig为信号值。

在这里的参数pid,根据取值范围不同,含义也不同,具体说明如下:

  • pid > 0 :向进程号为pid的进程发送信号
  • pid = 0 :向当前进程所在的进程组发送信号
  • pid = -1 :向所有进程(除PID=1外)发送信号(权限范围内)
  • pid < -1 :向进程组号为-pid的所有进程发送信号

另外,当sig值为零时,实际不发送任何信号,但函数返回值依然有效,可以用于检查进程是否存在。

4.2 等待信号被捕获

等待信号的过程,其实就是将当前进程(线程)暂停,直到有信号发到当前进程(线程)上并被捕获,函数有pausesigsuspend

pause函数:将进程(或线程)转入睡眠状态,直到接收到信号

函数声明如下:

#include <unistd.h>

int pause(void);

该函数调用后,调用者(进程或线程)会进入睡眠(Sleep)状态,直到捕获到(任意)信号为止。该函数的返回值始终为-1,并且调用结束后,错误代码(errno)会被置为EINTR。

sigsuspend函数:将进程(或线程)转入睡眠状态,直到接收到特定信号

函数声明如下:

#include <signal.h>

int sigsuspend(const sigset_t *mask);

该函数调用后,会将进程的信号掩码临时修改(参数mask),然后暂停进程,直到收到符合条件的信号为止,函数返回前会将调用前的信号掩码恢复。该函数的返回值始终为-1,并且调用结束后,错误代码(errno)会被置为EINTR。

4.3 修改信号的响应动作

用户可以自己重新定义某个信号的处理方式,即前面提到的修改信号的默认响应动作,也可以理解为对信号的注册,可以通过signalsigaction函数进行,这里以signal函数举例说明。

首先看一下函数声明:

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

第一个参数signum是信号值,可以从前面的信号列表中查到,第二个参数handler为处理函数,通过回调方式在信号触发时调用。

下面为示例代码:

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

/* 信号处理函数 */
void sig_callback(int signum) {
    switch (signum) {
        case SIGINT:
            /* SIGINT: Ctrl+C 按下时触发 */
            printf("Get signal SIGINT. \r\n");
            break;
        /* 多个信号可以放到同一个函数中进行 通过信号值来区分 */
        default:
            /* 其它信号 */
            printf("Unknown signal %d. \r\n", signum);
            break;
    }

    return;
}

/* 主函数 */
int main(int argc, char *argv[]) {
    printf("Register SIGINT(%u) Signal Action. \r\n", SIGINT);

    /* 注册SIGINT信号的处理函数 */
    signal(SIGINT, sig_callback);

    printf("Waitting for Signal ... \r\n");

    /* 等待信号触发 */
    pause();

    printf("Process Continue. \r\n");

    return 0;
}

源文件下载:链接

例子中,将SIGINT信号(Ctrl+C触发)的动作接管(打印提示信息),程序运行后,按下Ctrl+C,命令行输出如下:

./linux_signal_example
Register SIGINT(2) Signal Action. 
Waitting for Signal ... 
^CGet signal SIGINT. 
Process Continue.

进程收到SIGINT信号后,触发响应动作,将提示信息打印出来,然后从暂停的地方继续运行。这里需要注意的是,因为我们修改了SIGINT信号的响应动作(只打印信息,不做进程退出处理),所以我们按下Ctrl+C后,程序并没有直接退出,而是继续运行并将"Process Continue."打印出来,直至程序正常结束。


Comments