kill -l
查看所有信号
[CegghnnoR@VM-4-13-centos 2022_11_6]$ 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
一共有62个信号,没有0号信号,32号信号和33号信号。1~31号信号称为普通信号,34~64号信号称为实时信号。
因为信号产生是异步的,当信号产生的时候,对应的进程可能正在处理更重要的事情,那么进程可以暂时不处理这个信号。
信号可以由键盘产生,在命令行中我们要想终止一个正在运行的前台进程可以按 ctrl+c,这一指令实际上是向进程发送了2号信号SIGINT
此外,在命令行中使用 ctrl+\ 可以产生3号信号 SIGQUIT
。
我们可以使用 signal
来设置对信号的处理方式
NAME
signal - ANSI C signal handling
SYNOPSIS
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum
表示我们要对哪个信号设置捕捉动作。
handler
函数指针,允许用户自定义对信号的处理动作。
例子:
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "成功获取信号" << signo << endl;
}
int main()
{
// 设置对信号的处理方式
signal(SIGINT, handler);
signal(SIGQUIT, handler);
while (true)
{
cout << "正在运行中..." << endl;
sleep(1);
}
return 0;
}
运行:
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ./myproc
正在运行中...
^C成功获取信号2
正在运行中...
^C成功获取信号2
正在运行中...
正在运行中...
^\成功获取信号3
正在运行中...
^\成功获取信号3
正在运行中...
正在运行中...
可以发现 ctrl+c 和 ctrl+\ 都无法终止进程了,而是改为打印一句提示语。
要想终止这个进程,可以使用 kill -9
注意:9号信号无法被重新设置
NAME
kill - send signal to a process
SYNOPSIS
#include
#include
int kill(pid_t pid, int sig);
// 返回值:成功(至少发送了一个信号):返回0。出错:返回-1,并设置errno。
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
kill(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
例子:
模拟 kill -9 杀掉进程:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "成功获取信号" << signo << endl;
}
static void Usage(const string& proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
if (kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
{
cerr << "kill: " << strerror(errno) << endl;
exit(2);
}
return 0;
}
raise
函数
NAME
raise - send a signal to the caller
SYNOPSIS
#include
int raise(int sig);
这个函数就是给自己发信号
例子:
void handler(int signo)
{
cout << "成功获取信号" << signo << endl;
}
static void Usage(const string& proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
int main()
{
signal(2, handler);
while (1)
{
sleep(1);
raise(2);
}
return 0;
}
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
成功获取信号2
成功获取信号2
成功获取信号2
abort
函数
NAME
abort - cause abnormal process termination
SYNOPSIS
#include
void abort(void);
向自己发送 SIGABRT
信号
使用它也可以终止进程,和 9 号信号不同的是,它可以被 signal
捕获并处理,但是依然无法被重新设置。
void handler(int signo)
{
cout << "成功获取信号" << signo << endl;
}
static void Usage(const string& proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
int main()
{
signal(6, handler);
abort();
}
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
成功获取信号6
Aborted
可以看到它成功被捕获并处理了,但是程序依然通过 abort
退出了
alarm
函数
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include
unsigned int alarm(unsigned int seconds);
设置发送信号的闹钟
该函数会令进程在 seconds
秒后收到 4 号信号 SIGALRM
终止进程。
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号,例如当前进程执行了除以 0 的进程,CPU 的运算单元会产生异常,内核将这个异常解释为 SIGFPE
信号发送给进程。又比如,当前进程访问了非法内存得知,MMU 会产生异常,内核将这个异常解释为 SIGSEGV
信号发送给进程。
在[Linux](9)进程控制:进程创建,进程终止,进程等待,进程程序替换_世真的博客-CSDN博客中我们提到了进程的退出状态
其中的 core dump 标志表示是否进行了内存转储,”内存转储“是一个历史术语,意思是把代码和数据内存段的映像写到磁盘上。说人话就是,将进程在运行中出现的异常上下文数据,存储到磁盘上,方便调试。
使用 man 7 signal
我们可以看到这样一张表格:
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
在 Action 一列,Term 表示终止,Core 表示终止并进行 core dump
当进程接受到 Action 为 Core 的信号后,将会进行 core dump。通过观察可以发现,这些信号的原因大概是程序代码出了问题。
对于以下程序,我们创建一个子进程,让它发生野指针问题,最后将退出信息发送个父进程进行解析。
int main()
{
pid_t id = fork();
if (id == 0)
{
int* p = nullptr;
*p = 10;
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
printf("exitcode: %d, signo: %d, core dump flag: %d\n", (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
}
运行结果:
exitcode: 0, signo: 11, core dump flag: 0
可以发现,终止信号是11,core dump flag 是 0,也就是没有发生 core dump。
为什么呢?
因为在云服务器中,默认是不允许 core dump 的。
使用指令 ulimit -a
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7904
max locked memory (kbytes, -l) unlimited
max memory size (kbytes, -m) unlimited
open files (-n) 100002
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7904
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
第一列的 core file size 默认被设置为 0,也就是禁止发生 core dump
使用 ulimit -c 1000000
可以将它的大小设置为 1000000
,这样就允许生成 core file 了
接着再次运行上一个程序,即可成功生成 core file。
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
exitcode: 0, signo: 11, core dump flag: 1
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ll
total 296
-rw------- 1 CegghnnoR CegghnnoR 593920 Nov 7 16:24 core.13458
-rw-rw-r-- 1 CegghnnoR CegghnnoR 151 Nov 7 12:37 makefile
-rwxrwxr-x 1 CegghnnoR CegghnnoR 8896 Nov 7 16:11 mykill
-rw-rw-r-- 1 CegghnnoR CegghnnoR 477 Nov 7 16:11 mykill.cpp
-rwxrwxr-x 1 CegghnnoR CegghnnoR 9176 Nov 7 12:37 myproc
-rw-rw-r-- 1 CegghnnoR CegghnnoR 184 Nov 7 12:37 myproc.cpp
core 文件一般比较大。
编译时使用 -g 选项,编译后使用 gdb 调试:
然后使用 core-file [core file]
指令来指定刚才生成的 core 文件,这样就能迅速定位到出现错误的一行。
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ls
core.15976 makefile mykill mykill.cpp myproc.cpp
[CegghnnoR@VM-4-13-centos 2022_11_6]$ gdb mykill
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
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:
...
Reading symbols from /home/CegghnnoR/code/2022_11_6/mykill...done.
(gdb) core-file core.15976
[New LWP 15976]
Core was generated by `./mykill'.
Program terminated with signal 11, Segmentation fault.
#0 0x000000000040077f in main () at mykill.cpp:17
17 *p = 10;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64
(gdb)
sighandler
。sigset_t
sigset_t
是一个结构体类型,用于表示位图sigset_t
称为信号集信号集操作函数
sigset_t
类型对于每种信号用一个 bit 表示 有效 或 无效 状态,至于这个类型内部是如何实现的,使用者不必关心,使用者只能使用以下函数来操作 sigset_t
变量。
#include
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 信号集全置1
int sigaddset (sigset_t *set, int signo); // 加入信号
int sigdelset(sigset_t *set, int signo); // 删除信号
int sigismember(const sigset_t *set, int signo); // 判断信号是否在信号集中
调用函数 sigprocmask
以读取或更改进程的信号屏蔽字(阻塞信号集)
NAME
sigprocmask - examine and change blocked signals
SYNOPSIS
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// 返回值:成功:返回0,失败:返回-1
how
可以传入三个选项(假设当前的信号屏蔽字为 mask):
SIG_BLOCK | set 包含了我们希望添加到当前信号屏蔽字的信号,相当于 mask = mask | set |
SIG_UNBLOCK | set 包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于 mask = mask & ~set |
SIG_SETMASK | 设置当前信号屏蔽字为 set 所指向的值,相当于 mask = set |
set
是输入型参数,oldset
是输出型参数
oldset
是非空指针,则读取进程的当前信号屏蔽字通过 oldset
参数传出,如果 set
是非空指针,则更改进程的信号屏蔽字。参数 how
指示了如何更改。如果 oldset
和 set
都是非空指针,则先将原来的信号屏蔽字备份到 oldset
里,然后根据 set
和 how
参数更改信号屏蔽字。如果调用 sigprocmask
解除了对当前若干个未决信号的阻塞,则在 sigprocmask
返回前,至少将其中一个信号递达。
注意:9 号信号无法被屏蔽
NAME
sigpending - examine pending signals
SYNOPSIS
#include
int sigpending(sigset_t *set);
// 返回值:成功:返回0,失败:返回-1
set
为输出型参数,该函数可以直接将 pending
信息通过 set
传出
例子:
每隔一秒打印一次当前进程的 pending 信号集,在程序运行中我们给它发送 2 号信号,由于发完立刻就会被递达,所以我们先把 2 号信号屏蔽掉,方便查看发送信号后的 pending 信号集的变化
static void showPending(sigset_t* pendings)
{
// 遍历31个信号
for (int sig = 1; sig <= 31; ++sig)
{
if (sigismember(pendings, sig))
{
cout << "1";
}
else
{
cout << "0";
}
}
}
int main()
{
sigset_t bsig, obsig;
sigemptyset(&bsig);
sigemptyset(&obsig);
// 添加2号信号到信号屏蔽字中
sigaddset(&bsig, 2);
// 让当前进程屏蔽2号信号
sigprocmask(SIG_SETMASK, &bsig, &obsig);
sigset_t pendings;
while (true)
{
sigemptyset(&pendings);
if (sigpending(&pendings) == 0)
{
showPending(&pendings);
}
sleep(1);
cout << endl;
}
}
[CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
0000000000000000000000000000000
0000000000000000000000000000000
^C0000000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
可以看到在发送了 2 号信号后,第 2 位变成 1 了。
上面我们说到,进程收到信号不是立即就进行处理的,而是在合适的时候。那么什么是合适的时候?
当当前进程从内核态,切换回用户态的时候,进行信号的检测与处理。
在讲虚拟进程地址空间中:[Linux](8)进程地址空间_世真的博客-CSDN博客,我们见过下面这张图:
我们知道,在虚拟进程地址空间与物理内存之间有一级页表,负责映射位置关系。其实页表分为两种:
这两个页表的访问权限是不一样的,当前进程要想访问内核级页表,需要进行身份切换。
CPU 内部有对应的状态寄存器 CR3,有 bit 位表示当前进程的状态:0——内核态,3——用户态
一般在系统调用和进程时间片到了,进行进程切换的时候会从用户态切换到内核态,在执行完系统调用或进程切换后,由内核态切换回用户态的时候,就会进行信号的检测与处理。
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号
如下图:
信号捕捉与状态切换的过程。
main
函数的上下文继续执行后续代码。sigaction 函数可以读取和修改与指定信号相关联的处理动作。
NAME
sigaction - examine and change a signal action
SYNOPSIS
#include
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
// 返回值:成功:返回0,失败:返回-1
signo
是指定信号的编号。
若 act
指针非空,则根据 act
修改该信号的处理动作。若 oldact
非空,则通过 oldact
传出信号原来的处理动作。
act
和 oldact
两个参数是结构体指针,其结构体被定义为:
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
我们只研究该结构体的 void (*sa_handler)(int);
sigset_t sa_mask;
两个成员
sa_handler
可以赋值为我们自己定义的处理方式,也可以赋值为 SIG_IGN
表示忽略信号。赋值为 SIG_DFL
表示执行系统默认动作。例子:
void handler(int signo)
{
cout << "获取到一个信号,信号编号:" << signo << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while (true)
{
sleep(1);
}
return 0;
}
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,如果这种信号再次产生,那么它就会被阻塞到当前的处理结束为止。
例子:
设置对2号信号的处理函数,处理方式为一个死循环,不停打印 pending 位图。
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "获取到一个信号,信号编号:" << signo << endl;
sigset_t pending;
while (true)
{
cout << "." << endl;
sigpending(&pending);
for (int i = 1; i <= 31; ++i)
{
if (sigismember(&pending, i))
cout << '1';
else cout << '0';
}
cout << endl;
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while (true)
{
cout << "main running" << endl;
sleep(1);
}
return 0;
}
[CegghnnoR@VM-4-13-centos 2022_11_9]$ ./mysignal
main running
main running
main running
^C获取到一个信号,信号编号:2
.
0000000000000000000000000000000
.
0000000000000000000000000000000
.
0000000000000000000000000000000
^C.
0100000000000000000000000000000
.
0100000000000000000000000000000
^C.
0100000000000000000000000000000
当我们第一次发送 2 号信号时,进程成功开始处理这个信号。之后再多次发送 2 号信号,我们发现进程只会把 2 号信号设为未决状态,不会继续去处理新的信号,因为内核自动将当前信号加入进程的信号屏蔽了。
sa_mask
是 sigaction
结构体中的 sigset_t
类型成员。设置该变量可以让进程在处理信号的时候不仅屏蔽当前信号,也可以同时屏蔽其他的由用户指定的信号。例子:
在如上代码基础上对 sa_mask
进行设置,屏蔽 3 号信号。
//...
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3); // 设置 sa_mask
sigaction(2, &act, &oact);
//...
[CegghnnoR@VM-4-13-centos 2022_11_9]$ ./mysignal
main running
main running
main running
^C获取到一个信号,信号编号:2
.
0000000000000000000000000000000
.
0000000000000000000000000000000
^C.
0100000000000000000000000000000
.
0100000000000000000000000000000
.
0100000000000000000000000000000
^\.
0110000000000000000000000000000
.
0110000000000000000000000000000
.
0110000000000000000000000000000
.
0110000000000000000000000000000
.
在处理 2 号信号的时候,2 号信号和 3 号信号都被屏蔽了。
以链表的插入为例:
main
函数调用 insert
函数向一个链表 head
中插入结点 node1
,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核态,再次回到用户态之前检查到有信号待处理,于是切换到 sighandler
函数,sighandler
也调用 insert
函数向同一个链表 head
中插入结点 node2
,插入操作的两步都做完之后从 sighandler
返回内核态,再次回到用户态就从 main
函数调用 insert
函数调用的 insert
函数中继续往下执行,先前做一步之后被打断,现在继续做完第二步。结果是,main
函数和 sighandler
先后向链表中插入两个结点,而最后只有第一个结点真正插入链表中了。insert
函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert
函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。具有以下条件之一的函数是不可重入的:
malloc
或 free
,因为 malloc
也是用全局链表来管理堆的。volatile
是 C 语言中的关键字,这里我们站在信号的角度来重新理解一下。
例子:
我们定义一个全局变量 flags = 0
,然后死循环,直到发送 2 号信号,通过信号处理函数将 flags
改为 1,结束循环
#include
#include
int flags = 0;
void handler(int signo)
{
printf("更改flags: 0->1\n");
flags = 1;
}
int main()
{
signal(2, handler);
while (!flags);
printf("进程是正常退出的\n");
return 0;
}
[CegghnnoR@VM-4-13-centos volatile]$ ./mysignal
^C更改flags: 0->1
进程是正常退出的
运行结果是符合预期的!
但是在编译器高优化级别下,结果就不一定了,
如下,开启 O2 优化后的运行结果:
[CegghnnoR@VM-4-13-centos volatile]$ make
gcc -o mysignal mysignal.c -std=c99 -O2
[CegghnnoR@VM-4-13-centos volatile]$ ./mysignal
^C更改flags: 0->1
^C
编译器为了提高运行速度,可能会把 flags
放到寄存器中,while
循环判断的时候就直接从寄存器中读取,而我们的信号处理函数只是修改了内存中的 flags
,最后导致 while
循环无法退出。
使用 volatile
关键字,可以告诉编译器不准对 flags
进行优化,每次 CPU计 算的时候,要从内存中读取数据。
volatile int flags = 0;
这样运行结果又符合预期了。
子进程退出、暂停、继续的时候,都会自动给父进程发送 SIGCHLD
信号。
那么这个信号有什么用呢?
我们以前是让父进程回收子进程是通过阻塞等待或者轮询等待,现在我们可以基于 SIGCHLD
信号,完成一种新的非阻塞等待:SIGCHLD
信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD
信号的处理处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,当子进程终止时会发信号通知父进程,父进程在信号处理中调用 waitpid
回收子进程即可
例子:
#include
#include
#include
#include
#include
#include
using namespace std;
void FreeChld(int signo)
{
assert(signo == SIGCHLD);
while (true)
{
pid_t id = waitpid(-1, nullptr, WNOHANG);
if (id > 0)
{
cout << "父进程等待成功, chld pid: " << id << endl;
}
else if (id == 0)
{
// 还有子进程,但是现在没有退出
cout << "还有子进程,但是没有退出,父进程要继续忙自己的事情了" << endl;
break;
}
else
{
// waitpid 调用失败,即没有子进程了
break;
}
}
}
int main()
{
signal(SIGCHLD, FreeChld);
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
cout << "子进程pid: " << getpid() << "当前cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出,进入僵尸状态" << endl;
exit(0);
}
while (true)
{
cout << "父进程正在运行: " << getpid() << endl;
sleep(1);
}
return 0;
}
注意:如果多个子进程同时退出,则会同时向父进程发送多次 SIGCHLD
信号,其中必然会有一部分信号无法递达(信号屏蔽以及pending 位图只能存储 01 的原因),所以在处理函数中不能只等待一次,而是需要循环等待,将所有已经退出的子进程都回收完再结束等待。如果有子进程,但这些子进程还未退出,那么也要结束等待,让父进程去做其他事,当这些子进程也退出的时候,会重新发送 SIGCHLD
信号让父进程去回收。
事实上,由于 UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程将 SIGCHLD
的处理动作置为 SIG_IGN
,这样 fork
出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
通常系统默认的忽略动作和用户自定义的忽略是没有区别的,但是这里是一个特例。此方法对于 Linux 可用,但不保证在其他 UNIX 系统上可用。