2. 信号

信号是Linux系统中 进程间异步通信 的核心机制,也被称作软中断,用于内核或进程向目标进程通知异步事件、触发应急处理, 无需进程主动轮询等待,是Linux进程管理、异常处理、进程协作的重要手段。 相较于管道、消息队列等同步通信方式,信号的异步特性使其能快速响应系统异常、终端操作等突发场景。

2.1. 信号的基本概念

2.1.1. 概述

信号(signal),又称为软中断信号,用于通知进程发生了异步事件, 它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟, 是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上, 进程也不知道信号到底什么时候到达。正如我们所了解的中断服务函数一样,在中断发生的时候, 就会进入中断服务函数中去处理,同样的,当进程接收到一个信号的时候,也会相应地采取一些行动。 我们可以使用术语“生成(raise)”表示一个信号的产生,使用术语“捕获(catch)”表示进程接收到一个信号。

2.1.2. 信号的触发场景

信号的触发场景主要分为三类,覆盖系统异常、用户操作、进程协作全场景:

  • 系统异常触发:进程执行非法指令、访问非法内存、除零运算等硬件/内核检测到的错误,内核主动发送信号终止/通知进程;

  • 用户终端操作:通过终端快捷键(Ctrl+C、Ctrl+、Ctrl+Z)、kill命令向进程发送信号;

  • 进程主动发送:进程通过系统调用(kill、raise、sigqueue等)向自身或其他进程发送信号,实现进程间协作。

2.1.3. 系统支持的信号

我们可以使用kill 命令来查看系统中支持的信号种类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#查看系统中支持的信号种类
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系统支持信号62种信号,每种信号名称都以SIG三个字符开头, 注意,编号为32和33的信号值是不存在的。

可以将这62种信号分为两大类:信号值为1~31的信号属于非实时信号(也称为不可靠信号), 它们是从UNIX系统中继承下来的信号,具体的作用见下表, 信号值为34~64的信号为实时信号(也称为可靠信号)。

Linux信号种类与描述:

Linux信号速查表

信号值

名称

描述

默认处理

1

SIGHUP

控制终端关闭时触发

终止

2

SIGINT

Ctrl+C 中断前台进程

终止

3

SIGQUIT

Ctrl+退出,生成 core

终止+core

4

SIGILL

执行非法指令

终止+core

5

SIGTRAP

调试断点/陷阱触发

终止+core

6

SIGABRT

调用 abort() 主动退出

终止+core

7

SIGBUS

内存地址对齐/总线错误

终止+core

8

SIGFPE

算术错误(除0、溢出等)

终止+core

9

SIGKILL

强制杀死进程,不可捕获

终止

10

SIGUSR1

用户自定义信号1

终止

11

SIGSEGV

非法内存访问(段错误)

终止

12

SIGUSR2

用户自定义信号2

终止

13

SIGPIPE

向无读端的管道/Socket 写数据

终止

14

SIGALRM

alarm() 定时器超时

终止

15

SIGTERM

正常终止信号,可捕获处理

终止

16

SIGSTKFLT

已废弃

终止

17

SIGCHLD

子进程退出/暂停,通知父进程

忽略

18

SIGCONT

让暂停的进程继续运行

恢复运行

19

SIGSTOP

强制暂停进程,不可捕获

暂停

20

SIGTSTP

Ctrl+Z 暂停前台进程

暂停

21

SIGTTIN

后台进程尝试读终端

暂停

22

SIGTTOU

后台进程尝试写终端

暂停

23

SIGURG

Socket 有紧急数据

忽略

24

SIGXCPU

进程 CPU 时间超出限制

终止+core

25

SIGXFSZ

文件大小超出限制

终止+core

26

SIGVTALRM

进程虚拟定时器超时

终止

27

SIGPROF

进程性能统计定时器超时

终止

28

SIGWINCH

终端窗口大小改变

忽略

29

SIGIO

文件描述符可 I/O

终止

30

SIGPWR

电源异常

终止

31

SIGUNUSED

未使用/无效系统调用

终止+core

对于以上表格,有几点需要注意的地方:

  • 信号的“值”在x86、PowerPC和ARM平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。

  • “描述”中注明的一些情况发生时会产生相应的信号,但并不代表该信号的产生就一定发生了这个事件。 事实上,任何进程都可以使用kill()函数来产生任何信号。

  • 信号SIGKILL和SIGSTOP是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作来响应。

2.2. 信号的处理流程

信号从产生到处理完毕,需经历产生->未决->递送->处理四个阶段,内核通过PCB中的信号位图记录信号状态,核心流程如下:

  1. 信号产生:内核/进程触发信号,标记目标进程的信号位图;

  2. 信号未决:信号已产生但未被递送,若信号被阻塞,会一直处于未决状态直至阻塞解除;

  3. 信号递送:内核解除阻塞后,将信号递送给目标进程;

  4. 信号处理:进程按预设逻辑处理信号,分为三种处理模式:

  • 默认处理(SIG_DFL):执行内核预设行为(终止、暂停、忽略等);

  • 忽略信号(SIG_IGN):直接丢弃信号,不做任何处理;

  • 自定义捕获:执行用户注册的信号处理函数,完成定制化逻辑。

2.3. 非实时信号与实时信号

Linux 把信号分成两大类,完全按信号编号划分:

  • 非实时信号(不可靠信号):1 ~ 31

  • 实时信号(可靠信号):34 ~ 64

核心区别:

非实时信号与实时信号核心区别

对比项

非实时信号(1~31)

实时信号(34~64)

信号丢失

可能丢失,多次发送会合并

不会丢失,支持排队

投递顺序

不保证顺序

严格 FIFO,先到先处理

携带数据

不能携带自定义数据

可通过 sigqueue 携带数据

可靠性

不可靠信号

可靠信号

典型用途

系统异常、终端控制、进程退出

进程间实时通信、高精度事件通知

2.4. 捕获信号相关API

Linux提供两类核心API:信号处理API(注册信号处理逻辑)、信号发送API(主动触发信号), 同时补充信号集、阻塞控制等进阶API,覆盖全场景开发需求。

以下介绍信号处理相关API:

2.4.1. signal函数

signal函数主要是用于捕获信号,可以改变进程中对信号的默认行为,我们在捕获这个信号后, 也可以自定义对信号的处理行为,当收到这个信号后,应该如何去处理它, 这也是我们在Linux开发最常需要做的事。

使用signal函数时,它需要提前设置一个回调函数,即进程接收到信号后将要跳转执行的响应函数, 或者设置忽略某个信号,才能改变信号的默认行为,这个过程称为“信号的捕获”。 对一个信号的“捕获”可以重复进行,不过signal函数将会返回前一次设置的信号响应函数指针。

signal函数原型如下:

1
2
3
4
5
#include <signal.h>

// 函数原型
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

这个相当复杂的函数定义说明,signal是一个带有signum和handler两个参数的函数。 准备捕获或忽略的信号由参数signum指出,接收到指定的信号后将要调用的函数由参数handler指出。

signum是指定捕获的信号名称,如果指定的是一个无效的信号, 或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL。

handler是一个函数指针,它的类型是 void(*sighandler_t)(int) 类型,拥有一个int类型的参数, 这个参数的作用就是传递收到的信号值,返回类型为void。

signal()函数会返回一个sighandler_t类型的函数指针,这是因为调用signal()函数修改了信号的行为, 需要返回之前的信号处理行为是哪个,以便让应用层知悉, 如果修改信号的默认行为识别则返回对应的错误代码SIG_ERR。

handler需要用户自定义处理信号的方式,当然还可以使用以下宏定义:

  • SIG_IGN:忽略该信号。

  • SIG_DFL:采用系统默认方式处理信号。

还需要注意一下的:如果调用处理程序导致信号被阻塞,则从处理程序返回后, 信号将被解除阻塞。无法捕获或忽略信号SIGKILL和SIGSTOP。

2.4.1.1. 实验分析

我们可以使用这个函数做个小实验,代码如下:

signal()函数示例(base_linux/system_programing/signal/sources/signal.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

/** 信号处理函数 */
void signal_handler(int sig)            //(3)
{
    printf("\nthis signal number is %d \n",sig);

    if (sig == SIGINT) {
        printf("I have get SIGINT!\n\n");
        printf("The signal has been restored to the default processing mode!\n\n");
        /** 恢复信号为默认情况 */
        signal(SIGINT, SIG_DFL);        //(4)
    }

}

int main(void)
{
    printf("\nthis is an singal test function\n\n");

    /** 设置信号处理的回调函数 */
    signal(SIGINT, signal_handler);         //(1)

    while (1) {
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n");
        sleep(1);                           //(2)
    }

    exit(0);
}

解析一下这段代码:(先从第23行的main函数开始)。

  • (1) :使用signal()函数捕获SIGINT信号(这个信号可以通过按下 CTRL+C 产生), 并设置回调函数为signal_handler(),当产生信号的时候就调用该函数去处理这个信号。

  • (2) :在信号没有到来的时候就打印信息并且休眠。

  • (3) :signal_handler()是信号处理函数,它传入一个int类型的信号值, 在信号传递进来的时候就将对应的信号值打印出来,在此例中我们可以看到, 信号处理函数使用了一个单独的整数参数,它就是引起该函数被调用的信号值。 如果需要在同一个函数中处理多个信号,这个参数就很有用。

  • (4) :如果信号是SIGINT,则打印对应的信息,并且调用signal()函数将SIGINT信号的处理恢复默认的处理(SIG_DFL), 在下一次接收到SIGINT信号的时候就不会进入这个函数里了。

本实例代码在system_programing/signal目录下, signal.c文件中包含了上述信号默认处理的示例,编译前注意通过宏去切换为本小节的示例代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 以下操作在 system_programing/signal代码目录进行
make

# 运行
./build/signal_demo

# 按 Ctrl+C 键发送 SIGINT 信号,signal_handler将会捕获到该信号输出信息并处理
# 再按一次  Ctrl+C 键发送 SIGINT 信号,Linux将按默认方式处理,终止进程

#信息输出如下
this is an singal test function

waiting for the SIGINT signal , please enter "ctrl + c"...
^C
this signal number is 2
I have get SIGINT!

The signal has been restored to the default processing mode!

waiting for the SIGINT signal , please enter "ctrl + c"...
^C

当我们按下“CTRL+C”时,进入signal_handler()信号处理函数,打印对应的信息, 并且将SIGINT信号的处理恢复默认,因此当下一次按下“CTRL+C”时进程将直接退出。

2.4.2. sigaction函数

其实,我们不推荐读者使用signal()函数接口,之所以会在上一小节介绍它, 是因为读者可能会在许多老程序中看到它的应用,而且相对简单。 以下介绍一个定义更清晰、执行更可靠的sigaction()函数, 这个函数的功能与signal()函数是一样的,但是API接口稍微有点不同, 我们建议以后在所有的程序中都应该使用这个函数去操作信号。

sigaction()函数原型如下:

1
2
3
4
#include <signal.h>

// 函数原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

这个函数的参数比signal()函数多了一些,参数区别如下:

  • signum:指定捕获的信号值。

  • act:是一个结构体,该结构体的内容如下:

    struct sigaction {
                void     (*sa_handler)(int);
                void     (*sa_sigaction)(int, siginfo_t *, void *);
                sigset_t   sa_mask;
                int        sa_flags;
                void     (*sa_restorer)(void);
            };
    
    • sa_handler是一个函数指针,是捕获信号后的处理函数,它也有一个int类型的参数,传入信号的值,这个函数是标准的信号处理函数。

    • sa_sigaction则是扩展信号处理函数,它也是一个函数指针,但它比标准信号处理函数复杂的多, 事实上如果选择扩展接口的话,信号的接收进程不仅可以接收到int型的信号值, 还会接收到一个 siginfo_t类型的结构体指针,还有一个void类型的指针,还有需要注意的就是, 不要同时使用sa_handler和sa_sigaction,因为这两个处理函数是有联合的部分(联合体)。关于siginfo_t类型的结构体我们在后续讲解。

    • sa_mask是信号掩码,它指定了在执行信号处理函数期间阻塞的信号的掩码,被设置在该掩码中的信号, 在进程响应信号期间被临时阻塞。除非使用SA_NODEFER标志,否则即使是当前正在处理的响应的信号再次到来的时候也会被阻塞。

    • re_restorer则是一个已经废弃的成员变量,不要使用。

    • sa_flags是指定一系列用于修改信号处理过程行为的标志,由下面的0个或多个标志组合而成:

      • SA_NOCLDSTOP:如果signum是SIGCHLD,则在子进程停止或恢复时,不会传信号给调用sigaction()函数的进程。 即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU(停止)中的一种时或接收到SIGCONT(恢复)时, 父进程不会收到通知。仅当为SIGCHLD建立处理程序时,此标志才有意义。

      • SA_NOCLDWAIT:它表示父进程在它的子进程终止时不会收到SIGCHLD 信号, 这时子进程终止则不会成为僵尸进程。

      • SA_NODEFER:不要阻止从其自身的信号处理程序中接收信号,使进程对信号的屏蔽无效, 即在信号处理函数执行期间仍能接收这个信号,仅当建立信号处理程序时,此标志才有意义。

      • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。

      • SA_SIGINFO:指示使用sa_sigaction成员而不是使用sa_handler 成员作为信号处理函数。

      当在asa_flags中指定SA_SIGINFO标志时,信号处理程序地址将通过sa_sigaction字段传递。该处理程序采用三个参数,如下所示:

      void handler(int sig, siginfo_t *info, void *ucontext)
      {
      
          ...
      
      }
      

      info指向siginfo_t的指针,它是一个包含有关信号的更多信息的结构,具体成员变量如下所示:

      siginfo_t {
                  int      si_signo;     /* 信号数值 */
                  int      si_errno;     /* 错误值 */
                  int      si_code;      /* 信号代码 */
                  int      si_trapno;   /*导致硬件生成信号的陷阱号,在大多数体系结构中未使用*/
                  pid_t    si_pid;       /* 发送信号的进程ID */
                  uid_t    si_uid;       /*发送信号的真实用户ID */
                  int      si_status;    /* 退出值或信号状态*/
                  clock_t  si_utime;     /*消耗的用户时间*/
                  clock_t  si_stime;     /*消耗的系统时间*/
                  sigval_t si_value;     /*信号值*/
                  int      si_int;       /* POSIX.1b 信号*/
                  void    *si_ptr;
                  int      si_overrun;   /*计时器溢出计数*/
                  int      si_timerid;   /* 计时器ID */
                  void    *si_addr;      /*导致故障的内存位置 */
                  long     si_band;
                  int      si_fd;        /* 文件描述符*/
                  short    si_addr_lsb;  /*地址的最低有效位 (从Linux 2.6.32开始存在) */
                  void    *si_lower;     /*地址冲突时的下限*/
                  void    *si_upper;     /*地址冲突时的上限 (从Linux 3.19开始存在) */
                  int      si_pkey;      /*导致的PTE上的保护密钥*/
                  void    *si_call_addr; /*系统调用指令的地址*/
                  int      si_syscall;   /*尝试的系统调用次数*/
                  unsigned int si_arch;  /* 尝试的系统调用的体系结构*/
              }
      

      上面的成员变量绝大部分我们是几乎使用不到的,因为我们如果是对信号的简单处理,直接使用sa_handler处理即可, 根本无需配置siginfo_t这些比较麻烦的信息。

  • oldact:返回原有的信号处理参数,一般设置为NULL即可。

2.4.2.1. 实验分析

sigaction看起来复杂,直接分析源码能有更清晰的认识。

sigaction示例(base_linux/system_programing/sigaction/sources/sigaction.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

/** 信号处理函数 */
void signal_handler(int sig)                    //(1)
{
    printf("\nthis signal number is %d \n",sig);

    if (sig == SIGINT) {
        printf("I have get SIGINT!\n\n");
        printf("The signal is automatically restored to the default handler!\n\n");
        /** 信号自动恢复为默认处理函数 */
    }

}

int main(void)
{
    struct sigaction act;

    printf("this is sigaction function test demo!\n\n");

    /** 设置信号处理的回调函数 */
    act.sa_handler = signal_handler;            //(2)

    /* 清空屏蔽信号集 */
    sigemptyset(&act.sa_mask);                  //(3)

    /** 在处理完信号后恢复默认信号处理 */
    act.sa_flags = SA_RESETHAND;                //(4)

    sigaction(SIGINT, &act, NULL);              //(5)

    while (1)
    {
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n\n");
        sleep(1);
    }

    exit(0);
}
  • (1) :信号处理函数signal_handler()与signal实验的信号处理函数几乎是一样的, 但是这里并没有在函数中让信号恢复默认处理,这是因为设置了sa_flags成员变量, 在处理完信号后自动恢复默认的处理。

  • (2) :设置信号处理的回调函数,在这个实验使用sa_handler作为信号处理成员变量而不是sa_sigaction。

  • (3) :调用sigemptyset()函数清空进程屏蔽的信号集,即在信号处理的时候不会屏蔽任何信号。

  • (4) :设置sa_flags成员变量为SA_RESETHAND,在处理完信号后恢复默认信号处理。

  • (5) :调用sigaction()函数捕获SIGINT信号。

本实例代码在system_programing/sigaction目录下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 以下操作在 system_programing/sigaction代码目录进行
make

# 运行程序
./build/sigaction_demo

# 按 Ctrl+C 键发送 SIGINT 信号,signal_handler将会捕获到该信号输出信息并处理
# 再按一次  Ctrl+C 键发送 SIGINT 信号,Linux将按默认方式处理,终止进程

#信息输出如下
this is sigaction function test demo!

waiting for the SIGINT signal , please enter "ctrl + c"...

waiting for the SIGINT signal , please enter "ctrl + c"...

waiting for the SIGINT signal , please enter "ctrl + c"...

^C
this signal number is 2
I have get SIGINT!

The signal is automatically restored to the default handler!

waiting for the SIGINT signal , please enter "ctrl + c"...

^C

当我们按下“CTRL+C”时,进入signal_handler()信号处理函数,打印对应的信息, 由于设置为处理后恢复默认,因此当下一次按下“CTRL+C”时进程将直接退出。

2.5. 发送信号相关API

前面的实验中我们通过“Ctrl+C”来发送了信号,在代码里, 可以通过调用kill()、 raise()、alarm()等信号发送函数,下面就依次对其进行介绍。

2.5.1. kill函数

kill()函数与kill系统命令一样,可以发送信号给进程或进程组,实际上, kill系统命令只是kill()函数的一个用户接口。 这里需要注意的是,它不仅可以中止进程(实际上发出SIGKILL信号),也可以向进程发送其他信号。

函数原型:

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

kill()函数的参数有两个,分别是pid与sig,还返回一个int类型的错误码。

  • pid的取值如下:

    • pid > 1:将信号sig发送到进程ID值为pid指定的进程。

    • pid = 0:信号被发送到所有和当前进程在同一个进程组的进程。

    • pid = -1:将sig发送到系统中所有的进程,但进程1(init)除外。

    • pid < -1:将信号sig发送给进程组号为-pid (pid绝对值)的每一个进程。

  • sig:要发送的信号值。

  • 函数返回值:

    • 0:发送成功。

    • 1:发送失败。

进程可以通过调用kill()函数向包括它本身在内的其他进程发送一个信号。 如果程序没有发送该信号的权限,对kill函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。

Kill()函数会在失败时返回-1并设置errno变量。失败的原因可能是:给定的信号无效(errno设置为INVAL)、 发送进程权限不够(errno设置为EPERM)、目标进程不存在(errno设置为ESRCH)等情况。

2.5.2. raise函数

raise()函数也是发送信号函数,不过与 kill()函数所不同的是, raise()函数只是进程向自身发送信号的,而没有向其他进程发送信号, 可以说kill(getpid(),sig)等同于raise(sig)。

函数原型:

1
2
#include <signal.h>
int raise(int sig);

raise()函数只有一个参数sig,它代表着发送的信号值,如果发送成功则返回0,发送失败则返回-1, 发送失败的原因主要是信号无效,因为它只往自身发送信号,不存在权限问题,也不存在目标进程不存在的情况。

2.5.2.1. 实验分析

我们来做个小实验,包含了raise与kill的示例,实验代码在野火提供资料的system_programing/kill目录下:

raise与kill函数示例(base_linux/system_programing/kill/sources/kill.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid;

    int ret;

    /* 创建一子进程 */
    if ((pid = fork()) < 0) {               // (1)
        printf("Fork error\n");
        exit(1);
    }

    if (pid == 0) {                          // (2)
        /* 在子进程中使用 raise()函数发出 SIGSTOP 信号,使子进程暂停 */
        printf("Child(pid : %d) is waiting for any signal\n\n", getpid());

        /** 子进程停在这里 */
        raise(SIGSTOP);                     // (3)

        exit(0);
    }

    else {                                  // (4)
        /** 等待一下,等子进程先执行 */
        sleep(1);

        /* 在父进程中收集子进程发出的信号(不阻塞),并调用 kill()函数进行相应的操作 */
        if ((waitpid(pid, NULL, WNOHANG)) == 0) {       // (5)
            /** 子进程还没退出,返回为0,就发送SIGKILL信号杀死子进程 */
            if ((ret = kill(pid, SIGKILL)) == 0) {
                printf("Parent kill %d\n\n",pid);       // (6)
            }
        }

        /** 一直阻塞直到子进程退出(杀死) */
        waitpid(pid, NULL, 0);              // (7)

        exit(0);
    }
}
  • (1) :fork启动一个子进程,如果返回值小于0(值为-1),则表示启动失败。

  • (2) :如果返回值为0,则表示此时运行的是子进程,打印相关信息。

  • (3) :在子进程中使用 raise()函数发出SIGSTOP信号,使子进程暂停。

  • (4) :而如果运行的是父进程,则等待一下,让子进程先执行。

  • (5) :在父进程中使用waitpid()函数收集子进程发出的信号(不阻塞)。

  • (6) :如果子进程还未退出,则调用kill()函数向子进程发送终止信号,子进程收到这个信号后会被杀死。

  • (7) :使用waitpid()函数回收子进程资源,如果子进程未终止,父进程则会一直阻塞等待,直到子进程终止。

本实例代码在system_programing/kill目录下,执行如下步骤进行实验。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 以下操作在 system_programing/kill代码目录进行
make

# 运行
./build/kill_demo

#信息输出如下
Child(pid : 11285) is waiting for any signal

Parent kill 11285

2.5.3. alarm函数

alarm()函数也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间seconds到时, 它就向进程发送SIGALARM信号。其函数原型如下:

unsigned int alarm(unsigned int seconds);

如果在seconds秒内再次调用了alarm()函数设置了新的闹钟,则新的设置将覆盖前面的设置, 即之前设置的秒数被新的闹钟时间取代。它的返回值是之前闹钟的剩余秒数,如果之前未设闹钟则返回0。 特别地,如果新的seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。

2.5.3.1. 实验分析

了解了alarm()函数的功能特性和返回值的特性后,我们就可以对其测试。 测试方向有两个:其一,测试常规只单独存在一个闹钟函数alarm()的程序; 其二,测试程序中包含多个alarm()闹钟函数。因此整理了下面两个程序,通过比较学习更有助于理解。

alarm()函数示例(base_linux/system_programing/alarm/sources/alarm.c文件)
1
2
3
4
5
6
7
8
int main()
{
    printf("\nthis is an alarm test function\n\n");
    alarm(5);
    sleep(20);
    printf("end!\n");
    return 0;
}

这个测试是为了验证SIGALRM信号的默认处理。

  • 实际上这个程序只是定义了一个时钟alarm(5),它的作用是让SIGALRM信号在经过5秒后传送给目前main()所在进程;

  • 接着又调用了sleep(20)让进程睡眠20秒的时间。

  • 当main()程序挂起5秒钟后,alarm产生了SIGALRM信号,由于我们没有做捕获处理, 系统会调用该信号的默认处理函数,即执行exit(0)函数直接终止进程,并且在终止的时候自动打印”Alarm clock”(闹钟)。

  • 由于执行默认处理函数后进程终止,代码自身的最后一句printf(“end!n”)代码是不会被执行的。

本实例代码在system_programing/alarm目录下, alarm.c文件中包含了上述alarm示例,编译前注意通过宏去切换为本小节的示例代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 编译前请先打开源文件通过宏去切换要试验的代码!!!
# 以下操作在 system_programing/alarm代码目录进行
make

# 运行
./build/alarm_demo

# 等待5秒后,终端将会输出“Alarm clock”(闹钟)并结束进程

#信息输出如下
this is an alarm test function

Alarm clock

接下来,再进行一个alam()函数覆盖配置实验。

alarm()覆盖示例(配套代码仓库/system_programing/alarm/sources/alarm.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main()
{
    unsigned int seconds;

    printf("\nthis is an alarm test function\n\n");

    seconds = alarm(20);

    printf("last alarm seconds remaining is %d! \n\n", seconds);

    printf("process sleep 5 seconds\n\n");
    sleep(5);

    printf("sleep woke up, reset alarm!\n\n");

    seconds = alarm(5);

    printf("last alarm seconds remaining is %d! \n\n", seconds);

    sleep(20);

    printf("end!\n");

    return 0;
}

这个alarm测试代码是为了验证多次设置alarm的时候,它会覆盖前一次的设置值。 代码的逻辑非常简单,首先调用alarm(20)函数设置在20秒后产生一个SIGALRM信号, 进程睡眠5秒后唤醒,再次设置alarm(5)函数在5秒后产生SIGALRM信号终止进程, 此时上一个alarm设置就被覆盖了,并且返回上一次设置的剩余的时间(15秒),覆盖配置后,进程还需要睡眠, 等待5秒后SIGALRM信号的到来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 编译前请先打开源文件通过宏去切换要试验的代码!!!
# 以下操作在 system_programing/alarm代码目录进行
make

# 运行
./build/alarm_demo
# 等待5秒后,终端将会输出“Alarm clock”(闹钟)并结束进程

#信息输出如下
this is an alarm test function

last alarm seconds remaining is 0!

process sleep 5 seconds

sleep woke up, reset alarm!

last alarm seconds remaining is 15!

Alarm clock

如果希望亲自对alarm信号处理,使用上一节的signal()或sigaction()函数捕获SIGALRM信号即可。

2.6. 信号阻塞与控制API

信号阻塞用于临时屏蔽指定信号,避免关键业务被信号打断,内核通过信号掩码(sigset_t)管理阻塞状态,核心API如下:

  • sigemptyset(&set):清空信号集,不阻塞任何信号;

  • sigfillset(&set):填满信号集,阻塞所有信号;

  • sigaddset(&set, sig):向信号集添加指定信号;

  • sigdelset(&set, sig):从信号集删除指定信号;

  • sigprocmask(how, &set, &oldset):设置进程信号掩码,how可选SIG_BLOCK(阻塞)、SIG_UNBLOCK(解除阻塞)、SIG_SETMASK(覆盖掩码)。

2.7. 信号开发注意事项

  • 避免信号处理函数不可重入:信号处理函数中禁止调用不可重入函数(如printf、malloc、free),仅能调用异步信号安全的函数,防止数据混乱;

  • 非实时信号丢失问题:1~31号非实时信号不支持排队,短时间触发多个相同信号仅处理1次,需用实时信号(34~64)保证信号不丢失;

  • SIGCHLD信号处理:子进程退出默认触发SIGCHLD,父进程若不捕获回收,子进程会变为僵尸进程,必须注册SIGCHLD处理函数;

  • 信号阻塞的合理使用:关键业务代码段可临时阻塞信号,避免被打断,业务执行完毕后及时解除阻塞,防止信号堆积;

  • 特权信号不可触碰:切勿尝试捕获、忽略SIGKILL(9)和SIGSTOP(19),此类操作无任何效果,还会引发程序异常。