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

Linux中信号的保存

一、认识信号的其他相关概念

实际执行信号的处理动作称为信号递达

信号从产生到递达之间的状态,称为信号未决

进程可以选择阻塞某个信号

被阻塞的信号产生时将保持在未决状态,直到进程解除对该信号的阻塞,才进行递达的动作

阻塞和忽略是不同的,阻塞就不会递达。忽略是递达后可选择的一种处理动作

        特定的信号被阻塞(屏蔽,与之前的IO阻塞不同),但是信号已经产生了,一定要把信号进行pending(保存),永远不递达,除非解除阻塞 

二、task_struct中的三张表

这三张表横着看!

在进程的task_struct中,保存着上图三张表:block,pending,handler

pending表:

        其实是一个位图,每个比特位其实就是对应的信号编号,比特位的内容:1/0 表示是否收到对应的信号。其实就是当前进程收到的信号列表

handler表:

        其实是一个handler_t xxx[N]:函数指针数组,信号编号-1:就是函数指针数组的下标 

 block表:

        本质也是一张位图,1-31,比特位的位置与pending表与handler表一一对应,比特位的位置依旧是信号编号,比特位的内容:0/1 是否阻塞/屏蔽特定的信号。假设block表中2号信号为1,也就是把2号信号给屏蔽了,这时pending表的2号信号为1,也不会执行handler表递达

如果没收到信号,能不能提前对信号进行屏蔽?

能!因为这是两张位图 

三、sigset_t

每个信号只有一个比特位的未决标志,非0即1,不记录信号产生了多少次。因此未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,称为信号集,这个类型可以表示每个信号的有效或无效状态。阻塞信号集也叫做当前进程的 信号屏蔽字(SignalMask)

四、对于位图的增删查改接口

sigprocmask

读取或更改进程的信号屏蔽字(阻塞信号集),也就是修改block表

返回值:成功为0,失败为-1

参数介绍:

int how 的可选值分别是:

SIG_BLOCK(增添屏蔽信号)

SIG_UNBLOCK(解除屏蔽信号)

SIG_SETMASK(覆盖原有的屏蔽信号)

const sigset_t *set:        输入型参数,一个新的信号集

sigest_t *oldset:        输出型参数,被修改前的信号集 

 sigpending

获取当前的pending表,参数是输出型参数

为什么没有对pending的修改的?其实我们已经是通过硬件,软件条件,指令,系统调用等方法对pending表进行修改了,所以sigpending函数不需要再额外提供修改pending表的方法 

handler表的修改?通过signal

其他接口介绍

#include <signal.h> 
//初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含
任何有效信号。
int sigemptyset(sigset_t *set); //初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰该信号集的有效信号
包括系统⽀持的所有信号
int sigfillset(sigset_t *set); //增加信号集上的某个信号
int sigaddset(sigset_t *set, int signo); //删除信号集上的某个信号
int sigdelset(sigset_t *set, int signo); //查看某个信号是否在信号集里,在的话返回1,否则返回0
int sigismember(const sigset_t *set, int signo);

实验:屏蔽2号信号,打印pending表,验证pending表的变化 

//1.屏蔽二号信号
//2.获取并打印pending表
//3.发送2号信号,观察效果
void PrintPending(const sigset_t &pending)
{std::cout<<"pending List:["<<getpid()<<"]:";for(int i=32;i>0;i--){//判断当前信号在不在信号集中if(sigismember(&pending,i)){std::cout<<1;}else{std::cout<<0;}}std::cout<<std::endl;
}
int main()
{//这两个数据结构是在用户栈上面开辟的,是乱码,所以我们需要对其进行清0,但是不建议手动清0sigset_t block,oblock;sigemptyset(&block);sigemptyset(&oblock);//将2号信号进行屏蔽sigaddset(&block,2);//并没有设置进内核中,只是在用户栈上设置了block的位图结构//设置进内核中sigprocmask(SIG_SETMASK,&block,&oblock);//获取pending表while(1){//int sigpending(sigset_t *set);//输出型参数sigset_t pending;sigpending(&pending);//打印PrintPending(pending);sleep(1);}return 0;
}

打开新终端,输入kill -2 进程号,OS向进程发送2号信号,相应的位图的2号位置置1,因为我们提前将二号信号进行屏蔽,所以2号信号无反应

那么我们该如何解除对2号信号的屏蔽呢?怎么屏蔽就怎么解屏蔽

void PrintPending(const sigset_t &pending)
{std::cout<<"pending List:["<<getpid()<<"]:";for(int i=32;i>0;i--){//判断当前信号在不在信号集中if(sigismember(&pending,i)){std::cout<<1;}else{std::cout<<0;}}std::cout<<std::endl;
}
void handler(int signo)
{std::cout<<"对2号信号进行了处理"<<std::endl;
}
int main()
{//这里是block二号信号后再解除2号信号的屏蔽的操作signal(2,handler);//这两个数据结构是在用户栈上面开辟的,是乱码,所以我们需要对其进行清0,但是不建议手动清0sigset_t block,oblock;sigemptyset(&block);sigemptyset(&oblock);//将2号信号进行屏蔽,先对位图进行修改sigaddset(&block,2);//并没有设置进内核中,只是在用户栈上设置了block的位图结构//设置进内核中,sigpromask是对block表的修改sigprocmask(SIG_SETMASK,&block,&oblock);int cnt=10;//获取pending表while(1){//int sigpending(sigset_t *set);//输出型参数sigset_t pending;sigpending(&pending);//打印PrintPending(pending);sleep(1);cnt--;if(cnt==0){std::cout<<"解除对2号信号的屏蔽"<<std::endl;//怎么屏蔽的就怎么解除屏蔽,oblock中存储了原本的位图sigprocmask(SIG_SETMASK,&oblock,nullptr);//SIG_SETMASK 覆盖原本的位图}}return 0;
}

 运行后对进程发送2号信号,可以看到,发送2号信号后,位图相应的位置变成了1,当解除了对2号信号的屏蔽后,位图相应的位置变回了0

 五、信号捕捉

sigaction

        相比 signal 功能更丰富,能对信号处理进行更精细控制 。成功时返回 0 ,失败返回 -1 并设置 errno

其中,act和oldact是一个结构体!这个结构体的内容如下:

我们只需要关注红色箭头这两个参数即可,一个是对应的方法的函数指针

另一个是sa_mask 定义了在执行信号处理函数时需要阻塞的信号集合。也就是说,当信号处理函数开始执行时,sa_mask 中包含的信号将被阻塞,不会被立即处理,直到信号处理函数执行完毕,被阻塞的信号屏蔽字才会恢复到之前的状态。OS这样做规避了信号处理被嵌套的情况,避免了栈溢出!

验证OS不允许信号处理方法进行嵌套:

#include<iostream>
#include<signal.h>
#include<unistd.h>
//验证OS不允许信号处理方法进行嵌套
void handler(int signo)
{static int cnt=0;cnt++;while(1){std::cout<<"get a signal:"<<signo<<"cnt:"<<cnt<<std::endl;sleep(1);}exit(1);
}
int main()
{struct sigaction act, oact;//新的处理方法,旧的处理方法act.sa_handler=handler;sigaction(2,&act,&oact);//将2号信号的执行方法改成自定义while(1){pause();}return 0;
}

运行代码可以发现,处理方法并没有进行嵌套 

信号处理函数执行完毕,被阻塞的信号屏蔽字才会恢复到之前的状态 

void PirintBLock()
{sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);//SIG_BLOCK(增添屏蔽信号),新的信号集,旧的信号集sigprocmask(SIG_BLOCK, &set, &oset);//调用它的这个进程自己的信号屏蔽集std::cout << "block: ";//打印block表中31-1号信号的信号集for (int signo = 31; signo > 0; signo--){if (sigismember(&oset, signo)){std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void handler(int signo)
{static int cnt = 0;cnt++;while (true){std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;PirintBLock();sleep(1);break;}}int main()
{struct sigaction act, oact;//结构体act中,我们只需要关注两个变量,handler与sa_mask,自定义函数指针与信号集act.sa_handler = handler;sigemptyset(&act.sa_mask);//对位图进行清0sigaddset(&act.sa_mask, 3);//将3号信号加入到信号集中sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);sigaddset(&act.sa_mask, 6);sigaddset(&act.sa_mask, 7);::sigaction(2, &act, &oact);//更改2号信号的处理方法,处理方法为打印block表,打印一次后退出,2号信号旧不阻塞了,那么走到下面的while,位图恢复while (true){PirintBLock();pause();}
}

当我们发送2号信号时,执行自定义函数方法 

现在,我们知道了,在进程执行信号方法时,block表中相应的信号会被置1,也就是把当前执行的信号进行屏蔽,当handler表中的方法执行完成后,再将block表进行回复。

收到信号后pending表的修改以及恢复

我们知道,当进程收到信号的时候,pending表相应的信号会置为一,相当于时进程收到了该信号

那么pending表从1变回0这个过程,是在handler表中的方法完成后变回0,还是handler表中的方法执行期间变回0的?

答案是执行handler表期间的方法时,pending表从1变回0的,如果是在执行完函数方法后再变回0的,那么pending表中的1代表的是之前收到的信号,还是新收到的信号?这样OS就分辨不清了

我们也可以通过代码来观察一下pending表中的信号集

在信号处理之间,pending表全为0,证明在handler之间把pending表清0

void PrintPending()
{sigset_t pending;::sigpending(&pending);//输出型参数,输出pending表的信号集std::cout << "Pending: ";for (int signo = 31; signo > 0; signo--){if (sigismember(&pending, signo))//查看某个信号是否在信号集里{std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void handler(int signo)
{static int cnt = 0;cnt++;while (true){std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;PrintPending();sleep(1);}
}int main()
{struct sigaction act, oact;//结构体act中,我们只需要关注两个变量,handler与sa_mask,自定义函数指针与信号集act.sa_handler = handler;sigemptyset(&act.sa_mask);//对位图进行清0sigaddset(&act.sa_mask, 3);//将3号信号加入到信号集中sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);sigaddset(&act.sa_mask, 6);sigaddset(&act.sa_mask, 7);::sigaction(2, &act, &oact);//更改2号信号的处理方法,处理方法为打印block表,打印一次后退出,2号信号就不阻塞了,那么走到下面的while,位图恢复while (true){PrintPending();pause();}
}
//当我们第二次发送2号信号时,pending表的2号信号又重新置为1,只是因为我们的block表中2号信号置1了

代码编译完成,运行,可以看到,pending表全为0,说明是在handler表的方法处理中把pending表清0的 

 

六、编译器的优化与volatile

在我们的编译器中,其实有一个优化功能,gcc test.c -O1/O2/O3

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int flag = 0;void change(int signo) // 信号捕捉的执行流
{(void)signo;flag = 1;printf("change flag 0->1, getpid: %d\n", getpid());
}int main()
{printf("I am main process, pid is : %d\n", getpid());signal(2, change);while (!flag); // 主执行流--- flag我们没有做任何修改!printf("我是正常退出的!\n");
}

不做任何的优化 ,每一次while,都要将flag从内存放到寄存器中做逻辑运算

如果做优化,提前把flag从内存放到寄存器中,省略了缓存这一步骤,所以我们的while时一直跑的

但是不同的平台对编译器的优化有差别,有的默认就会做优化,有的默认是不做优化,为了解决这种情况,我们可以使用volatile关键字对变量就行修饰,它的作用与static相反

volatile int flag = 0;

 七、通过信号解决父进程等待子进程

验证子进程退出时向父进程发送17号信号

void handler(int signo)
{std::cout<<"Get a signal: "<<signo<<std::endl;
}
int main()
{signal(17,handler);if(fork()==0){sleep(5);exit(0);}while(1);return 0;
}

我们知道,父进程是需要回收子进程的,所以我们也可以加上waitpid() 

void handler(int signo)
{std::cout << "Get a signal: " << signo << std::endl;
}
int main()
{signal(17, handler);if (fork() == 0){sleep(5);exit(0);}while (1){//这里直接waitpidwaitpid(-1, nullptr, 0);//回收任意子进程,阻塞等待sleep(2);std::cout<<"子进程退出成功"<<std::endl;exit(1);}return 0;
}

        但是这样写并不好看,因为我们在父进程中把回收子进程写的太过于明显,而且如果我们没有回收子进程,waitpid后面的代码就无法运行,也就是我们的子进程与父进程无法并行 !    因此,我们可以基于子进程结束时向父进程发送信号这一原理,直接在信号的调用方法动刀

void handler(int signo)
{std::cout << "get a sig: " << signo << " I am : " << getpid() << std::endl;while (true){pid_t rid = ::waitpid(-1, nullptr, WNOHANG);//回收任意一个进程,非阻塞等待,这样就可以解决10个进程回收6个,4个还在运行的情况if (rid > 0){std::cout << "子进程退出了,回收成功,child id: " << rid << std::endl;}else if(rid == 0){std::cout << "退出的子进程已经被全部回收了" << std::endl;break;}else{std::cout << "wait error" << std::endl;break;}}
}
int main()
{signal(17,handler);//多个子进程都向父进程发信号for (int i = 0; i < 10; i++){if (fork() == 0){sleep(5);std::cout << "子进程退出" << std::endl;// 子进程exit(0);}}//父进程一直运行while(1);return 0;
}

最优解:

        SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不 会产⽣僵⼫进程,也不会通知⽗进程。

signal(SIGCHLD,SIG_IGN);

    


http://www.mrgr.cn/news/98520.html

相关文章:

  • 阿里FPGA XCKU3P开箱- 25G 光纤
  • 【CUDA 】第3章 CUDA执行模型——3.5循环展开(3)
  • 音视频小白系统入门笔记-0
  • 【强化学习漫谈】3.RLHF:从Reward Model到DPO
  • 代码随想录算法训练营Day30
  • C#中async await异步关键字用法和异步的底层原理
  • 怎么看英文论文 pdf沉浸式翻译
  • (二)Graspnet在mujoco的仿真复现(操作记录)
  • linux多线(进)程编程——(7)消息队列
  • Leetcode 2814. 避免淹死并到达目的地的最短时间【Plus题】
  • 第IV部分有效应用程序的设计模式
  • STM32F103_HAL库+寄存器学习笔记15 - 梳理CAN发送失败时,涉及哪些寄存器
  • 实战指南:封装Whisper为FastAPI接口并实现高并发处理-附整合包
  • 欧拉服务器操作系统安装MySQL
  • 单片机 + 图像处理芯片 + TFT彩屏 触摸开关控件 v1.2
  • Transformer-PyTorch实战项目——文本分类
  • (劳特巴赫调试器学习笔记)四、Practice脚本.cmm文件编写
  • C++第三方库【JSON】nlohman/json
  • Cribl 数据脱敏 -02 (附 测试数据)
  • 如何评估cpu的理论FLOPS能力