目录
[1. 什么是信号?](#1. 什么是信号?)
[2. 信号的产生](#2. 信号的产生)
[⑥进程等待中的core dump](#⑥进程等待中的core dump)
[3. 信号的保存](#3. 信号的保存)
[4. 信号的捕捉处理](#4. 信号的捕捉处理)
在介绍信号之前,我们先用一个浅显的例子来理解信号
我们点了一个外卖,
外卖小哥打了个电话------产生了一个信号
此时可能在打游戏,等待一个合适的时间处理------信号被记录下来
打完游戏后,去取外卖------处理信号
1. 什么是信号?
对于信号的概念,我们还是举几个生活中的例子,比如
照明信号弹,上下课铃声,发令枪,集合哨子,闹钟,外卖电话等等
从这里我们就可以提出几点
我们是怎么认识这些信号的?------有人教 -> 我们记住了这些常见的信号
即便没有信号产生,我们也知道信号产生之后该干什么
信号产生了并不代表我们要立刻处理这个信号,可以挑选一个合适的时间去处理,因为我们有可能正在做更重要的事。因此,信号产生后会为他创建一个时间窗口,在处理信号时,我们需要在这个时间窗口内记住有信号来过。
我们类比到OS中来,进程就是这里的"我们",即
进程必须能够识别 + 处理信号------即使信号没有产生,也要有具备处理信号的能力------信号的处理能力属于进程内置功能的一部分
当进程真的收到一个具体信号时,进程可能并不会立即处理这个信号,而是留待合适的时候处理
注:信号的处理方式分为:1. 默认动作; 2. 忽略; 3. 自定义动作
- 一个信号从产生到信号开始被处理,一定会有一个时间窗口,进程具有临时保存哪些信号已经产生了的能力
我们可以使用
bash
kill -l
来查看所有信号,即
每个信号的序号及其完整的英文名称:
SIGHUP - Hangup (挂断)
SIGINT - Interrupt (中断)
SIGQUIT - Quit (退出)
SIGILL - Illegal Instruction (非法指令)
SIGTRAP - Trace/Breakpoint Trap (跟踪/断点陷阱)
SIGABRT - Abort (中止)
SIGBUS - Bus Error (总线错误)
SIGFPE - Floating-Point Exception (浮点异常)
SIGKILL - Kill (杀死进程)
SIGUSR1 - User-Defined Signal 1 (用户定义信号1)
SIGSEGV - Segmentation Fault (段错误)
SIGUSR2 - User-Defined Signal 2 (用户定义信号2)
SIGPIPE - Broken Pipe (管道破裂)
SIGALRM - Alarm Clock (闹钟)
SIGTERM - Termination (终止)
SIGSTKFLT - Stack Fault (栈错误)
SIGCHLD - Child Status Changed (子进程状态改变)
SIGCONT - Continue (继续)
SIGSTOP - Stop (Cannot be caught or ignored) (停止)
SIGTSTP - Terminal Stop Signal (终端停止信号)
SIGTTIN - Background Read from TTY (后台读取TTY)
SIGTTOU - Background Write to TTY (后台写入TTY)
SIGURG - Urgent Condition on Socket (套接字紧急情况)
SIGXCPU - CPU Time Limit Exceeded (CPU时间限制超出)
SIGXFSZ - File Size Limit Exceeded (文件大小限制超出)
SIGVTALRM - Virtual Alarm Clock (虚拟闹钟)
SIGPROF - Profiling Timer Expired (性能分析时间到期)
SIGWINCH - Window Size Change (窗口大小改变)
SIGIO - I/O Now Possible (I/O操作可能)
SIGPWR - Power Failure (电源故障)
SIGSYS - Bad System Call (错误系统调用)
其中1~31号信号为普通信号,34~64号信号为实时信号(需要立即处理)
我们之前已经知道了,ctrl+c 能够直接杀掉前台进程,这是为什么呢?------键盘输入是前台输入,首先会被前台进程捕获到,而 ctrl+c 本质上被进程解释成收到了2号信号,我们可以使用signal接口来验证,接口如下
signal()
的行为在不同的 UNIX 版本之间有所不同,并且在不同版本的 Linux 中也有历史上的变化。建议避免使用signal()
,而应使用sigaction(2)
。
signal()
将信号signum
的处理方式设置为handler
,该处理方式可以是SIG_IGN
、SIG_DFL
,或者是程序员定义的函数的地址(即"信号处理函数")。如果信号
signum
被传递给进程,则会发生以下情况之一:
如果处理方式设置为
SIG_IGN
,则信号将被忽略。如果处理方式设置为
SIG_DFL
,则会执行与该信号相关的默认操作(请参见signal(7)
)。如果处理方式设置为一个函数,则首先会将处理方式重置为
SIG_DFL
,或者信号会被阻塞(请参见下面的可移植性部分),然后调用handler
,并将signum
作为参数传递。如果调用处理函数导致信号被阻塞,则在返回处理函数后,该信号会被解锁。信号
SIGKILL
和SIGSTOP
不能被捕获或忽略。
测试代码如下
cpp
int main()
{
// 默认处理
signal(SIGINT, SIG_DFL);
while (true)
{
lg(Debug, "this is a test process\n");
sleep(3);
}
return 0;
}
运行如下
可以看到,对2号信号的默认动作就是终止自己,接下来我们使用自定义动作试试,测试代码如下
cpp
// signo - 信号编号
void myhandler(int signo)
{
lg(Info, "get a signal, signo : %d\n", signo);
}
int main()
{
signal(SIGINT, myhandler);
while (true)
{
lg(Debug, "this is a test process\n");
sleep(3);
}
return 0;
}
运行有
可以看到,我们确实收到了2号信号,且由于我们采取了自定义操作,将处理方式变为了向屏幕打印而不是终止。那我们为什么要将 signal 函数放在代码最前面呢?------设置一次后,再往后都有效。
接下来我们讲讲键盘数据是如何输入给OS的,以及 ctrl+c 是如何变成信号的,这就需要我们谈到硬件了,我们用画图来解释,如图
如图所示,键盘输入内容后,键盘被按下后,键盘通过针脚向CPU发送硬件中断,随后OS根据中断号,将其作为索引在中断向量表中执行对应的方法,最后这个读方法会让OS将键盘中的内容读取到内部的文件缓冲区中。
在这个过程中,如果在最后一步,OS将键盘中内容拷贝到内部文件缓冲区前,OS会先判断数据,即如果识别到数据是 ctrl+c 就会将其转化成2号信号发送给对应进程。我们学习的信号,就是使用软件方式模拟硬件中断,从而给进程发送信号。
2. 信号的产生
信号的产生一般分为如下几种
①键盘的组合键
我们可以使用键盘的组合键向前台进程发送信号,比如:
ctrl+c表示发送2号信号(SIGINT);
ctrl+\表示发送3号信号(SIGQUIT);
ctrl+z表示发送19号信号(SIGSTOP)。
②kill命令
举个例子
bash
kill -19 23541
就表示向pid为23541的进程发送19号信号(SIGSTOP)。
③系统调用。如:kill, raise, abort等,测试代码如下
cpp
void Help(string proc)
{
lg(Fatal, "Please follow the format: %s signo pid\n", proc.c_str());
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Help(argv[0]);
exit(1);
}
int signo = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int n = kill(pid, signo);
if (n == -1)
{
lg(Fatal, "kill error : %s", strerror(errno));
exit(2);
}
return 0;
}
运行效果
测试代码2
cpp
int main()
{
int cnt = 5;
while (true)
{
cout << "this is a test process, my pid : " << getpid() << endl;
sleep(1);
cnt--;
if (cnt == 0) raise(9);
}
return 0;
}
运行效果
abort类似于raise,不同点在于发送的是6号信号。
④产生异常
我们举个例子,测试代码如下
cpp
int main()
{
sleep(1);
lg(Debug, "point error before\n");
int *p;
*p = 5;
sleep(1);
lg(Debug, "point error after\n");
sleep(1);
return 0;
}
运行效果
可以发现,进程由于使用了野指针崩溃了,我们对其捕捉并自定义有
cpp
void handler(int signo)
{
lg(Info, "process %d get a signal, signo : %d\n", getpid(), signo);
}
int main()
{
signal(SIGSEGV, handler);
sleep(1);
lg(Debug, "point error before\n");
int *p;
*p = 5;
sleep(1);
lg(Debug, "point error after\n");
sleep(1);
return 0;
}
运行有
为什么这里一直调用了handler函数呢?信号又为什么一直触发呢?------进程未退出(未变成Z状态)。而我们之前学到过try ... catch{} 肯定与信号有关,不过它发出的信号应该是很温和的,因为我们可以自己对这种情况进行处理。此外,还有除0错误等硬件异常。但是,异常并不只由硬件产生,比如在使用管道的时候,写端正在写入,读端被关闭也会产生异常。
⑤软件条件
在Linux中,我们可以使用alarm接口来向进程发出14号信号(SIGALRM),它的接口如下
alarm()
函数安排在指定的秒数后将一个 SIGALRM 信号发送给调用进程。如果秒数为零,任何待处理的警报将被取消。
无论如何,之前设置的任何
alarm()
都会被取消。
测试代码如下
cpp
int main()
{
int n = alarm(5);
while (true)
{
lg(Info, "this is a process, my pid : %d\n", getpid());
sleep(1);
}
return 0;
}
运行效果如下
我们可以通过重复设置闹钟,使代码每5s响一次,每几秒执行一次自己的任务。修改后代码如下
cpp
void handler(int signo)
{
lg(Info, "pocess %d get a signal, signo : %d\n", getpid(), signo);
alarm(5);
}
int main()
{
signal(SIGALRM, handler);
int n = alarm(5);
while (true)
{
lg(Info, "this is a process\n");
sleep(1);
}
return 0;
}
运行效果如下
⑥进程等待中的core dump
之前我们进程控制中提到,对于一个进程来说,其正常退出与被信号杀死退出是不一样的,具体表现在 waitpid 的第二个参数status,图解如下
这个core dump具体大致分为两种形式,我们可以在man 7 signal 中查看到
我们可以用下面的代码来验证
cpp
int main()
{
pid_t id = fork();
// child
if (id == 0)
{
while (true)
{
lg(Info, "this is child, my pid: %d", getpid());
sleep(1);
}
exit(0);
}
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid == id)
{
lg(Info, "child quit! rid: %d, exit code: %d, exit signal: %d, core dump: %d\n", rid, (status>>8)&0xFF, (status&0x7F), (status>>7)&1);
}
return 0;
}
我们向其发送2号信号有
向其发送8号信号有
可以看到,两种退出方式中的core dump各不相同,而在我们发送信号后,会生成一个core.xxx的文件,如图
这里的32734其实就是运行时进程的pid。
当我们打开系统中的core dump功能时,一旦进程出现异常,OS会将进程在内存中的运行信息 dump(转储) 到进程的当前目录中,形成 core.pid 文件,我们将其称为核心转储(core dump)
形成 core dump 属于运行时出错,而出错了我们一定是想知道在哪一行出了问题。而我们可以在gdb调试中使用 core_file core.pid 来直接定位到出错行。
但是这个功能在云服务器中是默认关闭的,为什么?------因为如果服务器因为出问题陷入了停机就会重启,而重启之后又停机的话,会在这个过程中产生大量的core dump文件从而冲击到磁盘。
3. 信号的保存
①信号的发送与接收
在讲信号的保存前,我们先来讲讲信号是如何发送与接收的。
之前我们说过,OS可以使用 kill 命令来向指定的进程发送对应信号。那么对于一个进程来说,我怎么知道自己有没有收到一个信号呢?其实OS给进程发信号,其实就是给进程的PCB中发信号,我们可以在PCB(task_struct)中查看到
其实OS是将一个 int 的32位作为一个位图,并使用它来管理普通信号(1~31号)。那就意味着如果一次性发十几个信号的话会遗失一部分。
注:实时信号不会遗失,且会立刻执行,虽然它的设计理念与普通信号相同,但是它们实际是使用双链表来管理的。
我们来谈谈这个位图,一个 int 可以表示成 0000 0000 0000 0000 0000 0000 0000 0000,其中
比特位的内容是0还是1,表示是否收到该信号
比特位的位置(第几个)表示信号的编号
所谓的向进程"发信号",其本质就是在OS去修改PCB中的信号位图对应的比特位!
②为什么需要保存信号?
因为进程在收到信号后,可能不会在现在处理这个信号,而信号不被处理,就会产生一个时间窗口
③信号是如何保存的
我们先来介绍一些信号的常见概念
实际执行信号的处理动作称为信号递达(Delivery)。
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞(Block)某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
因为普通信号一共有31种,每一种信号都要有自己的处理办法,那么我们可以猜测一下,OS中一定有下面这种类似的设计
cpp
typedef void (*handler_t)(int); // -- 对应操作的函数指针
handler_t handler[31]; // -- 31种信号的操作集
实际上,在Linux中是这样设计的,如图
即在PCB中,对于信号方面有三个表,分别是阻塞表,保存表,处理表。
在阻塞表中,对应位置的数字为1则表示该信号被阻塞,数字为0则则表示该信号未被阻塞;
在保存表中,对应x位置的数字被设置成1时,表示进程收到了x号信号;
在处理表中,对应位置的处理一般分为SIG_DFL(默认处理),SIG_IGN(忽略处理),自定义处理,而在内核中我们可以看到SIG_DFL与SIG_IGN分别是由0和1强制类型转换来的。
注:阻塞和忽略是不同的,举个简单的例子,忽略就是已读不回,而阻塞是未读。
那我们有没有办法来修改这几个表呢?------有的,我们可以使用信号集操作函数来修改block表与pending表。
cpp
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
我们简单介绍一下上面这些函数
函数 sigemptyset 初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
函数 sigfillset 初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注: 在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化, 使信号集处于确定的状态。
初始化 sigset_t 变量之后就可以调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
sigismember 是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
在介绍完后,我们讲讲如何修改 Block 表和pending表,我们需要使用
cpp
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
sigprocmask()
函数用于获取和/或更改调用线程的信号屏蔽字。信号屏蔽字是指当前对于调用者被阻塞(即无法接收)的信号集合(有关更多详细信息,请参阅signal(7)
)。此调用的行为取决于
how
参数的值,具体如下:
SIG_BLOCK
- 被阻塞的信号集合是当前集合与
set
参数的并集。SIG_UNBLOCK
- 从当前被阻塞的信号集合中移除
set
中的信号。尝试解除一个未被阻塞的信号的阻塞是允许的。SIG_SETMASK
- 被阻塞的信号集合被设置为
set
参数指定的集合。如果
oldset
非空,则信号屏蔽字之前的值会被存储在oldset
中。如果
set
为空,则信号屏蔽字保持不变(即how
被忽略),但如果oldset
非空,当前信号屏蔽字的值仍会被存储在oldset
中。在多线程进程中,
sigprocmask()
的使用未指定;请参见pthread_sigmask(3)
。
还有
cpp
int sigpending(sigset_t *set);
sigpending()
函数返回调用线程(即调用该函数的线程)待处理的信号集合(也就是那些在阻塞期间被引发但尚未交付的信号)。待处理信号的掩码会返回在set
中。
测试代码如下
cpp
void PrintPending(sigset_t &pending)
{
// 打印1~31号信号
cout << "pending: ";
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
// 1. 我们想阻塞2号信号
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set, 2); // 到这里只是预备好了资源,并没有设置进内核中
// 2. 设置进内核
sigprocmask(SIG_SETMASK, &set, &oset); // 已经将2号信号设置为阻塞了!
// 3. 打印pending
sigset_t pending;
int cnt = 10;
while (true)
{
int n = sigpending(&pending);
if (n < 0)
{
lg(Fatal, "sigpending error!\n");
continue;
}
PrintPending(pending);
// 10秒后解除阻塞2号信号
sleep(1);
cnt--;
if (cnt == 0)
{
lg(Info, "unblock the signo 2\n");
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
return 0;
}
运行效果如下
接下来,我们对2号信号进行自定义捕捉,有
那么我们可以屏蔽所有信号吗?------当然不行,前面我们提到过的9号与19号信号都是不可被屏蔽的!
4. 信号的捕捉处理
①信号的处理与处理时机
我们先来看看信号是如何被处理的,在这里我们可以使用一张图来描述
在这个过程中我们也可以看到,当进程从内核态返回到用户态的时候,进行信号的检测与处理。那么什么时候才会切换身份呢?我们举几个例子
调用系统调用。OS是会自动做身份切换的,即 用户身份 ↔ 内核身份
汇编指令 int 80。这条汇编指令会使用户态陷入内核态
②再谈进程地址空间
在一个正常运转的OS中,用户页表有几份呢?------因为进程具有独立性,因此有几个进程就有几个用户级页表。那么内核页表有几份呢?------仅有1份!因为每个进程看到的3~4GB(即内核空间)的东西是一样的,整个OS中进程再怎么切换,内核空间的内容是不会变的。
对于进程来说,自己调用系统中的方法,就是在我进程地址空间中的代码区执行的;
对于OS来说,在任意一个时刻,都有进程正在运作,它不管什么时候想执行OS的代码,就可以马上执行。
因此,对于一个进程的工作状态来说,内核态就是能够访问OS的代码与数据,用户态就是只能访问自己的代码与数据。
③信号的捕捉
我们已经知道,信号在收到后会保存在三个表中,而信号的捕捉实际上就是在研究 pending 表什么时候由1->0,我们可以使用 sigaction 函数来对每个信号的行为,其接口如下
cpp
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
其中,signum 表示信号序号,act 是一个输入型参数,oldact 是一个输出型参数,而这个struct sigaction 结构体如下
cpp
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
测试代码如下
cpp
void handler(int signo)
{
lg(Info, "catch a signal, signal number: %d", signo);
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
sigaction(2, &act, &oact);
while(true)
{
lg(Info, "this is a process: %d", getpid());
sleep(1);
}
return 0;
}
运行效果如下
要研究 pending 表中的信号什么时候由1->0,我们在 handler 中加上一个打印函数来验证,即
cpp
void PrintPending(sigset_t &pending)
{
cout << "pending: ";
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
sigset_t set;
sigpending(&set);
PrintPending(set);
lg(Info, "catch a signal, signal number: %d", signo);
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
sigaction(2, &act, &oact);
while(true)
{
lg(Info, "this is a process: %d", getpid());
sleep(1);
}
return 0;
}
运行效果如下
从结果中,我们可以看到在打印 pending 表时,信号已经变为0,结论就是在执行捕捉方法时,是先将 pending 表清零,再调用 handler 方法。且在收到2号信号后,如果当前正在处于捕捉状态,2号信号的block表会设置成1,捕捉结束后变为0,这样可以防止出现各种嵌套调用,我们可以以下代码来测试
cpp
void PrintPending(sigset_t &pending)
{
cout << "pending: ";
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
sigset_t set;
sigpending(&set);
lg(Info, "catch a signal, signal number: %d", signo);
while (true)
{
PrintPending(set);
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
sigaction(2, &act, &oact);
while(true)
{
lg(Info, "this is a process: %d", getpid());
sleep(1);
}
return 0;
}
运行效果如下
可以看到,正在捕捉的时候,如果再传来相同的信号会被block表中的1阻塞。那在处理2号信号时,2号信号被自动屏蔽了,我们可不可以屏蔽更多信号呢?------可以设置 sigaction 结构体中的 sa_mask 参数即可!
④可重入函数与不可重入函数
假如有这样一个场景,我们想向链表中插入一个节点,即
如果现在 main 函数中调用了一次 insert (newnode1),如果在执行了①后,此时来了一条信号(这个信号的处理方法也是调用 insert(newnode2) ),执行后 head 节点指向 newnode2,newnode2 指向 node1,此时回到 main 函数的执行流,执行②此时就会将 head->next = newnode1,即
由于 insert 函数被 main 和 sighandler 执行流重复进入,导致了节点丢失与内存泄漏。
如果一个函数被重复进入的情况下出错了或者可能会出错,我们将其称为不可重入函数。反之,称为可重入函数。
目前,我们使用的大部分函数都是不可重入函数!
⑤从信号角度谈谈C++中的volatile
我们先来看下面这段代码
cpp
int flag = 0;
void handler(int signo)
{
lg(Info, "catch a signal, signo: %d", signo);
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag); // flag 0, !flag真
lg(Info, "process quit success!");
return 0;
}
g++ 可能会对这段代码进行优化,在优化条件下,flag 变量可能会被直接优化到CPU的寄存器中,比如我们使用 -O1进行优化,有如下情况
在未优化情况下,我们使用2号信号可以直接退出,其情况如下
使用优化后,我们不能退出,其情况如下
因为优化,导致我们在内存中不可见了!所以,我们可以在定义 flag 时加上 volatile 关键字,其核心作用就是防止编译器过度优化,保持内存的可见性!加上此关键字后我们再次运行有
⑥SIGCHLD
我们之前提到过,子进程在退出的时候并非是静悄悄地退出的。实际上子进程在退出的时候,会主动向父进程发送SIGCHLD(17号)信号,我们使用下面的代码验证
cpp
void handler(int signo)
{
lg(Info, "%d proccess catch a signal, signo: %d", getpid(), signo);
}
int main()
{
signal(17, handler);
pid_t id = fork();
if(id == 0) // child
{
while(true)
{
lg(Info, "this is child process, pid: %d, ppid: %d", getpid(), getppid());
sleep(1);
break;
}
exit(0);
}
// father
while(true)
{
lg(Info, "this is father process, pid: %d", getpid());
sleep(1);
}
return 0;
}
运行效果如下
所以,父进程在进行等待的时候,我们可以采用基于信号的方式进行等待。进程等待有如下的好处
获取子进程的退出状态,释放子进程的僵尸
即使不知道父子进程谁先运行,但一定是 father 进程先退出
我们还需要让父进程调用wait/waitpid接口,即父进程必须保证自己是一直在运行的。既然如此,我们也可以试着将子进程的进程等待写入信号捕捉中,如下
cpp
void handler(int signo)
{
pid_t rid = waitpid(-1, nullptr, 0);
lg(Info, "%d proccess catch a signal, signo: %d", getpid(), signo);
}
如过有多个子进程可以使用 while 循环捕捉,即
cpp
void handler(int signo)
{
pid_t rid;
while(rid = waitpid(-1, nullptr, WNOHANG) > 0)
{
lg(Info, "%d proccess catch a signal, signo: %d, %d child process quit succcess!", getpid(), signo, rid);
}
}
换一种说法,我们必须要进行进程等待吗?或者说必须调用 wait 接口吗?
也不是必须要这样做,我们可以使用 signal(17, SIG_IGN) 即可!
注:此方法对 Linux 有效,但是对类 Unix 不一定有效。
我们查看 signal 的手册
可以看到,OS 对17号信号默认执行的就是忽略操作,即17号信号的 SIG_DFL 的行为就是 SIG_IGN!