信号概述
信号是消息的载体,进程信号用于通知进程发生了某种情况。在现实生活中,我们是通过以下方式让认识信号的:第一,我们可以识别信号,知道信号的到来并对其进行区分;第二,我们知道信号的应对、处理方式;第三,我们在某些情况下可以记住信号。在计算机中,进程作为用户的代表,也应该具有与上述类似的特性和功能:
- 进程必须能够识别、处理信号。
- 即使没有收到信号,进程也应该知道各个信号的处理方法,处理信号的能力,属于进程内置功能的一部分。
- 收到信号时,进程可以不立即处理,而是在合适的时候处理。从信号产生到信号被处理的的时间窗口,进程要具有临时保存信号的能力。
上述几点的具体实现方式和原理会在下文详细解释。
在 Linux 中有多种信号,使用man 7 signal
或kill -l
命令查看所有信号:
//共有62个信号 没有32、33号信号
[@shr Tue Feb 27 10:44:43 11.24_signal_test]$ 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
号新号是普通信号,34~64
号新号是实时信号。实时信号需要被立即处理,为了阐释信号的完整的生命周期,这里不对实时信号进行讨论。
发送信号的方式有多种,下文会进行详细解释。假设现在一个进程收到了一个信号,它对这个信号的处理方式无非是三种:
- 执行针对这个信号的默认动作。
- 忽略这个信号,不做任何处理。
- 执行用户自定义的动作。
这三个方式只能选择一个进行。
本文会从进程信号的整个生命周期出发,对进程信号做详细阐释。
信号的产生
在讨论信号的产生之前,需要先明确认识到:因为操作系统是进程的管理者,所以所有信号最终都是由操作系统发送给进程的。
键盘组合键
键盘组合键是产生信号的一个快捷方式,当输入某些组合键时,本质是被进程解释成了某种信号。
ctrl + c -> 2号信号
ctrl + \ -> 3号信号
ctrl + z -> 19号信号
关于键盘组合键产生信号的方式,有两点需要说明。
第一,前台进程和后台进程。Linux 在启动时,会为终端分配一个 bash 进程,bash 进程默认是前台进程。每一次登录最多只能有一个前台进程,可以同时有多个后台进程。前台进程可以获取键盘输入,谁能获取键盘输入,谁就是前台进程。所以键盘组合键输入方式发送的信号,只能传递给前台进程。
第二,关于硬件中断与信号的关系。操作系统是通过硬件中断来判断硬件就绪和操作硬件的,例如,当键盘准备就绪,会向 CPU 发送硬件中断和中断号,操作系统会根据中断号查中断向量表,执行对应的方法,对硬件进行操作。
在上述过程中,硬件向 CPU 发送硬件中断,本质是发送一个“信号”,告诉CPU自己已经准备就绪。这与现在正在讨论的信号有异曲同工之妙,事实上,信号是纯软件层面的,我们现在正在谈的进程信号,本质是模拟的硬件的中断信号。
“中断”是计算机中一个极其重要的概念和行为,事实上,操作系统本身也是由硬件中断推动运行的。CPU会每隔一个很短的时间给操作系统发送一个时钟中断,当操作系统收到时钟中断时,会停止其调度工作,转而执行中断向量表中自己的代码。即:操作系统本身是由硬件推动执行的,是一个基于时钟中断的死循环。
kill命令和系统调用
使用kill signo [pid]
命令对进程发送信号:
kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
kill(2) 系统调用与 kill 命令类似,对进程 pid 发送 sig 信号:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//成功返回0,失败返回-1
C接口 raise(3) 是对 kill(2) 的封装,对调用者发送 sig 信号:
#include <signal.h>
int raise(int sig);
//成功返回0,失败返回一个非0值
raise(3) 相当于kill(getpid(), sig)
。
C接口 abort(3) 是对 kill(2) 的封装,对调用者发送 6 号信号(SIGABRT),并且使进程中止:
#include <stdlib.h>
void abort(void);
通过alarm(2)设置闹钟,定时向进程发送 14 号信号:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//seconds秒后向进程发送SIGALRM信号
//返回上一个被设定的闹钟的剩余时间
alarm 的原理大致如下:根据当前时间与 seconds 参数计算出一个 timeout 时间,通过比较当前时间与 timeout 时间是否相等来判断是否到时。当系统中存在多个 alarm 时,可以通过小根堆将这些 alarm 维护起来,如果堆顶的闹钟不到时,则其他闹钟一定不到时。
异常
当进程发生异常,例如发生除零错误或野指针错误时,操作系统会给异常的进程发送信号,进程对异常信号的默认处理方式是进程退出。
操作系统是通过以下方式知道进程发生异常的:针对浮点异常,CPU中的寄存器会保存数据的溢出状态;针对野指针异常,当虚拟地址通过 MMU 和页表映射到物理内存失败时,这个转化失败的虚拟地址会被存储到一个寄存器中。操作系统是硬件的管理者,CPU 中寄存器的异常可以被操作系统捕捉到,并依此向异常的进程发送信号。同时,操作系统可以通过判断寄存器种类来区分进程发生了哪个异常。在这个过程中,虽然修改的是CPU内部的寄存器,但这些寄存器中的内容终归是要保存在进程自己的硬件上下文中,当一个新的进程被调度时,这些寄存器中的内容会被新进程填充,不被上一个异常的进程影响。当这个异常的进程再次被调度时,其存在异常数据的上下文信息会被再次填充到CPU的寄存器中,再次被操作系统得知,再次向进程发送信号,如果自定义捕捉异常信号不使进程退出,则会一直处于这个循环。
异常的目的,是让用户更清晰地认识到进程的死亡原因以及做一些收尾工作,而不是让用户立即解决问题。C++等语言层面的异常处理,其本质都是让操作系统向进程发送某种信号,并使进程捕捉到这个信号,进而指导开发者进行错误排查。
关于异常还有一个话题——核心转储(core dump)。打开核心转储功能后,当进程异常退出时,操作系统会将内存中的运行信息转储到当前工作目录(cwd)的core.[pid]
文件中,这个行为即是核心转储,对应的文件被称为核心转储文件。在调试时,可以利用核心转储文件直接定位到程序的出错行。
/*
filename:test.cpp
out:test.cpp
g++ $^ -o $@ -g -std=c++11
*/
int main()
{
int a = 10, b = 0;
int c = a / b;
return 0;
}
[@shr Wed Feb 28 20:49:39 2.27_signal_test]$ ll
total 32
-rw-rw-r-- 1 shr shr 73 Feb 28 15:04 makefile
-rwxrwxr-x 1 shr shr 24528 Feb 28 20:48 out
-rw-rw-r-- 1 shr shr 1149 Feb 28 20:48 test.cpp
[@shr Wed Feb 28 20:49:40 2.27_signal_test]$ ./out
Floating point exception (core dumped)
[@shr Wed Feb 28 20:49:41 2.27_signal_test]$ ls
core.29949 makefile out test.cpp
[@shr Wed Feb 28 20:49:44 2.27_signal_test]$ gdb out
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/shr/code/2024_y/2.27_signal_test/out...done.
(gdb) core-file core.29949
[New LWP 29949]
Core was generated by `./out'.
Program terminated with signal 8, Arithmetic exception.
#0 0x0000000000400643 in main () at test.cpp:4
58 int c = a / b;
/*……此处省略……*/
在Linux服务器中,核心转储功能一般是被关闭的,主要有几点原因:
- 当服务出现问题需要自动化重启时,生成转储文件可能会延迟服务重启的过程,影响系统的稳定性。
- 核心转储文件会占据大量磁盘空间,关闭核心转储可以避免其冲击磁盘而影响操作系统。
- 核心转储文件可能包含敏感信息,如果被未经授权的用户拿到,可能会导致安全漏洞。
可以使用ulimit -c [filesize]
命令打开核心转储功能。
[@shr Fri Mar 01 09:56:53 ~]$ ulimit -c 10240
[@shr Fri Mar 01 09:56:58 ~]$ ulimit -a
core file size (blocks, -c) 10240
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
/*……此处省略……*/
信号的发送
对于进程而言,进程需要知道自己的是否收到了信号、收到了哪一个信号。信号是发送给进程的 PCB 的。下文会看到,针对普通信号,进程 PCB 中使用一个32位的位图管理信号,比特位的位置代表信号的种类,比特位的状态代表对应信号的接收状态。操作系统是进程的管理者,只有操作系统才有资格修改 PCB的内容,所谓的发送信号,本质是操作系统对进程PCB中管理信号的位图进行修改。
信号的保存
进程之所以需要有能力保存信号,是因为当进程收到一个信号时,可能不会立即处理这个信号,从进程收到信号到处理信号,可以有一个时间窗口。承上文,进程是通过 task_struct 中的位图对普通信号进行保存的。
pending表、handler表和block表
在进程的 task_struct 中,存在三个与信号有关的表:pending、handler与block。
- pending 表用来保存信号。
- block 表用来记录信号的阻塞状态。
- handler 表用来保存对信号的处理动作,实际存储的是函数指针。下文会看到,用户可以通过系统调用来使 handler 表中的指针指向用户空间中自定义的处理函数。
将实际执行信号的处理动作称为信号递达(delivery),将信号从产生到递达之间的状态称为信号未决(pending)。进程可以选择阻塞(block)/屏蔽(mask)某个信号,信号被屏蔽后, block 表的对应位置被置为 1。被屏蔽的信号无法被递达。屏蔽是一种状态,无论进程是否实际收到了一个信号,这个信号都可以被屏蔽。为了系统的安全考虑,9 号和 19 号信号无法被屏蔽。
如果信号未被屏蔽,则进程收到信号后,可以将 pending 表对应位置修改为 1,表示保存这个信号。由于位图结构的限制,对于多个相同的信号,进程最多只能对其保存一次。当收到一个信号时,会首先将 pending 表对应的位置修改为 1,然后判断这个信号是否在 block 表中被屏蔽,如果 block 表中对应的位置为 1,则保持在未决状态,不执行 handler 表中的方法,直到解除对这个信号的屏蔽才会进行递达。
系统提供了3个内置的 handler 方法,分别是 SIG_ERR、SIG_DFL 和 SIG_IGN,其中第一个作为错误返回值,第二个为信号的默认处理方法,第三个为忽略方法。
/* Fake signal functions. */
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
上文中所说的屏蔽(mask)与忽略方法(ignore)并不是一个概念,被屏蔽的信号无法被递达,而忽略是信号被递达后的一种处理方法。
信号集操作
信号集(sigset)是一个能表示多个信号的数据类型,类型为sigset_t
,是操作系统提供的一种数据类型。信号集主要用来管理信号,上文中谈到的 pending 表和 block 表即是信号集结构。sigset_t 本质是一个位图结构,它的定义如下:
/*
filename:signal.h
*/
typedef __sigset_t sigset_t;
/*
filename:sigset.h
*/
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
信号集操作是由操作系统提供的C语言接口,方便对信号集进行操作。系统提供了下面几种信号集操作:
#include <signal.h>
int sigemptyset(sigset_t *set); //将信号集的位置全部置 0
int sigfillset(sigset_t *set); //将信号集的位置全部置 1
int sigaddset(sigset_t *set, int signum); //向信号集中添加编号为signum的信号
int sigdelset(sigset_t *set, int signum); //删除信号集中编号为signum的信号
int sigismember(const sigset_t *set, int signum); //判断编号为signum的信号是否在信号集中
对于 pending 表的操作,可以通过各种信号产生方法向进程发送信号进行,使用sigpending(2)
获取 pending 表。在这个部分,主要针对于对 block 表的操作。
#include <signal.h>
int sigpending(sigset_t *set);
//成功返回0,失败返回-1
在信号集操作中,将block表称作阻塞信号集或者信号屏蔽字,可以使用sigprocmask(2)
系统调用对阻塞信号集进行操作:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//成功返回0,失败返回-1
参数 set 是一个输入型参数,用于传入一个信号集,参数 oldeset 是一个输出型参数,用于带出旧的信号集。参数 how 用于指定进行何种操作:
- SIG_BLOCK 将当前进程的阻塞信号集设置为当前阻塞信号集与 set 的并集。
- SIG_UNBLOCK set 中的信号将会在当前进程的阻塞信号集中被移除。
- SIG_SETMASK 设置当前进程的阻塞信号集至与 set 相同。
信号的处理
在讨论信号的捕捉、处理之前,需要先对内核态与用户态有所了解,这是理解进程对信号的检测与处理时机的关键。
内核空间与内核态
在讨论进程地址空间和内存管理概述时,已知每个进程的地址空间中都会包含一部分内核空间,这个内核空间本身属于进程地址空间。每个地址空间都会有一份自己的页表,用于进行虚拟地址到物理地址的映射,这类页表是进程的用户页表,通过用户页表,进程只能访问属于自己的内存资源,而不能访问操作系统的资源。有几个进程,就有几个地址空间,有几个用户页表。在系统中还有一个内核页表,内核页表存储了操作系统的代码和数据的地址映射,如果权限允许,可以通过内核页表访问操作系统的代码和资源。内核页表只有一份,这意味着每一个进程通过自己地址空间中的内核空间看到的操作系统的代码和资源都是一样的。
基于上述设计,在进程视角看来,当调用系统调用时,是在自己的地址空间中进行的;在操作系统视角,在权限允许的情况下,进程随时可以执行自己(操作系统)的代码,因为每个地址空间中都包含一部分内核空间。当进程需要通过内核空间执行操作系统的代码时,需要进入另一种状态,即内核态。处于内核态的进程才可以合法执行操作系统的代码。当执行操作系统的代码结束时,进程会重新回到用户态。进程不是只有在执行系统调用时才会陷入内核,在进程被调度时,加载进程的内核数据结构、填充CPU的寄存器等行为都需要CPU以内核态进行。
CPU中的 ecs 寄存器记录了CPU当前的工作状态,其保存的二进制的低两位00
、11
分别代表内核态和用户态。
对信号的自定义捕捉
使用signal(2)
系统调用可以对指定信号进行自定义捕捉:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//成功返回前一次signum信号的处理函数的指针,失败返回SIG_ERR
signal(2) 本质是绑定一个对信号的自定义捕捉动作。使用一次 signal(2),在进程的整个生命周期内有效。只有在进程接收到指定的信号之后,对应的自定义函数才会执行。为了保证系统的安全,9 号(SIGKILL)和 19 号(SIGSTOP)信号不能被自定义捕捉。
void handler(int signum)
{
std::cout << "get a signal: " << signum << std::endl;
}
int main()
{
signal(2, handler);
std::cout << "pid: " << getpid() << std::endl;
while(true);
return 0;
}
进程不需要进行任何操作来等待信号的到来,当定义好自定义捕捉函数后,只要收到了对应的信号,便会自动执行捕捉方法。这是因为信号的产生与进程的运行是异步的,正如上文所说,信号最终是由操作系统发送给进程的,进程无法预测信号产生的准确时间,也不需要通过任何操作来等待信号的到达。当然,可以使用sigwait(3)
函数来使进程阻塞等待一个信号,但是一般不这样做。
信号被处理的具体时间,是进程从内核态准备返回到用户态的时刻。当进程准备从内核态返回到用户态时,会对信号进行检测和处理:查看pending表,对于表中为 1 的信号,查看是否在 block 表中被阻塞,如果 block 表对应位置为0,则执行 handler 表中的方法,执行完成后返回到用户态。如果要处理的信号的方法为 SIG_DEL 或 SIG_IGN,则从内核处理后直接返回到用户态;如果对信号进行了自定义捕捉,则进程首先会进入用户态,以用户态执行自定义捕捉方法,执行完成后重新陷入内核,然后从内核返回用户态。操作系统不相信任何用户,不会以内核态执行用户的任何代码。
在进入用户态执行自定义捕捉方法 handler 时,handler中可能包含系统调用接口,或者进程在执行 handler 时被调度,就有可能在handler中陷入内核,从内核准备返回用户态时,进行信号的检测与处理,又会重进调用 handler,形成信号的嵌套捕捉。在多线程环境中,也可能出现类似问题,当一个线程正在执行信号处理函数时,另一个线程可能又发送了相同的信号给进程。
这是不被接受的,所以在执行信号的自定义捕捉方法期间,这个信号会被屏蔽,以防止重复不断地调用 handler 方法。下面的代码可以验证这一点:
/*
filename:test.cpp
*/
void PrintPending()
{
sigset_t set;
sigpending(&set);
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&set, signo)) cout << "1";
else cout << "0";
}
cout << "\n";
}
void handler(int signum)
{
std::cout << "get a signal: " << signum << std::endl;
while(true)
{
PrintPending(); //不断打印pending表以查看信号的屏蔽情况
sleep(1);
}
}
void handler_0(int signum)
{
std::cout << "get:" << signum << std::endl;
}
int main()
{
signal(2, handler);
std::cout << "pid: " << getpid() << std::endl;
while(true);
return 0;
}
/*
输出
*/
[@shr Sun Mar 03 10:25:19 2.27_signal_test]$ ./out
pid: 19989
^Cget a signal: 2 //第一次发送2号信号,开始执行自定义捕捉方法
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000 //第二次发送2号信号 2号信号被阻塞
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
killed
对信号的第二个自定义捕捉方法为sigaction(2)
系统调用:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
//成功返回0,失败返回-1
第一个参数 signum 为自定义捕捉的信号,后面的参数是一个 sigaction 结构指针,这个结构中的字段如下:
struct sigaction {
void (*sa_handler)(int); //对信号的自定义捕捉方法
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //处理当前信号时,要阻塞的其他信号
int sa_flags;
void (*sa_restorer)(void);
};
在执行某个信号的自定义捕捉方法期间,除了系统会自动阻塞当前信号,用户也可以选择阻塞其它信号,sigaction结构中的 sa_mask 字段指明。这些被阻塞的信号会在自定义捕捉方法执行完成后被取消阻塞。
sigaction(2) 系统调用中,第二个参数 act 是一个输入型参数,第三个参数 oldact 是一个输出型参数,输出旧的 sigaction 结构。
/*
filename:test.cpp
*/
void PrintPending()
{
sigset_t set;
sigpending(&set);
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&set, signo))
cout << "1";
else
cout << "0";
}
cout << "\n";
}
void handler(int signo)
{
cout << "catch a signal, signal number : " << signo << endl;
while (true)
{
PrintPending(); //不断打印pending表以查看信号的屏蔽情况
sleep(1);
}
}
int main()
{
struct sigaction act, oact; //定义sigaction结构
//初始化
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
sigemptyset(&act.sa_mask);
//向阻塞信号集中添加信号
sigaddset(&act.sa_mask, 1);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
act.sa_handler = handler; //设置自定义捕捉方法
sigaction(2, &act, &oact); //调用sigaction(2)
while (true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}
/*
输出
*/
[@shr Sun Mar 03 10:39:48 2.27_signal_test]$ ./out
I am a process: 20147
I am a process: 20147
^Ccatch a signal, signal number : 2 //收到信号,开始执行自定义捕捉方法
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000 //收到2号信号,被阻塞
0100000000000000000000000000000
1100000000000000000000000000000 //收到1号信号,被阻塞
1100000000000000000000000000000
1110000000000000000000000000000 //收到3号新号,被阻塞
1110000000000000000000000000000
1111000000000000000000000000000 //收到4号新号,被阻塞
1111000000000000000000000000000
killed
有关信号的衍生话题
可重入函数
函数的“重入”指的是函数被多个执行流重复进入执行,期间可能存在多个执行流并发执行这个函数的情况。当多个执行流并发执行一个函数时,如果函数内部有对资源的使用、操作和修改,大概率会出现意料之外的错误。例如如果一个函数的功能是对一个全局的链表做头插操作,在某个执行流修改中间某个指针时,这个执行流可能被调走,新的执行流进入函数又会做一系列指针操作,此时就可能会造成指针修改的混乱。
如果一个函数在被重入的情况下可能出错,则这个函数为不可重入函数,否则称之为可重入函数。可重入和不可重入描述的是函数的特点。
SIGCHLD信号
子进程退出时,会给父进程发送SIGCHLD
信号。父进程可以基于 SIGCHLD信号对子进程进行处理。一个方法是,当子进程退出并给父进程发送 SIGCHLD 信号时,父进程再在自定义捕捉方法中对子进程进行资源回收与状态捕获。
void handler(int signo)
{
cout << "get a signal: " << signo << endl;
pid_t rid = waitpid(-1, nullptr, 0); //等待子进程
cout << "child exit, pid: " << rid << endl;
}
int main()
{
signal(SIGCHLD, handler); //绑定自定义捕捉方法
pid_t id = fork();
if(id == 0)
{
cout << "i am child, pid: " << getpid() << endl;
sleep(3);
exit(0);
}
else if(id > 0)
{
cout << "i am father, pid: " << getpid() << endl;
sleep(6);
} else {}
return 0;
}
/*
输出
*/
[@shr Sun Mar 03 13:05:03 2.27_signal_test]$ ./out
i am father, pid: 22725
i am child, pid: 22726
get a signal: 17
child exit, pid: 22726
如果有多个子进程同时退出或者退出一部分,可以使用非阻塞方式对多个子进程进行一次性等待。
/*
filename:test.cpp
*/
void handler(int signo)
{
pid_t rid = 0;
//进行非阻塞等待,一次性回收多个子进程
while((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "child exit, pid: " << rid << endl;
}
}
int main()
{
signal(SIGCHLD, handler);
for(int i = 0; i < 10; ++i)
{
pid_t id = fork();
if(id == 0)
{
cout << "i am child, pid: " << getpid() << endl;
sleep(3);
exit(0);
}
}
while(true)
{
cout << "i am father, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
/*
输出
*/
[@shr Sun Mar 03 13:01:36 2.27_signal_test]$ ./out
i am child, pid: 22450
i am father, pid: 22448
i am child, pid: 22451
i am child, pid: 22449
i am child, pid: 22452
i am child, pid: 22453
i am child, pid: 22454
i am child, pid: 22456
i am child, pid: 22455
i am child, pid: 22457
i am child, pid: 22458
i am father, pid: 22448
i am father, pid: 22448
child exit, pid: 22450
i am father, pid: 22448
child exit, pid: 22451
i am father, pid: 22448
child exit, pid: 22449
i am father, pid: 22448
child exit, pid: 22452
child exit, pid: 22453
i am father, pid: 22448
child exit, pid: 22454
i am father, pid: 22448
child exit, pid: 22455
child exit, pid: 22456
i am father, pid: 22448
child exit, pid: 22457
i am father, pid: 22448
child exit, pid: 22458
i am father, pid: 22448
i am father, pid: 22448
i am father, pid: 22448
^C
上述方法中,之所以要在接收 SIGCHLD 信号之后还需要自定义捕捉方法对子进程进行等待,是因为系统对 SIGCHLD 信号的默认处理方法 SIG_DFL 是什么也不做,此时子进程就会僵尸。另一个基于 SIGCHLD 信号简便处理子进程退出的方式是,将SIGCHLD信号的捕捉方法设置为 SIG_IGN,之后,当子进程退出、父进程收到 SIGCHLD 信号时,父进程会忽略这个信号,并且将子进程的资源进行回收。
int main()
{
signal(SIGCHLD, SIG_IGN); //设置进程忽略SIGCHLD方法
//批量创建子进程
for(int i = 0; i < 10; ++i)
{
pid_t id = fork();
if(id == 0)
{
cout << "i am child, pid: " << getpid() << endl;
sleep(3);
cout << "child exit" << endl;
exit(0);
}
}
while(true)
{
cout << "i am father, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}