Linux C 编程基础之信号机制(一)

之前的一篇文章中简单分析了 Java 程序和 Linux 信号机制的处理。其中提到了 Linux 对信号的处理,但是感觉对这部分的理解还是不行,经过重新分析学习后,在这里进行记录。

基本前提

什么是信号

首先看,什么是信号:

信号(signal)是一种软件中断,它提供了一种处理异步事件的方法,也是进程间唯一的一步通信方式。

这个是《Linux C 编程实战》的一段描述,要注意的就是不能直接说信号就是一种中断,只能说信号的响应是依赖于中断的。以时间片轮转为例,如果时间片结束时进程还在运行,那么 CPU 将剥夺这个进程的执行并分配给另一个进程,那么是如何知道“时间片结束”呢,这可以理解为中断的一种形式。我们还知道处理异步事件也可以使用线程,但是信号属于初步异步,而线程属于完全异步。

信号的种类

避免 OS 的影响,我在 CentOS 的虚拟机中输入 kill -l

[root@localhost ~]# 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

编号 1~31 的信号都是属于标准信号,继承自 UNIX 系统,也是不可靠信号。编号 34~64 的信号,从名字上可以看出是“RT+MIN+X”和“RT+MAX-X”,这里的 RT 就是 real time 的意思,即实时信号。

这里截取了 UNIX 系统的部分信号:

在这里插入图片描述

可以看到默认动作最多的就是“终止”和“终止+core”,这里的 core 指的就是在 Unix 系统下,应用程序崩溃产生的 core 文件,我们比较常见的“错误报告”其实就是一种 core 文件。

信号发生时,用户可以设置以下三种方式对信号做出响应:

  • 捕捉信号:对于要捕捉的信号,可以为其指定信号处理函数,信号发生时该函数自动被调用,在该函数内部实现对该信号的处理
  • 忽略信号:大多数信号都可使用这种方式进行处理,但是 SIGKILL 和 SIGSTOP 这两个信号不能被忽略,同时这两个信号也不能被捕获和阻塞。此外,如果忽略某某些由硬件异常产生的信号(如非法存储访问或除以0),则进程的行为是不可预测的。
  • 按照系统默认方式处理。大部分信号的默认操作是终止进程,且所有的实时信号的默认动作都是终止进程。

signal 函数

signal 函数原型如下:

 #include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

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

第一个参数是要处理的信号,当指定菜单信号到达时就会跳转到 handler 指定的函数执行,如果 handler 不是函数指针,那么还有两种方式处理:

  • SIG_DFL:由该特定信号的默认动作处理
  • SIG_IGN:忽略该信号,即使没有意义,代码执行仍将继续

函数执行成功会返回以前的信号处理函数指针,执行错误会返回 SIG_ERR(-1)。

先看一个简单的例子:

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

int main()
{
    while (1){
      sleep(1);
      printf("Hello World\n");
    };    
    //返回给 OS,0 是正常,非 0 是异常 
    return 0;
}

执行:

➜  ctemp ls
hello.c
➜  ctemp make hello  
cc  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib  hello.c   -o hello
hello.c:8:7: warning: implicit declaration of function 'sleep' is invalid in C99 [-Wimplicit-function-declaration]
      sleep(1);
      ^
1 warning generated.
➜  ctemp 
➜  ctemp ls
hello   hello.c
➜  ctemp ./hello

这时候会每隔 1s 打印"Hello World",此时如果在键盘输入 <control+c>

➜  ctemp ./hello 
Hello World
Hello World
Hello World
Hello World
^C

可以发现程序被终止了。结合上文中的信号表,能够知道 SIGINT 信号的默认处理是终止,接下来使用 signal 函数修改一下:

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

int main()
{
    while (1){
      signal(SIGINT,SIG_IGN);
      sleep(1);
      printf("Hello World\n");
    };    
    //返回给 OS,0 是正常,非 0 是异常 
    return 0;
}

再执行:

➜  ctemp make hello
cc  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib  hello.c   -o hello
hello.c:9:7: warning: implicit declaration of function 'sleep' is invalid in C99 [-Wimplicit-function-declaration]
      sleep(1);
      ^
1 warning generated.
➜  ctemp ./hello   
Hello World
Hello World
^C^CHello World
^C^C^C^C^C^CHello World
^CHello World
Hello World
Hello World

能够发现此时已经忽略了 SIGINT 信号。再使用之前的一篇文章中的例子:

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

void handler_sigint(int signo)
{
    printf("Hello World\n");
}

int main()
{
    //注册
    signal(SIGINT,handler_sigint);
    while (1);
    
    //返回给 OS,0 是正常,非 0 是异常 
    return 0;
}

执行:

➜  ctemp make hello2
cc  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib  hello2.c   -o hello2
➜  ctemp ./hello2 
^CHello World
^CHello World
^CHello World
^CHello World

能够看到此时程序在收到 SIGINT 信号的时候会响应输出"Hello World"。

还有一个非常重要的就是信号会打断阻塞的系统调用。先看一个例子:

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

static void sig_handler(int s)
{
    printf("Hello\n");
}

int main()
{
    int i ;
    signal(SIGINT,sig_handler);
    for(i = 0 ; i < 10 ; i++)
    {
        write(1,"*",1);
        sleep(1);
    }
}

运行:

➜  ctemp make hello3
cc  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib  hello3.c   -o hello3
hello3.c:16:9: warning: implicit declaration of function 'write' is invalid in
      C99 [-Wimplicit-function-declaration]
        write(1,"*",1);
        ^
hello3.c:17:9: warning: implicit declaration of function 'sleep' is invalid in
      C99 [-Wimplicit-function-declaration]
        sleep(1);
        ^
2 warnings generated.
➜  ctemp ./hello3   
****^CHello
*^CHello
*^CHello
*^CHello
*^CHello
*^CHello
*^CHello

理论上来说程序总共应该要运行 10s,每秒打印一个“*”,是如果我连续的在键盘输入 <control+c>,会发现程序虽然输出了 10 个"*",但是程序总共运行的时间并没有 10s,再看一个更直接的例子:

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

static void sig_handler(int s)
{
    printf("Hello\n");
}

char buf[10];
int main()
{
    int i ;
    signal(SIGINT,sig_handler);
    for(i = 0 ; i < 10 ; i++)
    {
        write(1,"*",1);      
        sprintf(buf, "[%d]", sleep(3));
        write(1,buf,3);
    }
}

sleep 函数会返回剩余秒数。运行:

➜  ctemp ./hello4   
*^CHello
[2]*^CHello
[2]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]*^CHello
[3]

可以看到会出现压根就没等待的情况。很多阻塞函数都会被信号打断,比如 read 函数:

 EINTR  While blocked waiting to complete an open of a slow device (e.g., a  FIFO;  see  fifo(7)),the call was interrupted by a signal handler; see signal(7).

References

  • 《Linux C 编程实战》
  • 《UNIX 环境高级编程(第3版)》

欢迎关注公众号
​​​​​​在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章