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

Linux 信号终篇(总结)

前文:本文是对信号从产生到被处理的过程中的概念和原理的总结,如果想了解具体实现,请查看前两篇博客:Linux 信号-CSDN博客、Linux 信号(下篇)-CSDN博客

一、信号的产生

1.1 信号产生的五种条件

①键盘组合键 :“ctrl + c”或者“ctrl + z”等;

②linux命令: kill - signo processpid , 例:杀掉进程号12345的进程  :kill -9 12345 ;

③系统调用:在自己的代码中调用系统调用接口,给某个进程发信号或者给自己发信号常用的几个接口:

#include <sys/types.h>
#include <signal.h>
int kill( pid_t pid, int sig);
//1. pid : 进程pid,想要发送到哪个进程?
//2. sig :要发送的信号码,想要发送什么信号?
//3. 返回值int,失败返回-1
#include <signal.h>
int raise(int sig);
//1. 给自己发信号,相当于 kill(getpid(),sig);
//2. sig: 想要给自己发送的信号
#include <stdlib.h>
void abort(void);
//1.终止自己
//2.给自己发送6号信号 :SIGABRT

④、异常

异常的过程是:当你的进程被调度的时候,cpu执行你的代码,当cpu执行到某行代码时(例如:除0错误或者对空指针访问等)->

cpu知道自己的运算异常,在cpu的状态寄存器中修改溢出标志位->

同时cpu给OS发送硬件异常错误 ->

OS收到cpu硬件异常的报错后去找到对应的进程 ->

并给这个进程发送对应的异常信号,提示这个进程需要注意 ->

如果这个进程不结束,异常一直没有解决,cpu会一直调度这个进程,cpu会一直给OS发送硬件异常错误,OS会一直给这个进程发信号,进程处于死循环;

⑤软件条件

例如:闹钟接口,每隔一段时间给进程发送闹钟信号;

#include <unistd.h>
unsigned int alarm( unsigned int second);
//1. second:每隔多少秒给进程发信号?
//2. 返回值unsigned int 是上一个闹钟剩余的时间
1.2 理解core dump

打开系统的core dump功能,一旦进程出现异常,OS会将进程在内存中的运行信息,给dump(转储)到进程的当前目录(磁盘)上,形成core.pid文件,(核心转储:core dump);

未来在调试的时候,在core-file 直接定位到代码出错行,这是事后调试的方法(运行出错后再调试);

1.3 深度理解信号的发送和产生

①给进程发信号其实是给进程的PCB发的信号;

②所谓的“发信号”,其本质是OS修改PCB内的一个用来管理信号的位图数据结构,这个数据结构其实是一个int的整形,因为一个int 32bit位刚好可以用来表示1~31号信号,所以所谓的“发信号”其实就是对内核数据的写入操作,把对应位置的0写成1,代表信号产生了;

③信号的产生处理整个过程都是OS在操作,因为OS是所有硬件的管理者,只有OS才能直接访问内核数据结构,普通用户只能调用系统接口,因为操作系统不相信任何用户;

二、信号的保存

2.1、信号为什么要保存?

因为进程收到信号之后,可能不会立即处理,会有一个时间窗口,在这个过程中需要先把信号保存起来;

2.2、信号如何保存?进程如何管理信号?

通过三张表:

①block表,位图数据结构,与每个信号映射对应关系,这张表用来记录某个信号是否被屏蔽,如果被屏蔽把对应信号位置的0改1,当取消屏蔽把对应的1再改为0;

②pending表,位图数据结构,与每个信号映射对应关系,这张表用来记录某个信号是否已经产生,是否已经收到某个信号,注意:如果信号已产生但并未处理即(信号未决),把对应信号位置的0改为1,当这个信号被处理后再从对应的1改为0;

handler表,指针数组结构,这是一个用来保存对信号的处理的方法对应的指针,当未来处理这个信号的时候,直接调用指针指向的方法;注意:这里的处理方法包括三种:SIG_DFL(默认)、SIG_IGN(忽略)、自定义方法;

2.3、操作这三张表相关的接口

因为这三张表属于内核数据结构,所以用户不能对其直接进行访问或修改操作,只能通过系统调用接口;

数据准备工作:信号集函数:

#include <signal.h>
int sigemptyset( sigset_t * set);
//1. 对信号集清空处理操作
//2. set:sigset_t类型结构,是系统封装的位图结构体
#include <signal.h>
int sigaddset( sigset_t * set, int signum);
//1. 添加信号编号到信号集中
//2. set:系统封装的位图结构体
//3. signum想要添加到信号集中的信号编号(未来对这个信号进行操作)
//4. 返回值int,如果成功返回0否则返回-1
#include <signal.h>
int sigismember(const sigset_t *set ,int signo);
//1. 检测信号signo是否存在内核中的pending表中,存在代表信号产生,信号未决;
//2. set: 系统封装的位图结构体
//3. signo: 需要检查的信号
//4. 返回值int,如果不存在返回0,如果存在返回1,如果失败返回-1
#include<signal.h>
int sigpending(sigset_t * set);
//1. 获取内核中pending表的接口
//2. 这是输出型参数,传入set带出内核中的pending表
//3. set: 系统封装的位图结构体
//4. 返回值int,如果获取失败返回值<0

系统调用接口:

#include <signal.h>
int sigprocmask( int how, const sigset_t*set, sigset_t* oset);
//1.how: 当how是SIG_SETMASK时,将set自定义设置好的数据一个一个设置进进程PCB的管理信号的block表里
//2. set: 自定义设置好的位图结构体,oset: 通过oset保存内核更改前的数据以便日后恢复
//3. 当第二个参数传入oset,第三个参数传入nullptr时,代表的是取消屏蔽信号

三、信号的捕捉

3.1两个信号捕捉接口

①:signal

#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//1. signum: 需要捕捉的信号
//2. handler: 捕捉信号后调用的自定义方法
//3. sighandler_t : 定义的函数指针类型,参数为int,返回值为void

②:sigaction

#include <signal.h>
int sigaction(int signum, struct action * act,struct action* oact);
//1. signum: 需要捕捉的信号
//2. act: 自己创建并设置好的action对象,输入型参数,OS通过act写入到内核数据中
//3. oact: 自己创建初始化好的action对象,输出型参数,通过oact把内核中修改前的数据带出来,以便日后恢复
//4. action: 结构体,成员中包括handler处理方法、sigset_t类型的sa_mask,传入前通过添加sa_mask字段达到信号在处理时屏蔽其它信号的效果;
//5.注意:这个接口需要再传参之前把act、oact创建并初始化好,同时act需要把结构体成员sa_handler(自定义方法)、sa_mask(需要屏蔽的信号)字段设置好(如果你不想屏蔽其它信号可以不设置);
3.2可重入函数理解

例如:当程序运行时向链表头插一个Node1节点,调用insert方法,在方法运行到创建了一个节点,链接到头部,并且正准备让head指向Node1的时候,突然接收到一个信号!这时主函数main会停下来,先执行处理信号的方法,正巧的是处理信号的方法也是insetr即(向链表头插一个节点),那么此时会先运行完处理信号的方法:把Node2头插到链表并把head指向了Node2,处理完后回到主函数main接着刚才的下一步即:让head指向Node1,那么此时Node2无法再被找到!!

简单的代码(不要扣细节,大概意思懂就行):

void insert()
{Node->next=head;//①当main函数执行到这里的时候收到2号信号head=Node;//④回到main刚才被暂停的位置继续往下执行,执行完后Node2没有指针指向它,成为丢失节点!!
}
void handler(int signo)
{list->insert(Node);//③执行inser成功头插并把head指向Node
}
int main()
{signal(2,handler);//② 二号信号被捕获并跳转到自定义的handler方法list->insert(Node1);return 0;
}

重入函数:如果一个函数被重复进入的情况下,出错了或者可能出错那么这个函数为不可重入函数!反之则是可重入函数!!!

3.3、volatile

volatile的作用:防止编译器过度优化,保持内存可见性!!

代码:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>
int flag=0;
void handler(int signo)
{flag=1;cout<<"  signo is: "<<signo<<endl;
}
int main()
{signal(2,handler);while(!flag);cout<<"i am process !! process id:"<<getpid()<<endl;return 0;
}

编译器优化后cup没从内存读取flag不会运行下一句:

加上volatile后再编译运行:

cpu从内存中读取flag,循环条件判断失败,指向下一条语句;

四、基于信号方式的进程等待

4.1、当子进程退出时会给父进程发一个信号

我们捕获这个信号然后自定义处理方法去等待回收这个进程,而父进程继续做自己的事:

#include <sys/wait.h>
#include <iostream>
using namespace std;
#include <unistd.h>
void handler(int signo)
{pid_t id= waitpid(-1,nullptr,0);if(id>0){cout<<"waitprocess success!! process id:"<<id<<endl;}
}
int main()
{signal(2,handler);pid_t id=fork();if(id==0){//childcout<<"i am a child process ,process id is: "<<getpid()<<"  ppid is:"<<getppid()<<endl;exit(0);}//fartherwhile(true){cout<<"i am farther process!!"<<endl;sleep(1);}return 0;
}

运行:

发送2号信号:

 

4.2 如果十个进程同时结束呢?或者结束一半后再结束另一半呢?

此时进程在正在执行信号处理方法,会把同信号屏蔽掉,那怎么办?

用循环非阻塞轮询:

一秒钟创建10个进程并处于僵尸状态:

发送2号信号:

成功回收完所有子进程!!

4.3 如果不需要获取子进程退出状态,不想自己等待的话,还有一个办法能让子进程退出时自动被回收掉

捕获17号信号(子进程结束时给父进程发送的信号),并把方法设置为SIG_IGN(忽略):

子进程被自动回收,监视窗口没有Z状态的进程,且只有一个父进程在跑:

今天的分享就到这里,如果对你有所帮助麻烦点赞收藏+关注哦!!谢谢!!!

咱下期见!!!

http://www.xdnf.cn/news/379297.html

相关文章:

  • OpenAI API JSON 格式指南与json_repair错误修复
  • 深入理解卷积神经网络的输入层:数据的起点与预处理核心
  • [Pandas]数据处理
  • MySQL 从入门到精通(六):视图全面详解 —— 虚拟表的灵活运用
  • PyTorch量化感知训练技术:模型压缩与高精度边缘部署实践
  • TDengine 在智能制造中的核心价值
  • 工控新宠| 触想Z系列工控机C款发布,方寸机身,智控万千
  • OSPF综合实验实验报告
  • 深度学习篇---MediaPipe 及其人体姿态估计模型详解
  • 广东省省考备考(第七天5.10)—言语:片段阅读(每日一练)
  • Vue插槽(Slots)详解
  • SkyReels-V2 视频生成
  • Cadence 高速系统设计流程及工具使用三
  • 加速pip下载:永久解决网络慢问题
  • 数据集-目标检测系列- 冥想 检测数据集 close_eye>> DataBall
  • AI实战笔记(1)AI 的 6 大核心方向 + 学习阶段路径
  • Linxu实验五——NFS服务器
  • WordPress插件targetsms存在远程命令执行漏洞(CVE-2025-3776)
  • 20250510-查看 Anaconda 配置的镜像源
  • redis未授权访问
  • [架构之美]从零开始整合Spring Boot与Maven(十五)
  • AUTODL Chatglm2 langchain 部署大模型聊天助手
  • C语言初阶秘籍6
  • 二分法和牛顿迭代法解方程实根,详解
  • 第十九节:图像梯度与边缘检测- Laplacian 算子
  • 「OC」源码学习——cache_t的原理探究
  • C32-编程案例用函数封装获取两个数的较大数
  • IPFS与去中心化存储:重塑数字世界的基石
  • nuscenes_devkit工具
  • Windows:Powershell的使用