当前位置: 首页 > news >正文

【Linux】进程信号

信号是什么

信号是信息的载体,在Unix和Linux环境下,是一种古老、经典的通讯方式,对于现代Unix和Linux来说,依然是非常重要的IPC(进程间通信)方式。
(1)信号:其实是就是一个软件中断,通知正在运行的进程发生了某个事件,进程收到通知后立刻停止当前所干的事情,然后后去处理这个事件。这种软件中断与硬件中断类似,都是提供了一种处理异步事件的方式,但是信号的软件中断却是在软件层面上所实现的中断。
(2)一种信号也就对应一种事件有多种信号也就能代表多种事件。
(3)每一个进程所收到的所有信号,都是有内核负责发送,并且有内核进行处理。

(1)信号的种类:通过命令 kill -l 查看信号列表在这里插入图片描述
通过从上面查看信号列表,儿们可以看出信号的种类有62种。并且其中:1到31号为非可靠信号、34到64号为可靠信号
可靠信号:本可能造成事件丢失,也叫实时信号
非可靠信号:可能造成事件丢失,也叫常规信号、普通信号

(2)信号的状态

1、递达状态:信号递送并且送达到目标进程
2、 未决状态:处于信号产生与信号递达之间的状态。主要由于阻塞/屏蔽而产生的状态
3、信号处理方式:执行默认动作、忽略、阻塞
4、阻塞信号集(信号屏蔽字):该集合用于设置信号屏蔽,当将这个信号加入到这个集合中,就可以阻塞/屏蔽该信号。后续收到的该信号,该信号的处理将被延后,知道该信号的屏蔽被解除为止。
5、未决信号集:信号从发出到递达的中间过程,叫做未决状态。内核中有个数组专门用来记录信号的未决状态,叫做未决信号集,当信号处于未决状态时,对应的位置被置为1,否则置0。
解决未决状态的方式有两种:
a.此信号解除阻塞
b.此信号被忽略

信号的产生

(1)硬件产生
ctrl+c(2号信号) 、ctrl+|(3号信号) 、 ctrl+z(20号信号)
以上三种都是 产生了一个硬件信号有操作系统转换为软件信号
(2)软件产生

kill + signum pid ---想一个进程发送一个信号,默认发送15号信号
#incldue<sys/types.h>---系统接口
#include<signal.h>
int kill(pid_t pid,int sig)--发送任意信号给任意进程
参数1:pid---进程ID
参数2:sig---信号编号
	  pid > 0:发送信号给进程号为pid的进程
	  pid = 0:发送信号给与调用kill函数进程属于同一个进程组内面的所有进程
返回值:成功返回0,失败-1,并且设置errno

#include<signal.h>---库函数
int raise(int sig)--发送一个信号给调用进程,就是自己给自己发信号
参数1:pid---进程ID
返回值:成功返回0,失败非0

#include<stdlib.h>
void abort(void)---给进程自己发送SIGABRT,引发进程非正常终止,出错时才用并且设置core文件

#include<unistd.h>---系统接口
unsigned int alarm(unsigned int seconds)---等到seconds后,给进程发送一个SIGALRM信号,终止当前进程
参数seconds:指定时间()
返回值:返回0或者闹钟剩余秒数,无失败情况

注意: 上面的abort函数,调用后会设置core文件,其实就是core dumped(核心转储)为的就是:进程非正常退出后,方便程序调试的。但是这个功能是默认关闭的。这些信息都属于限制信息。
在这里插入图片描述
默认关闭状态下core文件大小为0,如果想要使用这core文件,那么只需要改变文件大小使其不为0,则打开; 查看进程中限制信息:ulimit -a 设置命令:ulimit -c + 文件大小修改core文件大小

信号在进程中注册(修改信号pending位图)

我们都知道进程在内核中进行调度是通过PCB的,在PCB中有两个结构体一个pending结构体存储当前收到的信号,还有一个结构体blocked用于存储现在都有哪些信号要被阻塞。
(1)pending位图(未决信号集合)在PCB中的结构:

进程的task_struct结构中有关于本进程中未决信号的数据成员: struct sigpending pending:
struct sigpending{
     struct sigqueue *head, *tail;//内核当中的双向链表
     sigset_t signal;//结构体,只有一个成员是一个数组,用来做位图标记信号
};
存储信号的双向链表结构体:
struct sigqueue{
	struct list_head list;
	int flags;
	siginfo_t info;//存储信所携带的信息
	struct user_struct *user;
};
  • 信号的注册其实就是在pcb中进行记录
  • 信号集合:sigset_t 结构体(保存信号):进程记录一个信号时是通过这个结构体的位图来记录的(1号信号在位图第0位置存储,位图的该位置原本是0,如果有信号,该位置置1),这个位图的位数+1代表的就是指定 的信号存储位置

注册流程: 产生的信号发送给了进程,操作系统进程中的pending位图中信号对应的标记为,,将0置为1,表示该信号的已经存在,并且向pending结构体中sigqueue链表中添加一个注册信号的信号节点。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但是还没有来得及处理,或者该信号被进程阻塞。

(2)非可靠信号和可靠信号在进程注册有什么不同?
非可靠信号: 若当前未决信号集中指定的信号已经注册,则什么都不干,不去修改文位图也不向sigqueue链表中添加信号节点。
可靠信号: 不管当前信号是否已经注册,都回去修改对应位图,向sigqueue链表中添加一个新的信号节点。
注意:
1、位图只是标记有没有这个信号,而sigqueue链表才真正的标记了这个信号有多少个。
2、这也就同时印证了上面非可靠信号容易造成事件丢失的性质了,假如同时来了三个相同的非可靠信号,只有第一个注册并且添加到sigqueue链表当中,后面两个相当于没有注册,这不就丢失了吗!
3、总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)

信号在进程中注销(修改信号pending位图)

(1)注销: 其实就是在此修改PCB中的未决信号集合pending,删除其位图上对用的信号标记,并且删除sigqueue中的节点。

**注销流程:**去PCB中的pending(未决信号集合)中位图去查找相应的信号标志位,将标志位由1置为0,并且同时删除sigqueue中的对应注销信号的信号节点

(2)非可靠信号和可靠信号在进程注销有什么不同?
非可靠信号: 删除当前信号的sigqueue节点,并且修改位图置为0。
可靠信号: 删除节点后,判断是否还有相同的信号节点,若没有,则将位图置0。

信号在进程中处理(信号递达)

前面说过信号的处理方式有三种:执行默认动作、忽略、阻塞
(1)默认动作: 就是操作系统原本既定义好号的处理方式
(2)忽略: 收到信号后什么也不干,会注册,注销,但是处理方式就是什么都不干
(3)自定义: 修改原有操作系统定义好的处理动作,只执行自定义的动作
(4)信号
当然那么信号的处理方式有三种那也就意味着,信号的处理方式是可以修改的。下面有两个信号安装函数来改变信号处理方式:

#include<signal.h>--存在内核版本差异性
sighandler_t signal(int signum,sighandler_t handler);
signum:信号id
handler:信号的操作处理方式,是一个回调函数
1、SIG_IGN将信号处理方式改为忽略
2、SIG_DFL将信号的处理方式改为默认方式
3、回调函数typedef void(*sighandler_t)(int)  没有返回值但是有一个int参数的函数
最后把signum作为参数传入回调函数作为参数----作为信号捕捉使用
#include<signal.h>   sigaction>---实现---->signal
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)
signum:信号值
struct sigaction *act:signum当前要要修改的新的动作
struct sigaction *oldact:用于获取signum信号原有的动作---便于在还原回去要定义两个结构体作为参数;
struct sigaction newact
{
newact.sa_handler=//定义回调函数;
newact.sa_flags=0//决定使用哪个回调函数;0位默认使用sa_handler回调函数
sigemptyset(&newact.sa_mask);//清空临时要阻塞的信号集合
};
struct sigaction oldact;//接受原有动作

(5)signal()和sigaction()栗子运用:

#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>

struct sigaction oldact;//接受原有动作

void sidcd(int signum)
{
	printf("recv a signal:%d\n",signum);
	sigaction(signum,&oldact,NULL);//将信号处理方式有自定义改成默认
}

int main()
{
	//都是自定义信号处理方式
	//signal(2,sidcd);
	//signal(3,sidcd);
	struct sigaction newact;
	newact.sa_handler = sidcd;//设置自定义回调函数
	newact.sa_flags = 0;//=0时默认使用sa_handler回调函数
    sigemptyset(&newact.sa_mask);//清空临时要阻塞的信号集合
	sigaction(2,&newact,&oldact);
	while(1)
	{
		printf("=====================\n");
		sleep(3);
	}
	return 0;
}

(6)信号处理流程

处理流程: 在PCB有一个pending(未决信号集合),这是有一个2号信号到来,在pending中进行注册后。开始处理信号时,那么在PCB还有一个处理动作数组handler,其中存储的是每一个信号自己的处理动作,而pending中信号的值就是handler数组中的下标。例如:一个2号信号在PCB中已经注册了,那么处理是就去handler数组中2号下标去找该信号的对应处理动作。
在这里插入图片描述

自定义处理方式的捕捉流程

注意:一个信号虽然在PCB中进行了注册,但是并不是立即就会处理而是在合适的时机,合适的时机就是从内核态返回到用户态的时候。

信号有三种处理方式,其中忽略、默认都是操作系统内核中所定义好的在内核态中运行。而自定义处理方式由用户自己定义,那就意味着自己写的就运行在用户态当中,那么当我们自定义信号处理方式时,需要先调用自定义函数处理,这个过程叫**信号捕捉*
在这里插入图片描述

信号在进程中阻塞(屏蔽)

(1)阻塞: 阻止一个信号被递达(信号依然可以注册,只是暂时不被处理)
(2)如何阻塞: 只需要在PCB中将这个信号在阻塞集合blocked中标记起来就行了
(3)阻塞流程

阻塞流程: 一个信号到来在pending位图中进行注册,在信号由内核态返回用户态时,就要准备处理当前进程递达的信号,但是在处理递达信号之前还有一步操作那就是去blocked集合中查找看该信号有没有被标记,如果有那么则该信号阻塞不处理,返回主控流程,没有则信号正常处理去handler动作数组找相应动作进行处理,完毕后返回主控流程。
在这里插入图片描述

信号的阻塞---阻止一个信号被递达(信号依然可以注册,只是暂时不被处理)
阻塞一个信号只需要在PCB中将这个信号在阻塞信号集合中标记起来
#include<signal.h>
int sigprocmask(int how,const, sigset_t *set,sigset_t *oldset)---设置进程的一个信号掩码
参数1:how:对blocked位图要进行什么样的操作
SIG_BLOCK:添加一个阻塞信息  blocked | set
SIG_UNBLOCK:解除信号阻塞  blocked & (~set)
SIG_SETMASK:设置阻塞信号集合  blocked = set
参数2:sigset_t *set:当前阻塞信息集合
参数3:sigset_t *oldset:获取原有的状态,便于还原回去
向集合中添加信号:
int sigfillset(sigset_t *set)---将所有信号添加到集合当中
int sigaddset(sigset_t *set,int signum)---将指定信号添加到集合当中
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>


void sigcb(int signum)
{
	printf("recv signal:%d\n",signum);
}

int main()
{
	signal(SIGINT,sigcb);
	signal(40,sigcb);
	sigset_t mask_set,old_set;//设置信号集合
	sigemptyset(&mask_set);//清空信号集合
	sigfillset(&mask_set);//添加所有信号到集合中
	sigprocmask(SIG_BLOCK,&mask_set,&old_set);//阻塞mask_set中所有信号
	printf("press enter to continue!\n");
	getchar();//不按回车,则流程就卡在这里
	sigprocmask(SIG_UNBLOCK,&mask_set,NULL);//解除阻塞
	//sigprocmask(SIG_SETMASK,&old_set,NULL);//解除阻塞
	return 0;
}

输入:
在这里插入图片描述
在这里插入图片描述
结果:
在这里插入图片描述
结论:在所有信号都被阻塞时,所有的信号都可以注册但是都不能被处理,此时我们自定义了2号和40号信号的处理方式。我们用硬件产生2号信号,用软件产生40信号,刚好这两个被触发了5次。按下解除信号阻塞,信号开始处理,此时输出一次2号信号,五次40信号,明明都是输入5次,为什么?2号是非可靠信号,40号是可靠信号。因为这也正印证的可靠信号和非可靠信号在进程中注册的方式了!

尽管如此还是有一些特殊信号:就是9-SIGKILL、19-SIGSTOP信号,这两个无法被阻塞、无法被自定义、无法被忽略。

竞态条件

(1)竞态条件: 在多个执行流中,对同一个代码竞争执行。

(2)函数的可重入和不可重入
1、函数的重入:在多个执行流当中,重复进入同一个函数执行代码
2、不可重入:如果一个函数重入之后有可能造成数据二义或者程序的逻辑紊乱,这个函数则不可重入
3、可重入:函数重入之后,不会出现任何影响
(3)重入与不可重入判断依据
1、这个函数当中是否对全局数据进行了非原子安全操作
(4)重入与不可重入函数用处
1、当自己设计函数/使用别人函数的时候,根据使用场景就要考虑函数的重入状况
注意:malloc和free是不可重入函数,并且malloc什么的地址空间并没有立刻分配空间,使用的时候在分配

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int a = 0;
int b = 0;
int test()
{
    a++;
    sleep(10);
    b++;
    return a+b;
}
void sigcb(int signo)
{
    printf("signal:%d\n", test());
}
int main()
{
    signal(SIGINT, sigcb);
    printf("main:%d\n", test());
    return 0;
}

在这里插入图片描述
volatile关键字: 用于修饰一个变量保持变量的内存可见性,防止编译器过度优化(每次都从内存中重新获取变量的数据)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>


volatile int a = 1;
void sigcb(int signo)
{
    a = 0;
}

int main()
{
	signal(SIGINT,sigcb);
	while(a)
	{
	}
	return 0;
}

情况一:此时全局变量没有关键字volatile,且普通编译。这时的代码编译运行,ctrl+c,
产生SIGINT信号,a = 0,主函数循环退出。

情况二:但是经过编译优化后,gcc -O1/2  ;一二级优化,ctrl+c此时主函数不退出,
因为编译器发现在主函数中main中a变量不断在使用,使用直接将a放入寄存器,
为了提高效率,往后每次直接从寄存器中获取。此时虽然内存中a=0了,
但是寄存器中的还是a=1,所以主函数还在无限循环不能退出。

情况三:此时在全局变量a全面加上volatile关键字,在进过优化编译时。
a虽然被不断使用,但是由于关键字volatile所以编译器没有吧a放入寄存器,
所以ctrl+c产生SIGINT信号将a=0,此时主函数正常退出
编译过程中优化: gcc -O1/2  ;一二级优化

SIGCHLD信号

SIGCHLD信号:
子进程退出时操作系统个父进程发生的信号,因为SIGCHLD信号默认处理方式就是什么都不敢,一次父进程相当于没关注子进程的退出状态,因此导致子进程成为僵尸进程,否则就要进行进场等待(一直阻塞等待)

相关文章:

  • 【Linux】网络编程套接字(1)
  • 【Linux】UDP网络套接字编程
  • 【数据结构:树】——搜索二叉树-K模型(非递归和递归)
  • 【C++】——STL关联式容器认识以及使用
  • TCP三次握手和四次挥手详解
  • 【Linux】进程控制
  • 【Linux】进程程序替换——exec函数簇
  • 【Linux】入门基础命令(2)
  • 【Linux】权限管理和粘滞位理解
  • linux下inode节点理解
  • C语言函数
  • C语言数组
  • C语言表达式
  • C语言初识指针
  • C语言结构体
  • (十五)java多线程之并发集合ArrayBlockingQueue
  • [译]Python中的类属性与实例属性的区别
  • 【附node操作实例】redis简明入门系列—字符串类型
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • CentOS从零开始部署Nodejs项目
  • java小心机(3)| 浅析finalize()
  • Linux下的乱码问题
  • Map集合、散列表、红黑树介绍
  • mysql 5.6 原生Online DDL解析
  • Terraform入门 - 1. 安装Terraform
  • zookeeper系列(七)实战分布式命名服务
  • 闭包,sync使用细节
  • 测试开发系类之接口自动化测试
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用(一)
  • 使用docker-compose进行多节点部署
  • 推荐一个React的管理后台框架
  • 写代码的正确姿势
  • 原生 js 实现移动端 Touch 滑动反弹
  • 找一份好的前端工作,起点很重要
  • 如何用纯 CSS 创作一个货车 loader
  • # 飞书APP集成平台-数字化落地
  • #!/usr/bin/python与#!/usr/bin/env python的区别
  • #{}和${}的区别是什么 -- java面试
  • #if 1...#endif
  • #ubuntu# #git# repository git config --global --add safe.directory
  • (AngularJS)Angular 控制器之间通信初探
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (poj1.2.1)1970(筛选法模拟)
  • (pojstep1.3.1)1017(构造法模拟)
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (二)构建dubbo分布式平台-平台功能导图
  • (翻译)terry crowley: 写给程序员
  • (附源码)ssm高校社团管理系统 毕业设计 234162
  • (排序详解之 堆排序)
  • (五)IO流之ByteArrayInput/OutputStream
  • (转)http协议
  • (转)我也是一只IT小小鸟
  • .[backups@airmail.cc].faust勒索病毒的最新威胁:如何恢复您的数据?
  • .chm格式文件如何阅读
  • .desktop 桌面快捷_Linux桌面环境那么多,这几款优秀的任你选