本篇博客让我们一起来康康信号部分的内容
系统为CentOS7.6,完整代码见 Gitee
文章目录
- 1.什么是信号
- 1.1 何为异步?
- 1.2 信号的种类
- 1.3 信号产生
- 1.4 信号动作
- 2.系统接口
- 2.1 signal
- 2.1.1 前台进程和后台进程
- 2.1.2 循环捕捉所有信号
- 2.1.3 信号9/19
- 2.2 kill
- 2.2.1 killall
- 2.3 raise
- 2.4 abort
- 2.5 alarm
- 2.6 sigset_t信号集
- 2.7 sigprocmask
- 2.8 sigpending
- 2.8.1 屏蔽2号信号
- 2.8.2 屏蔽所有信号
- 2.8.3 解除屏蔽
- 2.9 sigaction
- 2.9.1 基本使用
- 2.9.2 sa_mask
- 3.软件崩溃的本质
- 3.1 情景演示
- 3.2 说明
- 4.coredump
- 4.1 开启该功能
- 4.2 使用coredump
- 4.3 为什么默认关闭?
- 5.进程处理信号
- 5.1 内核态/用户态
- 5.2 信号检测
- 6.可重入函数
- 7.volatile
- 7.1 示例
- 8.子进程发送信号
- 结语
1.什么是信号
在进程运行过程中,会出现各种各样的情况。操作系统需要用一套机制,来管理进程的事件
- 进程退出
- 进程停止
- 进程恢复运行
- ……
同时,这套管理机制是异步
的,属于一种软(件)中断
和硬件中断打断处理器类似,软件中断打断进程的执,让其执行对应代码进行响应
1.1 何为异步?
以网购物品为例:当商品寄到自提点的时候,会给你发送一条取件的短信(信号)。此时我正在打游戏,没时间去处理这个快递(即取快递的行为并不是必须立马执行)
但这个时候,我已经知道有一个快递到了(知道自己获取到了一个信号)本质上就是知道了一会要去取快递(一会要处理信号)
当游戏一把打完了,我们就去取快递了(处理信号)
这就是一种异步
的过程。因为你不知道你的快递什么时候会到站点,进程也不知道自己什么时候会收到一个信号
1.2 信号的种类
使用kill -l
命令,我们可以看到目前linux系统下64
种不同的类型。
其中前32为标准(Standard)信号,后32为实时(Real-time)信号;本篇博客只关注标准信号
这些信号,都是linux系统中预定义的宏
其中最常用的便是9号信号,来中断进程。平时我们最常用的CTRL+C
,也是通过向进程发2号信号让进程退出的
[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
/*
* 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类型来存放一个进程收到的各种信号。
在系统内核中,分别有三个表,用来存放进程的信号。而这些信号在位图中的位置,就是在handler
方法集中处理动作的下标
block - 1表示该进程屏蔽这个信号
pending - 表示进程收到了什么信号,1代表收到且未处理
handler - 每一个信号所对应的处理方法,默认/忽略/自定义
这一切都是处于进程PCB中的,只有操作系统能为我们管理。所以操作系统提供了相关的接口,方便我们对进程信号进行自定义设置。
pending表中的信号只能保存一个,如果一个信号尚未处理,该位图为1;另外一个相同信号到来的时候,会被直接丢弃掉。(pending表只能记住一个信号)
handler表中的两个宏如下:
- SIG_DFL 默认方法
- SIG_IGN 忽略
- 忽略是信号处理的一种方式,我们能正常收到这个信号,处理方法是不管他
2.系统接口
2.1 signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个函数可以用于设置某个信号的处理方法。如果设置成功,则返回这个信号的旧处理动作
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
终止这个进程
#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;
}
2.1.1 前台进程和后台进程
这里对ctrl+c
的作用进一步描述,它只能用来中断一个前台进程
./test #我们直接运行一个进程,就是前台进程
之前这种直接运行进程,在bash上打印内容的方式,都是一个前台进程,可以用ctrl+c
终止;我们可以在后面加上&
设置为一个后台进程
&
只是临时在后台运行,bash关闭后会终止;如果想持久在后台运行,需要在命令最前面加上nohup
[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
无法终止这个进程,因为它并没有在前台运行!
[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+
[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号信号的方法)
[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
循环,捕捉所有的信号
//.....
//其余代码同上
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
2.1.3 信号9/19
在LINUX
下,9号信号是一个管理员信号,具有杀死进程的最高权限,不能被自定义捕捉!
你想啊,要是linux不对9号进行限制,那我把所有信号都捕捉了,岂不是这个进行没有办法被外部中止了?小病毒啊!😂
和9号信号一样不能被屏蔽的,还有19号信号SIGSTOP
[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]$
2.2 kill
kill不仅是一个系统命令,同时还有一个系统接口;
一般这种情况,用
man kill
查看命令的文档,man 2 kill
查看接口函数
之前我以为它只是一个用来干掉进程的命令(毕竟kill就是这个意思)现在才知道原来它的作用是给进程发信号
//kill - send signal to a process
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
它的返回值很简单,如果成功发送信号,则返回0,否则返回-1并且更新errno
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately
所以我们可以写一个简单的函数实现,来制作一个自己的kill命令
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;
}
成功发送了信号!
如果我们使用错误的时候,则会发送提示信息👍
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./mkill
Usage: ./mkill signo-id process-id
2.2.1 killall
这个接口可以通过进程名向所有这个名字的进程发信号
[muxue@bt-7274:~/git]$ killall tsig
通过测试可以发现,它发送的是第15号信号
[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
这个系统接口的作用是给自己发信号
#include <signal.h>
int raise(int sig);
返回0代表调用成功,非0代表失败
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
用下面的代码进行测试,进程会不断的给自己发送2号信号
#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号信号
[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
信号
#include <stdlib.h>
void abort(void);
还是2.3
中的代码,将raise(2)
修改为abort()
,同时捕捉6号信号。
此时能观察到我们自己写的handler方法的确被调用了,但是进程依旧终止了
[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
信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
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.
用下面的代码进行测试
#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号信号。此时进程并没有退出,而是继续休眠
[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号信号自定义捕捉,则会直接退出进程
[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位图的存储结构,被称作信号集/信号屏蔽字
虽然我们能直接使用这个类型, 但是对这个信号集中的位图操作必须要调用系统接口来完成
#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
更改或则获取当前进程的信号屏蔽字
#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
中
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信号集
#include <signal.h>
int sigpending(sigset_t *set);
参数为一个输出型参数。正确获取返回0,否则-1
这时候我们就可以写一个简单的函数来打印当前进程的信号集了
//打印信号集的内容
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;
}
运行之后可以看到,程序一直在打印当前进程的信号集
[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
来屏蔽某一个信号,再来观察情况
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
不会有变化
[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 屏蔽所有信号
可以用一个循环设置所有的信号位,让当前进程屏蔽掉所有信号
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号信号依旧是老大哥,不受影响,依旧能干掉这个进程
2.8.3 解除屏蔽
如果在设置屏蔽之后,休眠15s(在此期间接收信号)再接触对信号的屏蔽
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
2.9 sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
这个函数的参数和signal
函数很相似,不过都变为了一个结构体。这个接口也可以用来处理实时信号
(不在文本考虑范围内)
- 第一个参数是需要处理信号的编号
- 第二个参数是自定义的action
- 第三个参数是输出型参数,可以获取到旧的处理方法
设置成功后返回0,出错返回-1
这个结构体的成员如下
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 基本使用
#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
方法!
[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.9.2 sa_mask
这个成员是一个sigset_t
类型,用于当处理一个信号的时候,连带屏蔽其他信号;
- 当一个进程正在处理A信号的时候,操作系统会把A信号自动添加入Block表,屏蔽该信号(不允许同时处理两个A信号,避免信号A的递归式处理)
如果你想在处理2号信号的时候,阻塞掉3、4、5号信号,就可以对sa_mask
进行设置,设置方法参考2.8
的操作
因为现在我演示的自定义方法只是一个再简单不过的示例,实际上进程收到信号的时候需要根据不同情况进行不同的自定义处理,这些自定义处理的过程可能会很长。此时就可以block掉其他的信号,不让它们影响当前进程运行的自定义方法
3.软件崩溃的本质
之前我们经常会遇到软件出错奔溃的情况,那么奔溃的本质是什么呢?
3.1 情景演示
用下面的一个除零错误作为演示
#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掉这个进程
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++中,并不会将除零错误当作一个异常进行处理!
我们自定义捕捉了8号信号,没能让进程终止。但此时这个进程已经出现了一个严重的bug,操作系统就会一直给进程发这个信号
[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号信号
int main(int argc, char *argv[])
{
TestSignal();
int *p;
*p=20;
return 0;
}
11) SIGSEGV
代表段错误,写OJ题目的时候这个报错很常见😂
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
标记位
在1.2贴出来的源码注释中可以看到,有不少信号的默认动作是进行core dump
,比如8号信号。那么这个东西到底是什么玩意呢?
通过fork创建子进程,让子进程除零产生8号信号,子进程退出
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
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
exitcode:0 signo:8 coredump: 0
4.1 开启该功能
默认情况下,我们云服务器的core dump功能是被关闭的,需要我们手动开启;
[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
的大小
[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
[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告诉我们这个不是一个普通的文本文件
这个现象告诉我们,默认动作是coredump的信号,会让进程退出,将coredump标记位置为1并且产生一个core.
文件
4.2 使用coredump
这个功能会将进程在运行中产生异常的上下文数据,执行core dump(核心转储)为一个文件,方便我们debug
如下所示,使用-g
命令以debug模式编译test.cc
,运行的时候可以看到除零错误之后跟了一个(core dumped)
提示我们进行了core dump操作,对应产生了一个core.
文件
[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 文件名
加载文件,就可以直接定位到出错代码的位置!
[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找错误方便多了
4.3 为什么默认关闭?
你可能会觉得,这个功能不挺好的吗,为啥默认没有开启呢?
先来看看这个文件的大小,足足有580KB
[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.
文件,塞满我们的硬盘。
对于求稳为主的服务器而言,这可不是一个好事。所以云服务器上默认禁止了这个功能。
[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的内核空间;该内核空间用于内核级页表的映射,即映射操作系统的物理内存!
有内核级页表的存在,无论进程怎么切换,都能找到操作系统内核的代码和数据,前提是有权限访问。
- CPU中的CR3状态寄存器会标识当前进程处于内核态还是用户态
- 内核态可以访问所有代码和数据,权限最高
- 用户态只能访问当前进程自己的数据
当我们进程需要执行内核接口的时候,就需要将进程切换为内核态;运行完毕之后,切换回用户态。
当我们进程出现了异常,会从用户态切换成内核态,由操作系统检测相关异常并向进程发送对应信号。
当我们进程的时间片到了(需要切换进程)也会从用户态转为内核态,由操作系统来进行进程切换。
5.2 信号检测
当进程从内核态切换回用户态的时候,会进行信号的检测和处理。此时判断pending表中是否有未处理信号,以及该信号是否有被block。如果一个信号没有被block,则将该信号递达给进程,执行对应的处理方法
- 执行用户的自定义方法时,应该以什么身份执行?
注意,当我们给一个信号指定了自定义处理方法,就代表该信号的处理方法是用户提供的。此时需要以用户的身份去执行这个代码,才能正确访问用户级页表。
这么做也能避免恶意代码的注入。如果有人在自定义方法中写一个修改系统内核的恶意代码,也能被操作系统发现并阻止。
这个过程可以用下面这张图来解释(并非完整过程,仅供参考理解)
每次处理完信号后,会返回用户进程,从上一次中断的位置开始继续往后运行
6.可重入函数
//头插
void insert(Node* p)
{
p->next=head;
head=p;
}
上面这个函数是一个非常简单的链表头插函数
如果我们这个头插函数处理的是一个全局的链表,就可能会因为用户态、内核态的切换,函数重入造成错误
所以insert
就是一个不可重入函数!除了这个头插,还有一些其他的函数也符合这个特效:
- 调用了malloc或者free(可能会多次malloc和多次free)
- 调用了I/O库的函数
依此类推,如果一个函数只访问他自己的局部变量,不会影响其他参数。那么他就是一个可重入函数
7.volatile
之前的学习中就已经知道,这个关键字的作用是每一次访问变量的时候,都必须要去内存中取。
假设我们进程中需要通过一个全局变量进行条件判断
int flag=0;
int main()
{
if(flags)
{
//..
}
else
{
//..
}
return 0;
}
如果我们自定义捕捉了一个信号,收到该信号的时候,会修改flag,执行if/else语句中对应的代码。
由于编译器的优化问题,每一次访问flag的时候,它可能不会每次都去内存中取,就会出现一个问题
- 寄存器中 flag=0
- 经过自定义捕捉函数处理,内存中 flag=1
这两个flag在if条件中会导向不同的结果!
为了避免这种可能因为平台、编译器、优化问题导致的代码bug,我们需要告诉所有编译器,不准对flag变量做任何优化处理,必须要老老实实的去内存中拿这个变量的数据!
//volatile保持内存的可见性
volatile int flag = 0;
7.1 示例
gcc编译器可以通过
-O2
指定较高的优化等级
以下面的代码为例
#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
为假,应终止循环,退出进程才对!
[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
关键字,则不会出现这个问题,进程能够正常退出
[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
关键字,会发现进程也能正常退出
[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号信号
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号信号,此时子进程因为错误退出
[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]$
除了退出时会发送信号,子进程暂停、继续运行的时候,都会向父进程发送信号
结语
进程信号到这里就基本over了,干货满满!
如果对你有帮助,还请点个赞吧!!!