Linux_进程信号

馋奶兔 提交于 2020-01-19 00:25:25

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库函数很多实现都以不可重入的方式使用全局数据结构
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!