Linux进程间通信——信号

3,290 阅读8分钟

一、认识信号

信号(Signals)是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。也可以简单理解为信号是某种形式上的软中断。

二、信号来源

一般情况下,信号的来源可分为以下三种:

  • 硬件方式:除数为零、无效的存储访问等硬件异常产生信号。这些事件通常由硬件(如:CPU)检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时正在进行的程序。
  • 软件方式:用户在终端下调用kill命令向进程发送任务信号、进程调用killsigqueue函数发送信号、当检测到某种软件条件已经具备时发出信号,如由alarmsettimer设置的定时器超时时将生成SIGALRM信号等多种情景均可产生信号。
  • 键盘输入:当用户在终端上按下某键时,将产生信号。如按下组合键Ctrl+C将产生一个SIGINT信号,Ctrl+\产生一个SIGQUIT信号等。

三、信号种类

可运行kill -l查看Linux支持的信号列表:

sl@Li:~/Works$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

可以看到Linux中系统一共支持64种信号,其中1到31号信号为普通信号(也程为不可靠信号),34到64为实时信号(可靠信号)。

可靠信号与不可靠信号的区别:

  • 这里的不可靠主要是不支持信号队列,就是当多个信号发生在进程中的时候(收到信号的速度超过进程处理的速度的时候),这些没来的及处理的信号就会被丢掉,仅仅留下一个信号。
  • 可靠信号是多个信号发送到进程的时候(收到信号的速度超过进程处理信号的速度的时候),这些没来的及处理的信号就会排入进程的队列。等进程有机会来处理的时候,依次再处理,信号不丢失。

以下列出几个常用的信号:

信号 描述
SIGHUP 当用户退出终端时,由该终端开启的所有进程都退接收到这个信号,默认动作为终止进程。
SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。
SIGQUIT SIGINT类似, 但由QUIT字符(通常是Ctrl+\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略
SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。
SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

四、可重入函数

要注意信号处理函数的可重入问题,因为信号处理函数执行过程中有可能被其他信号再次中断,这时程序会跳到另一个信号的处理函数中,处理完成后再次返回当前处理函数,编写自己定义的信号处理函数的时候一定要注意这一点。信号可以理解为“软中断”,这样可重入函数就很好理解了。

五、信号的捕获

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。 所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当进程接收到一个它忽略的信号时,进程丢弃该信号,就像没有收到该信号似的继续运行。

如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。 这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

下面引用一个例子说明这个过程:

  1. 用户程序注册了SIGQUIT信号的处理函数sighandler
  2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
  3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
  4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandlermain函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
  5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
  6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

六、代码示例1

下面的代码收到程序退出信号后会执行用户定义的信号处理函数来替代系统默认的处理程序。

#include<stdlib.h>
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

void sig_handle(int sig) {
    printf("received signal: %d, quit.\n", sig);
    exit(0);
}

int main () {
    signal(SIGINT, sig_handle);
    signal(SIGKILL, sig_handle);
    signal(SIGSEGV, sig_handle);
    signal(SIGTERM, sig_handle);

    int i = 0;
    while (1) {
        printf("%d\n", ++i);
        sleep(2);
    }

    printf("main quit.");

    return 0;
}

运行结果:

1
2
received signal: 15, quit.

七、代码示例2

这段代码功能与上面的例子差不多,信号也可以传参,但是更多的是通知功能,可传递的信息非常有限,如果需要传递大量的信息,可以考虑其他进程间通信方式。

#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>

void new_op(int, siginfo_t*, void*);

int main() {
    if (NULL == freopen("sigproc.log", "w", stdout)) {
        fprintf(stderr, "error redirecting stdout\n");
    }

    struct sigaction act;

    sigemptyset(&act.sa_mask);  //sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送
    sigaddset(&act.sa_mask, SIGTERM);
    sigaddset(&act.sa_mask, SIGINT);
    act.sa_flags = SA_SIGINFO;  //SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中
    act.sa_sigaction = new_op;

    if (sigaction(SIGINT, &act, NULL) < 0) {
        printf("install sigal error\n");
    }

    if (sigaction(SIGTERM, &act, NULL) < 0) {
        printf("install sigal error\n");
    }

    if (sigaction(SIGHUP, &act, NULL) < 0) {
        printf("install sigal error\n");
    }

    int i = 0;
    while (1) {
        printf("%d\n", ++i);
        sleep(1);
    }

    printf("end.");

    return 0;
}

void new_op(int signum, siginfo_t *info, void *myact) {
    printf("receive signal %d\n", signum);
    for (int i = 0; i < 5; ++i) {
        printf("signal processing: %d\n", i);
        sleep(1);
    }
    printf("process quit.");

    exit(0);
}

运行结果:

1
2
3
receive signal 15
signal processing: 0
signal processing: 1
signal processing: 2
signal processing: 3
signal processing: 4
process quit.

参考文档:
信号
Linux环境进程间通信(二)信号(上)
Linux环境进程间通信(二)信号(下)

最后,欢迎关注微信公众号,Let's go!