💓博主CSDN主页:杭电码农-NEO💓
⏩专栏分类:Linux从入门到精通⏪
🚚代码仓库:NEO的学习日记🚚
🌹关注我🫵带你学更多操作系统知识
🔝🔝
进程信号
- [1. 前言](#1. 前言)
- [2. 信号阻塞,信号递到和信号忽略](#2. 信号阻塞,信号递到和信号忽略)
- [3. 进程是怎样保存信号的?](#3. 进程是怎样保存信号的?)
- [4. 信号集操作函数](#4. 信号集操作函数)
- [5. 进程是如何捕捉信号的?](#5. 进程是如何捕捉信号的?)
- [6. 总结](#6. 总结)
1. 前言
上一篇文章了解到信号产生的四种方式,
但是信号产生后,然后呢?需要对信号
进行保存,最后对信号进行处理
如果你没有阅读过前一篇文章,或者不知道信号的默认处理方式,请先阅读这篇文章: 信号的基本概念
本章重点:
本篇文章着重讲解信号保存的方式以及
周边概率,信号阻塞,信号递达.理解
信号处理的默认方式,如何修改默认方法
最后会讲解进程是如何捕捉信号的?
2. 信号阻塞,信号递到和信号忽略
在讲解进程是如何保存信号之前,要
先了解下面几个概念:
信号递达:
实际执行处理信号的动作叫做信号的递达
信号的未决状态:
在信号产生到信号递达之间的状态叫做信号的未决状态
信号阻塞:
一旦一个信号被设置了阻塞之后,那么此进程就不会收到此信号
信号忽略:
一旦一个信号被设置为忽略,那么当这个信号来临后,进程不会对此信号做处理
阻塞和忽略的区别:
一个信号被阻塞后,它就不会递达,而忽略是指信号递达后,执行的动作是什么都不做
3. 进程是怎样保存信号的?
在进程的PCB中,存在三张和信号相关的表
更准确的讲,前两个结构是位图,最后一个是表(数组).
- block位图:
这个位图代表,在这个进程中,有哪些信号是被阻塞了的?位图的第一个元素为0,代表1号信号没有被阻塞,第n个位置为1,代表n号信号被阻塞了,也就是说0/1代表某个信号是否被阻塞,这个位图又被称为信号屏蔽字
- pending位图:
这个位图是进程存储信号的结构,位图中的第n个位置为0/1代表是否收到了n号信号,如若收到,会在后续进行处理,这个位图又被称为信号集
- handler数组:
首先,这个数组是一个函数指针数组,里面存储的是函数的地址,数组的n号元素存储的函数地址代表收到n号信号之后,要去处理信号时,需要调用的函数.在上图中,SIG_DFL宏代表这个函数就是此信号的默认处理函数,SIG_IGN宏代表收到这个信号后,直接忽略此信号,当然我们也可以自己写一个函数来充当信号的处理方法,这个后面会讲
4. 信号集操作函数
首先我想隆重介绍的是signal函数:
第一个参数代表要设置几号信号
第二个参数代表,信号到来需要调用哪个函数
下面可以进行一个简单的编码验证:
cpp
void mycatch(int signum)
{
cout<<"进程捕捉到了一个信号,正在自定义处理中... "<<signum<<"pid: "<<getpid()<<endl;
}
int main()
{
signal(SIGINT,mycatch);//既可以填写定义的宏,也可以直接写数字
while(1)
{
cout<<"我是一个进程,我正在运行...pid: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
下面的内容能掌握的最好:
信号集操作函数概览:
读取或更改信号屏蔽字:
下面是样例代码,有兴趣可以看看:
cpp
void showpending(sigset_t& tmp)
{
for(int i=1;i<=31;i++)
{
if(sigismember(&tmp,i))
cout<<1;
else
cout<<0;
}
cout<<endl;
}
void blocksig(int sig)//对指定信号对屏蔽
{
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset,sig);
int n = sigprocmask(SIG_BLOCK,&bset,NULL);//只对sig号信号屏蔽
assert(n==0);
cout<<"block success!"<<endl;
}
int main()
{
cout<<getpid()<<endl;
for(int i=1;i<=31;i++)//将所有信号都屏蔽掉
blocksig(i);
sigset_t pending;
while(1)
{
sigpending(&pending);
showpending(pending);
sleep(1);
}
return 0;
}
5. 进程是如何捕捉信号的?
先说结论:
从内核态返回用户态时,会进行信号的检测和处理
在此之前,大家肯定会有疑问:什么是内核态?什么是用户态?为什么这两个状态会相互切换?下面就来解答这些问题:
用户态指的是程序在执行用户写的代码时,会使用用户态的身份来执行代码,那么什么时候会进入内核态呢?答案是代码中存在系统调用,代码出现异常等情况,操作系统会将身份切换为内核态来执行代码.所以为什么要切换状态呢?直接用用户态执行全部代码难道不行吗?答案一定是否认的,因为群众中有坏人,在执行系统调用时,由于是很底层的代码或函数,所以操作系统是不信任用户的,切换为内核态的一大原因是为了安全性.另一方面,使用内核态执行代码时的优先级非常高.那么操作系统是怎样在两个状态中做切换的呢?答案很简单,CPU有两套寄存器,一套是可见的,一套是不可见的,而在不可见的这一套寄存器中,有一个寄存器叫CR3,它表示当前CPU的执行权限,数值为1代表内核态,数值为3代表用户态
了解完前景知识后,我们就可以得出一些结论:
- 当程序执行系统调用时会进入到内核态
- 当执行完系统调用后,会回到用户态
- 在这期间会进行信号的检测和处理
- 如若此时检测到有信号到来,那么会把代码直接跳转到信号处理的函数处
- 当信号处理函数返回时还会执行特殊的系统调用,再回到内核态
下面可以用一张图来代表整个过程:
把图片简化一下,就得到了一个无穷大的图像:
6. 总结
大家下来可以去试试将所有的信号都设置为阻塞,或者忽略,会发生什么?一旦这样做,进程运行起来后使用CTRL+c或者CTRL+/都不能终止进程,并且使用kill命令也无法杀掉进程.即使如此,即使所有信号都被屏蔽或忽略,但第九号信号是无法被屏蔽和忽略的,所以任何情况下都可以使用kill -9 pid来杀掉一个进程
信号这一章节是我们学习进程的最后一节,由于信号与进程的紧密关系,所以学习信号至关重要,除此之外,当子进程退出时也会向父进程发生SIG_CHILD信号,假设父进程并不想关心子进程的退出结果,只想执行自己的代码,那么我们可以将SIGCHLD信号设置为忽略,这样一来,父进程收到子进程退出的信号后就不会再拿一部分时间或资源来处理子进程了!
🔎 下期预告:线程的基本概念 🔍