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

【Linux取经之路】进程信号的保存

目录

前言

信号的捕捉

信号的保存

信号集和信号屏蔽字

信号集操作函数


前言

上一篇文章谈了进程信号的产生,这篇文章我们来聊一聊进程信号的处理。废话不多说,我们直入主题。

我们已经知道了信号是如何产生的以及信号是由操作系统发送给指定进程的。那么,有以下几个问题。

1)进程接收到操作系统发送的信号后,会立即处理吗?如果不是立即处理,那什么时候处理?

不会立即处理,而是会在合适的时候处理。合适的时候处理,这不废话吗?是的,关于什么时候处理我们后面会说。这里我们先通过一个例子来感性的理解。张三正在寝室里打王者荣耀,这时接到了外卖小哥的电话说外卖到学校东门了,过来取一下。恰巧张三正在进行团战,他意识到如果这波团输了,这把就很难再压制对面了。于是,张三让外卖小哥先把外卖放在外面柜里,他打完了再去取。张三就相当于一个进程,他正在忙自己的事情,而且优先级很高,外卖小哥的电话就相当于一个信号,他在接收到这个信号的时候并没有直接去执行,因为他还有更重要的事情要做。进程也有自己的事情要做,所以并不会立即执行操作系统发送给的信号。

2)进程接收到操作系统发给的信号后,怎么处理?

a.默认行为

b.忽略信号

c.自定义动作

下面举个例子来说明这三种做法。

张三在晚上睡前,看了一下第二天的课表,发现有早八,于是他定了早上7点的闹钟。第二天早上,闹钟响后,我们都知道,该起来洗漱吃早餐去上课了,这是默认行为。把闹钟一关接着睡,这是忽略信号。还有就是闹钟一响,张三并没有像我们认为的那样去洗漱然后准备去上课了,而是立马起床跳舞,这是张三的自定义动作。在这个例子中,张三可以看做是一个进程。

下面。我们来看看一些常见信号的默认动作。

命令:man 7 signal

可以看到,SIGINT这个信号的默认行为是Term,Term在图二中的解释为终止进程。

有了以上的铺垫,我们接着往下走。

信号的捕捉

关于信号的捕捉,这里我只介绍一个函数——signal。关于信号捕捉的内容,我还会再写一篇相关博客,因为都放在一篇里,篇幅是在太长了。

signum:这是一个整数参数,表示要处理的信号编号。 

handler:这是一个函数指针参数,指向一个信号处理函数。该函数接收一个整数参数(即信号编号),无返回值。

我们知道,Ctrl + c 产生的是2号信号SIGINT,该信号的默认处理办法是终止当前进程。如果我们捕捉了该信号,并让它执行我们的自定义行为,那是不是在按下Ctrl + C的时候进程就不会被干掉?实践出真知,我们来试试。

 

可以看到,我捕捉了2号信号,让它执行自定义行为。这时,我再用Ctrl + C试图去终止这个进程时,是无法做到的。这里说明一下,因为代码比较简单,所以我直接把截图拿过来了。遇到比较复杂的代码时,我会以文本的方式粘贴,方便大家复制。

关于上面的代码,有同学可能会有这样的疑问——如果不产生2号信号,handler方法会不会被调用?

不会,这里面涉及到一种机制,就是在没有触发相应信号时,对应的方法永远不会被调用。

也许你会这么想:如果我们把所有的信号都捕捉了,那么是不是意味着操作系统再也干不掉我们的进程了?我们是不是可以为所欲为了?哈哈,想法不错哈,我们来试试看。

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void handler(int signum)
{
    cout << "我捕捉了" << signum << "号信号" << endl;
}

int main()
{
    for(int signum = 1; signum < 32; signum++)
    {
        signal(signum, handler);
        cout << "自定义捕捉" << signum << endl;
    }
    while (true)
    {
        sleep(1);
        cout << "hello world" << endl;
        cout << getpid() << endl;
    }
    return 0;
}

可以看到,不管是Ctrl + C 还是Ctrl + \都干不掉该进程,也试着kill了几下,发现还是没用。真拿它没办法了?!当然不是,只不过还没击中要害罢了。下面我用kill -9 pid 试试。

果然,终于把它干掉了,原因在于9号信号无法被捕捉,设计操作系统的工程师早就想到我们会干坏事啦。所以一般情况下,我们都喜欢用kill -9 去杀进程。

信号的保存

我们已经知道了,进程在接收到操作系统发送的信号后,并不会立即执行,所以从接收到处理的这一段时间内,需要将信号保存起来。可是,怎么保存呢?我在《进程信号的产生》这篇文章里浅浅的提到过,不管你是否看过都不会妨碍理解,我还会再提的。

在进程的PCB内部,有一张位图signalbits,如下图:

比特位的位置就是信号的编号,比特位的内容1/0,表示是否收到对应信号。除此之外,进程的PCB内部还有这样一个数组sighandler_t arr[32] .

 sighandler_t为函数指针,所以arr数组就是一个函数指针数组,数组里的指针指向默认方法或者自定义方法等。

所以,发送信号的本质是写入信号——操作系统修改目标进程PCB的位图,0 -> 1(pending表)。 

下面我们看看进程PCB内部的几张位图,进一步加深理解。

其中pending表就是当前进程收到的信号列表, handler表就是上面所说的函数指针数组。我们横着看,比如2号信号,pending表中对应位置的值为1,说明该进程收到了2号信号,再往后看,可以看到对应的是handler表中的SIG_IGN,说明该信号的处理方式为SIG_IGN(即忽略)。

所以,你知道为什么signal函数只需要调用一次,对应的信号发无数次都是同一个处理方法了吗?因为handler表中的对应位置只需改一次就够了。

还剩block表没提了,在谈它之前,我们先来补充几个概念。

● 实际执行信号的处理动作称为信号递达(Delivery)
● 信号从产生到递达之间的状态, 称为信号未决 (Pending)
● 进程可以选择阻塞 (Block ) 某个信号。
● 被阻塞的信号产生时将保持在未决状态, 直到进程解除对此信号的阻塞 , 才执行递达的动作 .
● 注意, 阻塞和忽略是不同的 , 只要信号被阻塞就不会递达 , 而忽略是在递达之后可选的一种处理动作。
其中,pending表为1就说明已经接收到信号,且处于未决状态。调用handler中的方法时,叫信号递达。
block表中,1/0表示对应信号是否被阻塞。这里的阻塞和IO阻塞是完全不同的概念,这里的阻塞可以理解为屏蔽。
举个例子,我们看上图中的第二行,即2号信号,block表中的值为1,pending表中的值也为1,handler表中对应的SIG_IGN。意思就是,该进程选择屏蔽2号信号,即使2号信号已经被接收,也不可能被执行,即不可能递达。除非把block表中的1改为0。
有的同学可能会又这样的疑问——不是屏蔽了2号信号了嘛,它为什么还是被接收了?原因在于block表和pending表是独立的,你改你的我改我的,互不影响。

信号产生以及传递的一种路径为:键盘 -> 操作系统 -> 进程。现在我们已经知道了信号是如何从操作系统发送给进程的,就剩前面键盘->操作系统这一步没有打通,下面我们浅浅的谈一谈硬件。

操作系统是如何知道键盘上有数据要读入的?一直轮询检测吗?当然不是,这样操作系统会忙死。这里就不得不抛出一个名词——硬件中断。到这,也不可避免的谈到冯诺依曼体系了。下面是冯诺依曼体系结构图:

我们可以看到,CPU在数据层面上,是不直接与外部的输入设备打交道的。但是在控制信号层面上,输入设备是直接与CPU相连的,当输入设备把数据准备好后,会给CPU发送一个中断信号,CUP接收到中断信号后会告诉操作系统,外部设备已经准备好了,然后操作系统就会把外部设备输入的数据拷贝到内存。这样,操作系统只需要静静地等待即可,不用去轮询。 

信号集和信号屏蔽字

在上面,我们已经知道PCB中关于信号的三张位图。下面,当然是要围绕这几张位图来进行操作啦!可以明确的是,这几张位图是属于进程的内核数据结构,所有我们需要使用系统调用才可以对它们进行操作。接下来,为了更好的说明,我们先来补充几个概念。

1)sigset_t

sigset_t是一种数据类型,用于描述一个信号集。信号集又是什么?信号集就是一个能表示多个信号的数据类型,比如pending表,就是一个未决信号集,它可以表示1到31号信号是否处于未决状态。block表也是一个信号集——阻塞信号集,表示1到31号信号哪些信号被阻塞了,哪些信号没被阻塞。

2)  信号屏蔽字signal mask

知道了什么是信号集后,信号屏蔽字就很好理解了。信号屏蔽字就是一个阻塞信号集。

信号集操作函数

int sigemptyset(sigset_t *set);//清空set指向的信号集,所有位清零

int sigfillset(sigset_t *set);//和sigemptyset相反,所有位置1

int sigaddset(sigset_t *set, int signum);//向指定信号集里面添加信号

int sigdelset(sigset_t *set, int signum);//删除指定信号集里的指定信号

int sigismember(const sigset_t *set, int signum);//检查某个信号是否在该信号集中

关于以上函数的更多细节还请各位自行man sigemptyset。

需要注意的是,在使用sigset_t类型的对象时,必须先使用sigemptyset或sigfillset初始化。

● sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能一个用于获取或更改调用进程的信号屏蔽字的函数。

参数介绍:

  • how 参数指定了如何更改信号屏蔽字:

    • SIG_BLOCK:添加 set 中指定的信号到当前屏蔽字中,相当于 mask = mask | set。
    • SIG_UNBLOCK:从当前屏蔽字中移除 set 中指定的信号,相当于 mask = mask & ~set。
    • SIG_SETMASK:将当前屏蔽字设置为 set 中指定的值,相当于 mask = set。
  • set 参数指向一个 sigset_t 类型的变量,该变量包含了要更改的信号集。

  • oldset 参数(如果非空)指向一个 sigset_t 类型的变量,该变量在函数调用后将包含调用前的信号屏蔽字,该参数可以为空。

● sigpending

 int sigpending(sigset_t *set);

功能:读取当前进程的未决信号集。

函数的介绍就到这啦,下面我们通过一个程序来掌握上面一些函数的用法。

如果我们对2号信号进行屏蔽,那么我们在给指定进程发送2号信号时,它将不会被递达。也就是说kill -2 pid 杀不掉这个进程了。但即使杀不掉该进程,我们在个它发送2号信号的时候它所对应的pending表中的位依然从0变为1。通过下面程序我们可以看到这个现象。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void printPending(sigset_t& pending)
{
    cout << "curr pending [" << getpid() << "] :" ;
    for(int signum = 31; signum > 0; signum--)
    {
        if(sigismember(&pending, signum)) cout << 1;
        else cout << 0;
    }
    cout << endl;
}

int main()
{
    sigset_t block, oblock;
    //初始化
    sigemptyset(&block);
    sigemptyset(&oblock);

    //对2号信号进行屏蔽,所以在使用kill命令传2号信号杀该进程时,
    //杀不掉,但通过打印pending表,可以看到对应的位置被置为1了

    //仅仅sigaddset只是修改了在栈上开辟的信号集
    //要想修改进程内核数据结构中的表,需要调用sigprocmask函数
    sigaddset(&block, 2);    
    sigprocmask(SIG_SETMASK, &block, &oblock);

    while(true)
    {
        sigset_t pending;
        //检查pending表
        sigpending(&pending);
        printPending(pending);
        sleep(1);
    }
    return 0;
}

可以看到,我在向指定进程发送2号信号后,pending表中对应的位置从0变为1,但是由于2号信号被屏蔽了,所有不会递达,因而不会终止进程。下面解除对2号信号的屏蔽。怎么解除呢?还是通过sigprocmask函数来处理,oblock中保存了我们旧的pending表,我们只需把旧的设置回去即可。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void printPending(sigset_t& pending)
{
    cout << "curr pending [" << getpid() << "] :" ;
    for(int signum = 31; signum > 0; signum--)
    {
        if(sigismember(&pending, signum)) cout << 1;
        else cout << 0;
    }
    cout << endl;
}

int main()
{
    //解除对2号信号的屏蔽后,它会第达,
    //递达后选择忽略2号信号,所以进程不会被2号信号干掉
    signal(SIGINT, SIG_IGN);
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);

    //对2号信号进行屏蔽,所以在使用kill命令传2号信号杀该进程时,
    //杀不掉,但通过打印pending表,可以看到对应的位置被置为1了
    sigaddset(&block, 2);    
    sigprocmask(SIG_SETMASK, &block, &oblock);

    int cnt = 0;
    while(true)
    {
        sigset_t pending;
        //检查pending表
        sigpending(&pending);
        printPending(pending);
        sleep(1);
        cnt++;
        if(cnt == 20) sigprocmask(SIG_SETMASK, &oblock, nullptr);
    }
    return 0;
}


本文到这就结束啦~如有错误,请不吝指出! 

 


http://www.kler.cn/a/393541.html

相关文章:

  • 大白话拆解——多线程中关于死锁的一切(七)(已完结)
  • vue——滑块验证
  • 【数据仓库】hadoop web UI 增加账号密码认证
  • 性能测试03|JMeter:断言、关联、web脚本录制
  • Qt|QWidget窗口支持旋转
  • zookeeper+kafka
  • Python 正则表达式的一些介绍和使用方法说明(数字、字母和数字、电子邮件地址、网址、电话号码(简单)、IPv4 )
  • 报名开启|开放原子大赛“Rust数据结构与算法学习赛”
  • 吴恩达深度学习笔记(12)14
  • VBA高级应用30例应用3在Excel中的ListObject对象:插入行和列
  • 阿里云云效制品仓库(maven)私服配置快速入门
  • Linux软件包管理与Vim编辑器使用指南
  • 文件包含绕过(session打条件竞争应该是文件上传的!!!)
  • Python使用总结之如何去除图片的水印?
  • JavaScript入门笔记
  • SQL,力扣题目1107,每日新用户统计
  • Unity中实现战斗帧同步的高级技术
  • 网安加·百家讲坛 | 仝辉:金融机构鸿蒙应用安全合规建设方案
  • 重构代码之内联方法
  • 7、computed计算属性使用
  • 数据库参数备份
  • 爬虫开发工具与环境搭建——开发工具介绍
  • Spring Boot——日志介绍和配置
  • LeetCode 3249.统计好节点的数目:深度优先搜索(DFS)
  • WPF 中的视觉层和逻辑层有什么区别?
  • 问题(十九)JavaAgent-ByteBuddy与CGLIB字节码增强冲突问题