本篇博客让我们一起来康康信号部分的内容

系统为CentOS7.6,完整代码见 Gitee

[TOC]

1.什么是信号

在进程运行过程中,会出现各种各样的情况。操作系统需要用一套机制,来管理进程的事件

  • 进程退出
  • 进程停止
  • 进程恢复运行
  • ……

同时,这套管理机制是异步的,属于一种软(件)中断

和硬件中断打断处理器类似,软件中断打断进程的执,让其执行对应代码进行响应

1.1 何为异步?

以网购物品为例:当商品寄到自提点的时候,会给你发送一条取件的短信(信号)。此时我正在打游戏,没时间去处理这个快递(即取快递的行为并不是必须立马执行)

但这个时候,我已经知道有一个快递到了(知道自己获取到了一个信号)本质上就是知道了一会要去取快递(一会要处理信号)

当游戏一把打完了,我们就去取快递了(处理信号)

这就是一种异步的过程。因为你不知道你的快递什么时候会到站点,进程也不知道自己什么时候会收到一个信号

1.2 信号的种类

使用kill -l命令,我们可以看到目前linux系统下64种不同的类型。

其中前32为标准(Standard)信号,后32为实时(Real-time)信号;本篇博客只关注标准信号

这些信号,都是linux系统中预定义的

其中最常用的便是9号信号,来中断进程。平时我们最常用的CTRL+C,也是通过向进程发2号信号让进程退出的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[muxue@bt-7274:~/git]$ 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

include/linux/signal.h中,我们可以看到对信号的解释,以及其默认处理方法 default action

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
* In POSIX a signal is sent either to a specific thread (Linux task)
* or to the process as a whole (Linux thread group). How the signal
* is sent determines whether it's to one thread or the whole group,
* which determines which signal mask(s) are involved in blocking it
* from being delivered until later. When the signal is delivered,
* either it's caught or ignored by a user handler or it has a default
* effect that applies to the whole thread group (POSIX process).
*
* The possible effects an unblocked signal set to SIG_DFL can have are:
* ignore - Nothing Happens
* terminate - kill the process, i.e. all threads in the group,
* similar to exit_group. The group leader (only) reports
* WIFSIGNALED status to its parent.
* coredump - write a core dump file describing all threads using
* the same mm and then kill all those threads
* stop - stop all the threads in the group, i.e. TASK_STOPPED state
*
* SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
* Other signals when not blocked and set to SIG_DFL behaves as follows.
* The job control signals also have other special effects.
*
* +--------------------+------------------+
* | POSIX signal | default action |
* +--------------------+------------------+
* | SIGHUP | terminate |
* | SIGINT | terminate |
* | SIGQUIT | coredump |
* | SIGILL | coredump |
* | SIGTRAP | coredump |
* | SIGABRT/SIGIOT | coredump |
* | SIGBUS | coredump |
* | SIGFPE | coredump |
* | SIGKILL | terminate(+) |
* | SIGUSR1 | terminate |
* | SIGSEGV | coredump |
* | SIGUSR2 | terminate |
* | SIGPIPE | terminate |
* | SIGALRM | terminate |
* | SIGTERM | terminate |
* | SIGCHLD | ignore |
* | SIGCONT | ignore(*) |
* | SIGSTOP | stop(*)(+) |
* | SIGTSTP | stop(*) |
* | SIGTTIN | stop(*) |
* | SIGTTOU | stop(*) |
* | SIGURG | ignore |
* | SIGXCPU | coredump |
* | SIGXFSZ | coredump |
* | SIGVTALRM | terminate |
* | SIGPROF | terminate |
* | SIGPOLL/SIGIO | terminate |
* | SIGSYS/SIGUNUSED | coredump |
* | SIGSTKFLT | terminate |
* | SIGWINCH | ignore |
* | SIGPWR | terminate |
* | SIGRTMIN-SIGRTMAX | terminate |
* +--------------------+------------------+
* | non-POSIX signal | default action |
* +--------------------+------------------+
* | SIGEMT | coredump |
* +--------------------+------------------+
*
* (+) For SIGKILL and SIGSTOP the action is "always", not just "default".
* (*) Special job control effects:
* When SIGCONT is sent, it resumes the process (all threads in the group)
* from TASK_STOPPED state and also clears any pending/queued stop signals
* (any of those marked with "stop(*)"). This happens regardless of blocking,
* catching, or ignoring SIGCONT. When any stop signal is sent, it clears
* any pending/queued SIGCONT signals; this happens regardless of blocking,
* catching, or ignored the stop signal, though (except for SIGSTOP) the
* default action of stopping the process may happen later or never.
*/

这也意味着:即便没有接收到信号,进程也具备有识别和处理这个信号的能力!因为在系统中,已经给每一个进程和信号指定了默认动作!

1.3 信号产生

有很多情况会产生信号

  • 系统接口(kill命令)
  • 键盘产生(CTRL+R CTRL+\)
  • 软件条件(进程停止,进程运行完退出)
  • 硬件异常(比如除0错误)

1.4 信号动作

既然有默认动作,那肯定也有非默认的了。实际上,一个进程对信号的处理分为三种不同的方式

  • 默认动作
  • 自定义动作
  • 忽略

前面提到,一个进程并不一定需要立刻处理一个信号。那么它一定需要有一个办法来记住自己收到的信号。

而存储信号,是由进程的PCB来完成的!

细心的你可能会发现,进程中的信号一共是64个,刚好是8个字节!我们可以通过位图结构,用两个int类型来存放一个进程收到的各种信号。

image-20221118185053908

在系统内核中,分别有三个表,用来存放进程的信号。而这些信号在位图中的位置,就是在handler方法集中处理动作的下标

1
2
3
block - 1表示该进程屏蔽这个信号
pending - 表示进程收到了什么信号,1代表收到且未处理
handler - 每一个信号所对应的处理方法,默认/忽略/自定义

这一切都是处于进程PCB中的,只有操作系统能为我们管理。所以操作系统提供了相关的接口,方便我们对进程信号进行自定义设置。

pending表中的信号只能保存一个,如果一个信号尚未处理,该位图为1;另外一个相同信号到来的时候,会被直接丢弃掉。(pending表只能记住一个信号)

handler表中的两个宏如下:

  • SIG_DFL 默认方法
  • SIG_IGN 忽略
  • 忽略是信号处理的一种方式,我们能正常收到这个信号,处理方法是不管他

2.系统接口

2.1 signal

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

这个函数可以用于设置某个信号的处理方法。如果设置成功,则返回值为这个信号的旧处理动作;不过我们一般不关心这个函数的返回值。

1
2
RETURN VALUE
signal() returns the previous value of the signal handler, or SIG_ERR on error. In the event of an error, errno is set to indicate the cause.

比如我们将键盘退出的2号自定义一个回调函数,那么就不能用ctrl+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
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void handler(int signo)
{
cout << "process get signal: " << signo << endl;
}

int main()
{
//将二号信号设置一个回调,其余信号不做处理
signal(2, handler);
cout << "进程信号已经设置完了" << endl;
sleep(3);

while (true)
{
cout << "进程正在运行: " << getpid() << endl;
sleep(1);
}

return 0;
}

因为是系统调用接口,所以我们的当前进程处于什么状态,都不会影响这个回调办法的调用。

比如CTRL+C的默认策略是终止某个前台进程,但在我们之前的测试中,即便这个进程在sleep或者是在某些共享资源中进行阻塞等待,它依旧能被CTRL+C所发送的2号信号终止。

同理,在设置了自定义的信号捕捉函数后,我们发送信号的时候,操作系统也能根据当初设置的回调函数(底层有对应的结构体来存储和这个进程相关的信号处理函数)成功将其调用。

2.1.1 前台进程和后台进程

这里对ctrl+c的作用进一步描述,它只能用来中断一个前台进程

1
./test #我们直接运行一个进程,就是前台进程

之前这种直接运行进程,在bash上打印内容的方式,都是一个前台进程,可以用ctrl+c终止;我们可以在后面加上&设置为一个后台进程

&只是临时在后台运行,bash关闭后会终止;如果想持久在后台运行,需要在命令最前面加上nohup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./test &
[1] 8898
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
^C
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
^C
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898

这时候这个进程会一直在当前bash的后台打印,期间我们可以执行其他的命令,但是它依旧会不停的打印。ctrl+c无法终止这个进程,因为它并没有在前台运行!

1
2
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ps jax | grep test
22965 8898 8898 22965 pts/22 22965 S 1001 0:00 ./test

ps命令查看,可以看到其运行态为S;而前台进程,运行态为S+

1
2
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ps jax | grep test
22965 9664 9664 22965 pts/22 9664 S+ 1001 0:00 ./test

不过,虽然我们不能用CTRL+C终止这个进程,但使用kill -2发送2号信号,是可以终止掉这个进程的(前提是没有自定义2号信号的方法)

1
2
3
4
5
6
7
8
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程信号已经设置完了
进程正在运行: 10464
进程正在运行: 10464
进程正在运行: 10464
进程正在运行: 10464
进程正在运行: 10464
[1]+ Interrupt ./test
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

2.1.2 循环捕捉所有信号

我们可以用一个for循环,捕捉所有的信号;所有信号的列表可以通过kill -l命令获取到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//.....
//其余代码同上
int main()
{
//对所有的进程信号都设置一个回调
for (int sig = 1; sig <= 31; sig++)
{
signal(sig, handler);
}
//signal(2, handler);//将二号信号设置一个回调,其余信号不做处理
cout << "进程信号已经设置完了" << endl;
sleep(3);

while (true)
{
cout << "进程正在运行: " << getpid() << endl;
sleep(1);
}

return 0;
}

设置了之后,对应的信号都会调用我们自己写的函数。但有一个例外,那便是kill -9,九号信号是不能被自定义捕获的!

image-20221118193453220

2.1.3 信号9/19

LINUX下,9号信号是一个管理员信号,具有杀死进程的最高权限,不能被自定义捕捉

你想啊,要是linux不对9号进行限制,那我把所有信号都捕捉了,岂不是这个进行没有办法被外部中止了?小病毒啊!😂

和9号信号一样不能被屏蔽的,还有19号信号SIGSTOP

1
2
3
4
5
6
7
8
9
10
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process running: 7779
process running: 7779
process running: 7779
process running: 7779
process running: 7779

[1]+ Stopped ./tsig
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

而6号信号可以被捕捉,自定义捕捉方法执行后,进程依旧会退出。参考2.4 abort

2.2 kill

kill不仅是一个系统命令,同时还有一个系统接口;

一般这种情况,用man kill查看命令的文档,man 2 kill查看接口函数

之前我以为它只是一个用来干掉进程的命令(毕竟kill就是这个意思)现在才知道原来它的作用是给进程发信号

1
2
3
4
5
//kill - send signal to a process
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

它的返回值很简单,如果成功发送信号,则返回0,否则返回-1并且更新errno

1
2
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately

所以我们可以写一个简单的函数实现,来制作一个自己的kill命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void mykill(int argc,char *argv[])
{
if(argc != 3)
{
cout << "Usage: " << argv[0] << " signo-id process-id" <<endl;
exit(1);
}

if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
{
cerr << "kill: " << strerror(errno) << endl;
exit(2);//出现错误
}
exit(0);//正常执行
}
//argc和argv是命令行参数
//argc传入命令个数,包括./test
//argv传入命令的字符串地址
int main(int argc, char *argv[])
{
mykill(argc,argv);

return 0;
}

成功发送了信号!

image-20221118230540961

如果我们使用错误的时候,则会发送提示信息👍

1
2
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./mkill
Usage: ./mkill signo-id process-id

2.2.1 killall

这个接口可以通过进程名向所有这个名字的进程发信号

1
[muxue@bt-7274:~/git]$ killall tsig

通过测试可以发现,它发送的是第15号信号

1
2
3
4
5
6
7
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process running: 5846
process running: 5846
process 5846 get signal: 15
process running: 5846
process running: 5846

2.3 raise

这个系统接口的作用是给自己发信号

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

返回0代表调用成功,非0代表失败

1
2
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.

用下面的代码进行测试,进程会不断的给自己发送2号信号

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
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void handler(int signo)
{
cout << "process get signal: " << signo << endl;
}

void TestSignal()
{
signal(2, handler);//将二号信号设置一个回调,其余信号不做处理
cout << "进程信号已经设置完了" << endl;
sleep(3);
}

int main(int argc, char *argv[])
{
TestSignal();//设置对进程信号的屏蔽
while(1)
{
raise(2);
sleep(1);
}

return 0;
}

此时能看到每一秒会调用我们自己写的handler方法,打印收到2号信号

1
2
3
4
5
6
7
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process get signal: 2
process get signal: 2
process get signal: 2
process get signal: 2
process get signal: 2

2.4 abort

向自己发送6) SIGABRT信号

1
2
#include <stdlib.h>
void abort(void);

还是2.3中的代码,将raise(2)修改为abort(),同时捕捉6号信号。

此时能观察到我们自己写的handler方法的确被调用了,但是进程依旧终止了

1
2
3
4
5
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process get signal: 6
Aborted
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

这说明6号信号有一个特性:可以被捕捉执行自定义方法,但执行完毕之后需要退出

相比之下,9号信号是不能被捕捉


2.5 alarm

这个接口的作用是一个定时器,设定秒数,时间到了之后,会收到14) SIGALRM信号

1
2
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
1
2
RETURN VALUE
alarm() returns the number of seconds remaining until any previously scheduled alarm was due to be delivered, or zero if there was no previously scheduled alarm.

用下面的代码进行测试

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
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void handler(int signo)
{
cout << "process get signal: " << signo << endl;
}

void TestSignal()
{
//对所有的进程信号都设置一个回调
for (int sig = 1; sig <= 31; sig++)
{
signal(sig, handler);
}
cout << "进程信号已经设置完了" << endl;
sleep(3);
}

int main(int argc, char *argv[])
{
TestSignal();//设置对进程信号的屏蔽
alarm(4);//4s后向自己发送14信号
cout << "set alarm, sleep" << endl;
sleep(8);
cout << "sleep finish"<<endl;

return 0;
}

可以看到在休眠期间,进程收到了14号信号。此时进程并没有退出,而是继续休眠

1
2
3
4
5
6
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
set alarm, sleep
process get signal: 14
sleep finish
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

如果我们不对14号信号自定义捕捉,则会直接退出进程

1
2
3
4
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
set alarm, sleep
Alarm clock
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

另外需要注意的是,alarm信号本身不会让进程休眠。如果进程在alarm信号设定秒数之前结束,则什么事情都不会发生

2.6 sigset_t信号集

这是一个数据类型,其为block/pending位图的存储结构,被称作信号集/信号屏蔽字

虽然我们能直接使用这个类型, 但是对这个信号集中的位图操作必须要调用系统接口来完成

1
2
3
4
5
6
7
#include <signal.h>
int sigemptyset(sigset_t *set);//初始化位图(清空)
int sigfillset(sigset_t *set);//全部置为1
int sigaddset(sigset_t *set, int signum);//设置位图中某一位的数据
int sigdelset(sigset_t *set, int signum);//删除位图中某一位的数据
//判断某一位信号是否在该集合中
int sigismember(const sigset_t *set, int signum);

2.7 sigprocmask

更改或则获取当前进程的信号屏蔽字

1
2
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

其中第一个参数为处理方法,分别有下面三种

  • SIG_BLOCK 将set参数中包含的位数设置为屏蔽
  • SIG_UNBLOCK 将set参数中包含的位数解除屏蔽
  • SIG_SETMASK 将当前的信号屏蔽字设置为set(覆盖)

第三个参数是一个输出型参数。如果传入了oldset,那么旧的信号屏蔽字会被放入oldset

1
2
3
4
5
6
7
8
9
10
11
12
13
SIG_BLOCK
The set of blocked signals is the union of the current set and the set argument.

SIG_UNBLOCK
The signals in set are removed from the current set of blocked signals. It is permissible to attempt to unblock a signal which is not blocked.

SIG_SETMASK
The set of blocked signals is set to the argument set.

If oldset is non-NULL, the previous value of the signal mask is stored in oldset.

RETURN VALUE
sigprocmask() returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.

如果用该接口接触了对某一个信号的阻塞,那么在该函数return前,至少其中一个消息被送达

2.8 sigpending

获取当前进程的pending信号集

1
2
#include <signal.h>
int sigpending(sigset_t *set);

参数为一个输出型参数。正确获取返回0,否则-1

这时候我们就可以写一个简单的函数来打印当前进程的信号集了

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
//打印信号集的内容
void showPending(sigset_t* pdg_ptr)
{
for(int i=1;i<32;i++)
{
if(sigismember(pdg_ptr,i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}

int main()
{
sigset_t pdg;
while(1)
{
sigemptyset(&pdg);//初始化信号集
if(sigpending(&pdg)==0)//获取
{
showPending(pdg);//获取成功,打印
}
else
{
cout << getpid() << " get pending err"<<endl;
}
sleep(1);
}
return 0;
}

运行之后可以看到,程序一直在打印当前进程的信号集

1
2
3
4
5
6
7
8
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
start process: 30981
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000

不过此时我们并没有对信号进行屏蔽,所以给这个进程发信号会被立即处理(递达)不能在pending表中观察到现象

2.8.1 屏蔽2号信号

此时尝试使用sigprocmask来屏蔽某一个信号,再来观察情况

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
int main(int argc, char *argv[])
{
//block掉2号信号
sigset_t nsig,osig;
sigemptyset(&nsig);
sigemptyset(&osig);
sigaddset(&nsig,2);//在nsig中设置2为1
sigprocmask(SIG_BLOCK,&nsig,&osig);//添加屏蔽

cout << "start process: " << getpid() << endl;
sigset_t pdg;
while(1)
{
sigemptyset(&pdg);//初始化信号集
if(sigpending(&pdg)==0)//获取
{
showPending(&pdg);//获取成功,打印
}
else
{
cout << getpid() << " get pending err"<<endl;
}
sleep(1);
}

return 0;
}

可以看到当我们键入CTRL+C的时候,2号信号被block了没有处理,pending表上的2号信号就会变为1,且多次CTRL+C不会有变化

1
2
3
4
5
6
7
8
9
10
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
start process: 3608
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000

2.8.2 屏蔽所有信号

可以用一个循环设置所有的信号位,让当前进程屏蔽掉所有信号

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
int main(int argc, char *argv[])
{
//block掉所有信号
sigset_t nsig,osig;
sigemptyset(&nsig);
sigemptyset(&osig);
for(int i=1;i<32;i++)
{
sigaddset(&nsig,i);//在nsig中设置2为1
}
sigprocmask(SIG_BLOCK,&nsig,&osig);//添加屏蔽

cout << "start process: " << getpid() << endl;
sigset_t pdg;
while(1)
{
sigemptyset(&pdg);//初始化信号集
if(sigpending(&pdg)==0)//获取
{
showPending(&pdg);//获取成功,打印
}
else
{
cout << getpid() << " get pending err"<<endl;
}
sleep(1);
}

return 0;
}

运行之后可以观察到,不管给这个进程发几号信号,都会被屏蔽显示在pending集中;9号信号依旧是老大哥,不受影响,依旧能干掉这个进程

image-20221120183259473

2.8.3 解除屏蔽

如果在设置屏蔽之后,休眠15s(在此期间接收信号)再接触对信号的屏蔽

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
int main(int argc, char *argv[])
{
//block掉所有信号
sigset_t nsig,osig;
sigemptyset(&nsig);
sigemptyset(&osig);
for(int i=1;i<32;i++)
{
sigaddset(&nsig,i);//在nsig中设置2为1
}
sigprocmask(SIG_BLOCK,&nsig,&osig);//添加屏蔽

TestSignal();//设置信号自定义处理
cout << "start process: " << getpid() << endl;
sigset_t pdg;
int k=15;
while(k--)
{
sigemptyset(&pdg);//初始化信号集
if(sigpending(&pdg)==0)//获取
{
showPending(&pdg);//获取成功,打印
}
else
{
cout << getpid() << " get pending err"<<endl;
}
sleep(1);
}

//利用osig恢复之前的block表
sigprocmask(SIG_SETMASK,&osig,nullptr);
sigemptyset(&pdg);//初始化信号集
if(sigpending(&pdg)==0)//获取
{
showPending(&pdg);//获取成功,打印
}
sleep(10);
cout << "process quit"<<endl;

return 0;
}

此时就能观察到,信号被立马处理,pending表变为全0

image-20221120184159874

2.9 sigaction

1
2
3
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

这个函数的参数和signal函数很相似,不过都变为了一个结构体。这个接口也可以用来处理实时信号(不在文本考虑范围内)

  • 第一个参数是需要处理信号的编号
  • 第二个参数是自定义的action
  • 第三个参数是输出型参数,可以获取到旧的处理方法;如果你不关心旧的处理方法,可以将它置为NULL空指针;

设置成功后返回0,出错返回-1;

这个结构体的成员如下:

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int);//对信号的处理方法
void (*sa_sigaction)(int, siginfo_t *, void *);//可忽略
sigset_t sa_mask;//参考2.8中的处理方法
int sa_flags;//设为0
void (*sa_restorer)(void);//可忽略
};

2.9.1 基本使用

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
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

//打印收到的信号
void handler(int signo)
{
cout << "process " << getpid() << " get signal: " << signo << endl;
}

int main(int argc, char *argv[])
{
struct sigaction nact,oact;
nact.sa_flags = 0;
nact.sa_handler = handler;
sigemptyset(&nact.sa_mask);//初始化

sigaction(2,&nact,&oact);

while(1)
{
cout << "process running: " << getpid() << endl;
sleep(2);
}

return 0;
}

运行之后,我们自定义捕捉了2号信号,成功调用自己的handler方法!

1
2
3
4
5
6
7
8
9
10
11
12
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
process running: 15029
process running: 15029
^Cprocess 15029 get signal: 2
process running: 15029
process running: 15029
^Cprocess 15029 get signal: 2
process running: 15029
process running: 15029
process running: 15029
^\Quit
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

请注意,信号处理函数在自定义捕获后,进程接收到信号,会调用自定义捕捉的函数,再继续往后运行。所以如果你捕获了2号信号,用ctrl+c来退出的时候,理论上也是会调用对象的析构函数的(前提是你的进程里面没有死循环)。

信号2终止后是否会调用析构函数?

上面的代码中,我们有死循环,所以再接受到信号处理后,调用完毕函数,就会自动回到死循环的位置。

在下面的函数中,我们的main是直接在sleep里面阻塞等待的,那么在获取到ctrl+c的信号后,会先调用信号处理函数,再回到main函数中继续往后执行,最终调用对象的析构。

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
// 上面的代码省略了
// mytest和myclass是两个类,类中的构造和析构都添加了打印
// mytest中用智能指针包裹了一个myclass对象
class mytest{
public:
mytest(int a):_ptr(make_shared<myclass>(a))
{
cout << "init mytest" << endl;
}
~mytest()
{
cout << "destroy mytest" << endl;
}
private:
shared_ptr<myclass> _ptr;
};

int main()
{
RegisterSigTermHandler(); // 捕捉2号信号

mytest t1(1);
map<int,mytest> tmap;
for(int i=0;i<6;i++)
{
tmap.emplace(i,t1);
}
cout << "----" << endl;
tmap.clear();
cout << "----" << endl;
sleep(10); // 在这里ctrl+c,接收到信号,运行完毕信号处理函数后,会继续往后运行。
cout << "-- end --" << endl;

return 0;
}

这个程序的终端输出如下,在接收到信号后,会先调用信号处理函数,打印singal字符串,再返回到main函数,打印-- end --字符串,最终退出main函数,析构掉两个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> ./test
init myclass
init mytest
----
destroy mytest
destroy mytest
destroy mytest
destroy mytest
destroy mytest
destroy mytest
----
^Csingal
-- end --
destroy mytest
destroy myclass

记录这一点,是因为GPT告诉我如果是接收到信号退出是不会执行析构函数的。然而情况并非如此。析构函数依旧会被正常执行!

那如果有死循环的话,应该如何保证对象的析构能被正常执行呢?

  • 在死循环中加上if(全局变量)的判断逻辑,并在信号处理函数中设置变量
  • 在信号处理函数中手动调用一个shutdown函数,为了避免一个函数写两次,可以在析构函数中也调用这个shutdown函数。

但是,如果你的进程中是多层嵌套循环的话,最终的退出可能没有完全退出,会导致析构函数不被正常调用的资源泄漏问题!如果你需要使用的资源中包含IPC(进程间通信)这些必须要被删除的对象的话,这个问题就需要注意了。

比如你进程退出的时候析构函数没有调用,没有删除掉消息队列,下一次启动的时候,消息队列读端如果先启动,有可能会读取到上次冗余的脏数据。

所以我们要保证IPC资源能被正常释放。

2.9.2 sa_mask

这个成员是一个sigset_t类型,用于当处理一个信号的时候,连带屏蔽其他信号;

  • 当一个进程正在处理A信号的时候,操作系统会把A信号自动添加入Block表,屏蔽该信号(不允许同时处理两个A信号,避免信号A的递归式处理

如果你想在处理2号信号的时候,阻塞掉3、4、5号信号,就可以对sa_mask进行设置,设置方法参考2.8的操作

因为现在我演示的自定义方法只是一个再简单不过的示例,实际上进程收到信号的时候需要根据不同情况进行不同的自定义处理,这些自定义处理的过程可能会很长。此时就可以block掉其他的信号,不让它们影响当前进程运行的自定义方法

3.软件崩溃的本质

之前我们经常会遇到软件出错奔溃的情况,那么奔溃的本质是什么呢?

3.1 情景演示

用下面的一个除零错误作为演示

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 <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void handler(int signo)
{
cout << "process " << getpid() << " get signal: " << signo << endl;
}

void TestSignal()
{
//对所有的进程信号都设置一个回调
for (int sig = 1; sig <= 31; sig++)
{
signal(sig, handler);
}
cout << "进程信号已经设置完了" << endl;
sleep(3);
}


int main(int argc, char *argv[])
{
TestSignal();
int a = 10;
int b = 0;
try
{
int c = a / b; // C++的除0不是异常,不会抛出
//所以会直接linux系统运行报错
}
catch (const exception &e)
{
cerr << "a/0 err" << endl;
abort();
}
catch (...)
{
cout << "base catch" << endl;
abort();
}
return 0;
}

运行了之后,该进程会一直收到8号信号,直到我们手动kill掉这个进程

1
2
3
4
5
6
process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8Killed

你可能会觉得奇怪,不是用try/catch进行了异常处理吗?为什么没有用呢?

那是因为,在C++中,并不会将除零错误当作一个异常进行处理!

QQ图片20220413084241


我们自定义捕捉了8号信号,没能让进程终止。但此时这个进程已经出现了一个严重的bug,操作系统就会一直给进程发这个信号

1
2
3
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
Floating point exception
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

相比之下,如果不自定义捕捉,则会直接报错+终止进程

8号信号是SIGFPE,FPE即为Floating point exception的缩写!

3.2 说明

崩溃的本质,是该进程收到了异常信号,从而终止。

以除零错误为例,CPU内部会有一个状态寄存器,检测到用户进行除零计算的时候,会将状态寄存器设置为浮点数错误。当操作系统检测到这个错误的时候,便会向当前正在运行的进程发送8号信号。而我们的进程在收到信号的时候,会处理这个信号,默认的处理方法就是终止进程!

同理,当我们访问一个野指针的时候,操作系统能在虚拟地址转换的时候发现这个问题,向我们的进程发送11号信号

1
2
3
4
5
6
7
int main(int argc, char *argv[])
{
TestSignal();
int *p;
*p=20;
return 0;
}

11) SIGSEGV代表段错误,写OJ题目的时候这个报错很常见😂

1
2
3
4
5
process 6754 get signal: 11
process 6754 get signal: 11
process 6754 get signal: 11
process 6754 get signal: 11
process 6754 get signal: 11Killed

4.coredump

进程控制的博客中,提到当进程因为信号终止的时候,其status中的0-7位会是对应的终止信号,而第8位是该进程的core dump标记位

image-20221120110639470

1.2贴出来的源码注释中可以看到,有不少信号的默认动作是进行core dump,比如8号信号。那么这个东西到底是什么玩意呢?


通过fork创建子进程,让子进程除零产生8号信号,子进程退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char *argv[])
{
int status;
int id = fork();
if(id == 0)
{
//子进程
int b=0;
int a = 10/b;
}

int ret = waitpid(id,&status,0);
//打印子进程的退出信息
printf("exitcode:%d signo:%d coredump: %d\n",(status>>8)&&0xff,status&0x7f,(status>>7)&0x1);

return 0;
}

此时可以观察到,coredump标记位为0

1
2
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
exitcode:0 signo:8 coredump: 0

4.1 开启该功能

默认情况下,我们云服务器的core dump功能是被关闭的,需要我们手动开启;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -a
core file size (blocks, -c) 0 #coredump功能被关闭了
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 14691
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) 14691
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

使用ulimit -a命令指定core file的大小

1
2
3
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -c 10000
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -a
core file size (blocks, -c) 10000

再次运行刚刚的代码,可以看到标记位为1,并且产生了一个core.27908文件,这个文件的后缀是产生coredump文件的进程pid

1
2
3
4
5
6
7
8
9
10
11
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
exitcode:0 signo:8 coredump: 1
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ll
total 292
-rw------- 1 muxue muxue 593920 Nov 20 12:34 core.27908
-rw-rw-r-- 1 muxue muxue 194 Nov 20 10:31 makefile
-rw-rw-r-- 1 muxue muxue 601 Nov 20 09:57 mkill.cc
-rw-rw-r-- 1 muxue muxue 203 Nov 20 09:56 test.cc
-rwxrwxr-x 1 muxue muxue 13768 Nov 20 12:34 tsig
-rw-rw-r-- 1 muxue muxue 1772 Nov 20 12:34 tsignal.cpp
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

vscode告诉我们这个不是一个普通的文本文件

image-20221120123842724

这个现象告诉我们,默认动作是coredump的信号,会让进程退出,将coredump标记位置为1并且产生一个core.文件

4.2 使用coredump

这个功能会将进程在运行中产生异常的上下文数据,执行core dump(核心转储)为一个文件,方便我们debug

如下所示,使用-g命令以debug模式编译test.cc,运行的时候可以看到除零错误之后跟了一个(core dumped)提示我们进行了core dump操作,对应产生了一个core.文件

1
2
3
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ g++ test.cc -g -o test
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./test
Floating point exception (core dumped)

这时候打开gdb,输入core-file 文件名加载文件,就可以直接定位到出错代码的位置!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ gdb test
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/muxue/git/linux/code/22-11-16_signal/test...done.
(gdb) core-file core.31997
[New LWP 31997]

Core was generated by `./test'.
Program terminated with signal 8, Arithmetic exception.
#0 0x000000000040065c in main () at test.cc:13
13 int a=10/0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64
(gdb)
(gdb)

这可比我们手动debug找错误方便多了

QQ图片20220419102702

4.3 为什么默认关闭?

你可能会觉得,这个功能不挺好的吗,为啥默认没有开启呢?

先来看看这个文件的大小,足足有580KB

1
2
3
4
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ls -lht
total 292K
-rw-rw-r-- 1 muxue muxue 1.8K Nov 20 12:37 tsignal.cpp
-rw------- 1 muxue muxue 580K Nov 20 12:34 core.27908

一般而言,服务端运行的一些进程,都需要保持稳定性。比如B站的服务器挂了,第一时间要做的是重启服务进程(并不是重启服务器机器)

如果设置了这个coredump,当服务器进程因为错误退出的时候,会生成一个core.文件;这时候有一个守护进程(用来监视并及时重启服务器进程)发现服务器进程退出了,就会重启它。

这时候又遇到了刚刚那个bug,服务器进程又退出了,守护进程又来重启它……

如此往复,就会生成非常非常多的core.文件,塞满我们的硬盘。

对于求稳为主的服务器而言,这可不是一个好事。所以云服务器上默认禁止了这个功能。

1
2
3
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -c 0
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -a
core file size (blocks, -c) 0

umlimit -c 0指定大小为0关闭该功能


5.进程处理信号

前面八八了这么一大堆,进程到底是什么时候来处理信号的呢?

  • 开门见山:进程从内核态切换成用户态的时候,处理信号

5.1 内核态/用户态

在程序地址空间的博客中,提到了每一个进程都有1gb的内核空间;该内核空间用于内核级页表的映射,即映射操作系统的物理内存!

image-20221120185234159

有内核级页表的存在,无论进程怎么切换,都能找到操作系统内核的代码和数据,前提是有权限访问。

  • CPU中的CR3状态寄存器会标识当前进程处于内核态还是用户态
  • 内核态可以访问所有代码和数据,权限最高
  • 用户态只能访问当前进程自己的数据

当我们进程需要执行内核接口的时候,就需要将进程切换为内核态;运行完毕之后,切换回用户态。

当我们进程出现了异常,会从用户态切换成内核态,由操作系统检测相关异常并向进程发送对应信号。

当我们进程的时间片到了(需要切换进程)也会从用户态转为内核态,由操作系统来进行进程切换。

5.2 信号检测

当进程从内核态切换回用户态的时候,会进行信号的检测和处理。此时判断pending表中是否有未处理信号,以及该信号是否有被block。如果一个信号没有被block,则将该信号递达给进程,执行对应的处理方法

  • 执行用户的自定义方法时,应该以什么身份执行?

注意,当我们给一个信号指定了自定义处理方法,就代表该信号的处理方法是用户提供的。此时需要以用户的身份去执行这个代码,才能正确访问用户级页表。

这么做也能避免恶意代码的注入。如果有人在自定义方法中写一个修改系统内核的恶意代码,也能被操作系统发现并阻止。

这个过程可以用下面这张图来解释(并非完整过程,仅供参考理解)

image-20221121142458166

每次处理完信号后,会返回用户进程,从上一次中断的位置开始继续往后运行

6.可重入函数

1
2
3
4
5
6
//头插
void insert(Node* p)
{
p->next=head;
head=p;
}

上面这个函数是一个非常简单的链表头插函数

如果我们这个头插函数处理的是一个全局的链表,就可能会因为用户态、内核态的切换,函数重入造成错误

image-20221121211426452

所以insert就是一个不可重入函数!除了这个头插,还有一些其他的函数也符合这个特效:

  • 调用了malloc或者free(可能会多次malloc和多次free)
  • 调用了I/O库的函数
  • 绝大多数库函数都是不可重入的,比如C++的STL库

依此类推,如果一个函数只访问他自己的局部变量,不会影响其他参数。那么他就是一个可重入函数

6.1 原子操作

所谓原子操作是指不会被 线程调度 机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)

  • 如果一个函数进行了对全局变量的原子操作,那么它也是可重入的函数。因为原子操作不包含中间态
  • 如果一个函数对全局变量进行了非原子操作(比如上面的链表头插)那么他就是一个不可重入函数

7.volatile

之前的学习中就已经知道,这个关键字的作用是每一次访问变量的时候,都必须要去内存中取


假设我们进程中需要通过一个全局变量进行条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
int flag=0;
int main()
{
if(flags)
{
//..
}
else
{
//..
}
return 0;
}

如果我们自定义捕捉了一个信号,收到该信号的时候,会修改flag,执行if/else语句中对应的代码。

由于编译器的优化问题,每一次访问flag的时候,它可能不会每次都去内存中取,就会出现一个问题

  • 寄存器中 flag=0
  • 经过自定义捕捉函数处理,内存中 flag=1

这两个flag在if条件中会导向不同的结果!

为了避免这种可能因为平台、编译器、优化问题导致的代码bug,我们需要告诉所有编译器,不准对flag变量做任何优化处理,必须要老老实实的去内存中拿这个变量的数据!

1
2
//volatile保持内存的可见性
volatile int flag = 0;

7.1 示例

gcc编译器可以通过-O2指定较高的优化等级

以下面的代码为例

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
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

// 保持内存的可见性
int flag = 0;

void handler(int signo)
{
flag = 1;
printf("\n更改flags: 0->1\n");
}

int main()
{
printf("process start %d\n",getpid());
signal(2, handler);//自定义捕捉2号信号

while (!flag)
;//啥事不干的循环

printf("process exit!\n");
return 0;
}

运行之后,键入CTRL+C,你会发现进程依旧没有退出!理论上来说flags=1!flags为假,应终止循环,退出进程才对!

1
2
3
4
5
6
7
8
9
10
11
12
13
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ gcc test.c -o test -O2
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ ./test
process start 22333
^C
更改flags: 0->1
^C
更改flags: 0->1
^C
更改flags: 0->1
^C
更改flags: 0->1
^\Quit
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$

如果我们加上volatile关键字,则不会出现这个问题,进程能够正常退出

1
2
3
4
5
6
7
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ gcc test.c -o test -O2
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ ./test
process start 23086
^C
更改flag: 0->1
process exit!
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$

去掉gcc编译器的优化参数,去掉volatile关键字,会发现进程也能正常退出

1
2
3
4
5
6
7
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ gcc test.c -o test
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ ./test
process start 23224
^C
更改flag: 0->1
process exit!
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$

这就是编译器优化不同的影响!加上volatile关键字能避免这个问题,使代码运行能有唯一结果!

8.子进程发送信号

当子进程的状态变化的时候,会向父进程发送17号信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void testfork()
{
int status;
int id = fork();
if(id == 0)
{
//子进程
cout << "chlid process: " <<getpid()<<endl;
int b=0;
int a = 10/b;
}
TestSignal();
int ret = waitpid(id,&status,0);
//打印子进程的退出信息
printf("exitcode:%d signo:%d coredump: %d\n",(status>>8)&&0xff,status&0x7f,(status>>7)&0x1);
}

观察结果,可以看到父进程收到了子进程的17号信号,此时子进程因为错误退出

1
2
3
4
5
6
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
chlid process: 25319
process 25318 get signal: 17
exitcode:0 signo:8 coredump: 0
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

除了退出时会发送信号,子进程暂停、继续运行的时候,都会向父进程发送信号

image-20221121215300234

8.1 父进程回收子进程

既然父进程能收取到子进程发送的信号,那么我们自定义捕捉17号信号,在其中调用waitpid,即可在子进程出错或执行完毕需要退出的时候,进行父进程回收

这样就不需要在main函数中进行主动wait了

操作并不难,我们只需要将17号信号进行自定义捕捉,在捕捉函数中执行waitpid进行等待即可

  • 注意:这里的waitpid必须使用WNOHANG进行非阻塞等待
  • 当父进程正在阻塞等待子进程A的时候,它在处理17号信号,新的17号信号无法收到
  • 这就会导致在A之后种子的子进程,无法向父进程发送有效的17号信号
  • 从而出现了僵尸进程(未回收的子进程)

使用非阻塞等待,即可以避免此问题,又能让父进程在等待的过程中处理自己的其他工作,一举两得。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>

using namespace std;

void FreeChld(int signo)
{
assert(signo == SIGCHLD);
while (true)
{
//如果没有子进程了,waitpid就会调用失败
pid_t id = waitpid(-1, nullptr, WNOHANG); // 非阻塞等待
if (id > 0)
{
cout << "父进程等待成功, child pid: " << id << endl;
}
else if(id == 0)
{
//还有子进程没有退出
cout << "尚有未退出的子进程,父进程继续运行" << endl;
break;//退出等待子进程
}
else
{
cout << "父进程等待所有子进程结束" << endl;
break;
}
}
}

int main()
{
signal(SIGCHLD, FreeChld);
for (int i = 0; i < 5; i++)
{
pid_t id = fork();
if (id == 0)
{
//子进程
int cnt = 8;
while (cnt)
{
cout << "子进程 pid: " << getpid() << " cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出,进入僵尸状态: " << i << endl;
exit(0);
}
sleep(2);
}

while (true)
{
cout << "父进程正在运行: " << getpid() << endl;
sleep(1);
}
return 0;
}

如下右图中,第0个子进程退出进入将是状态,父进程收到17号信号,就开始回收子进程。此时还有4个子进程没有运行完毕,父进程会继续运行,等待其他子进程结束

image-20221214103626344

当所有子进程都等待结束了,waitpid返回-1让父进程退出回收子进程的循环,现在就只有父进程自己在干活了

1
while :; do ps jax | head -1 && ps jax | grep test | grep -v grep;sleep 1; echo "########################"; done

反应到监控信息上,父进程先是创建了5个子进程

image-20221214103938520

随后当第一个子进程退出的时候,父进程开始回收子进程。这里也能观测到,回收成功,并没有出现僵尸状态的子进程。

image-20221214104215559

8.2 父进程忽略子进程

前面提到过,SIG_IGN的作用是忽略这个子进程;你可能会想,这不对啊,忽略了子进程的信号,那岂不是父进程不想搭理子进程了吗?

1
*	|  SIGCHLD           |	ignore   	|

1.2表你还会发现,SIGCHLD的默认动作不就是忽略吗?自己设置一下是不是多此一举?

1
signal(SIGCHLD, SIG_IGN);

然并不是这样,对于SIGCHLD17号信号而言,我们给父进程设置了手动忽略后,子进程退出的时候将自动被回收

这么做的前提,是父进程不需要和子进程在回收资源的时候进行处理,说人话就是,只要父进程不想管任何子进程的工作,那就可以直接把17号信号手动设置成SIG_IGN

QQ图片20220413084241

结语

进程信号到这里就基本over了,干货满满!

如果对你有帮助,还请点个赞吧!!!

QQ图片20220416140203