1、信号基础
技术应用角度的信号
- 用户在Shell下输入命令启动一个前台进程
- 用户按下
ctrl+c
,此时键盘输入产生一个硬件中断,被操作系统获取并解释成信号,发送给目标前台进程 - 前台进程接收到信号后,进程执行退出
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(1)
{
cout<<"Process running, Waiting a signal!"<<endl;
sleep(1);
}
return 0;
}
ctrl+c
产生的信号只能发给前台进程。一个命令后面加&可以放到后台运行,这样Shell不必等待进程结束就可以接收新的命令,启动新进程- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接收
ctrl+c
这种控制键产生的信号 - 前台进程运行过程中用户随时可能按下
ctrl+c
而产生一个信号,说明该进程的用户空间代码执行到任何地方都有可能收到SIGINT
信号而终止,所以信号相对于进程的控制流程来说是异步的
信号的概念
信号是进程之间事件异步通知的一种方式,属于软中断
kill -l #查看系统定义的信号列表
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到
- 编号34以上的是实时信号(
man 7 signal
)
常见信号处理方式
- 忽略信号
- 执行该信号默认处理动作
- 提供一个信号处理函数,要求内核在处理信号时切换到用户态执行这个处理函数,称为捕捉一个信号
2、产生信号
(1)通过终端按键产生信号
SIGINT(ctrl+c
)的默认处理动作是终止进程。
SIGQUIT(ctrl+\
)的默认处理动作是终止进程并且核心转储(Core Dump)。当⼀个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做核心转储(Core Dump),帮助开发者进行调试,在程序崩溃时把内存数据dump到硬盘上,让gdb识别。
(2)调用系统函数向进程发信号
后台执行死循环程序,然后用kill命令发送以下信号
kill -SIGSEGV PID
#kill -11 PID
- kill命令是由kill函数实现的
- kill函数可以给一个指定的进程发送一个指定信号
- raise函数可以给当前进程发送一个指定信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);
//成功返回0,失败返回-1
- abort函数使当前进程接收到信号而异常终止
#include <stdlib.h>
void abort(void);
(3)由软件条件产生信号
SIGPIPE信号是一种由软件条件产生的信号。
SIGALRM信号默认处理动作是终止当前进程。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值为0或闹钟设定事件余下的秒数
调用alarm函数可以设定一个闹钟,告诉内核在多少秒后给当前进程发SIGALRM信号。
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
int count = 1;
alarm(1);
while(1)
{
cout<<"count = "<<count<<endl;
count++;
}
return 0;
}
(4)硬件异常产生信号
- 硬件异常被硬件以某种方式检测并通知内核,内核向当前进程发送适当的信号
- 在C/C++中除0、内存越界等异常,在系统层面上是被当作信号处理的
信号捕捉
#include<iostream>
#include<signal.h>
using namespace std;
void handler(int sig)
{
cout<<"catch a sig:"<<sig<<endl;
}
int main()
{
signal(2, handler);//信号捕捉函数
while(1);
return 0;
}
模拟野指针异常
//正常情况
#include<iostream>
using namespace std;
int main()
{
int *p = NULL;
*p = 100;
while(1);
return 0;
}
//捕捉异常
#include<iostream>
#include<signal.h>
using namespace std;
void handler(int sig)
{
cout<<"catch a sig:"<<sig<<endl;
}
int main()
{
signal(SIGSEGV, handler);
int *p = NULL;
*p = 100;
while(1);
return 0;
}
3、阻塞信号
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号
- 被阻塞信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作
信号在内核中的表示
- 每个信号都有两个标志位,分别表示阻塞和未决,还有一个函数指针表示处理动作
- 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才解除该标志
- 上图中SIGHUP信号未阻塞过也未产生过,当他递达时执行默认处理动作
- 上图中SIGINT信号产生过,但被阻塞,所以暂时不能递达,虽然处理动作是忽略,但是在没有解除阻塞之前不能忽略这个信号
- 上图中SIGQUIT信号未产生过,一旦产生将被阻塞,处理动作是进入用户空间执行用户自定义函数
- 常规信号在递达之前产生多次只记一次,实时信号在递达之前产生多次可以依次放在一个队列里
4、捕捉信号
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
- 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。(a)
- 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。(b)
- 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。(c)
- sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。(d)
- 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。(e)
信号捕捉函数signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
可重入函数
由于硬件中断而导致的一个函数被不同控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,称为重入。一个函数会因重入而导致错乱,称此函数为不可重入函数,反之称为可重入函数。如果一个函数符合以下条件之一是不可重入的:
- 调用了malloc或free,因为malloc采用全局链表来管理堆
- 调用了标准I/O库函数,因为标准I/O库函数很多实现都以不可重入的方式使用全局数据结构
来源:CSDN
作者:_NoBug_
链接:https://blog.csdn.net/qq_41245381/article/details/104030598