Linux信号机制探析--信号的保存


🍑个人主页:Jupiter. 🚀 所属专栏:Linux从入门到进阶 欢迎大家点赞收藏评论😊

目录


🐀信号其他相关常见概念

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

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

进程可以选择阻塞 (Block )某个信号。

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

注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

🦋信号保存

信号到来的时候,如果进程正在处理更重要的事情,暂时不能及时处理该信号,进程会将该信号进行临时保存。临时保存,那保存在哪里呢?

  • 保存在进程的PCB中,采用位图数据结构进行保存,称为pending位图。其中,比特位的位置,表示信号的编号,比特位的内容,表示是否收到该信号
  • 发送信号,实际上是OS进程的PCBpending位图中写入信号。PCB是内核数据结构,如果用户要更改pending内容,需要通过系统调用。

🐛阻塞信号

在内核中的表示

每个信号都有两个标志位分别表示阻塞(block 位图)未决(pending 位图),还有一个函数指针表示处理动作

信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?

  • Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
  • 其中每一个信号的默认处理是放在handler的函数指针数组里面的,实际上自定义捕捉就是将自己所写的函数的地址与handler中对应的位置的内容替换掉。
  • 当一个进程收到一个信号是,即在pengding位图中有该信号,会在block 位图中看该信号是否被阻塞,如果被阻塞,则不会被递达,如果没有被阻塞,就会在handler数组里面查找相对应的方法,将信号递达。
  • OS向目标进程发送信号,实际上是向目标进程写入信号。

阻塞 VS 忽略:忽略是信号递达的一种方式,阻塞仅仅是不让指定的信号递达。

阻塞一个信号和是否收到指定信号没有关系。

🦗三张表匹配的操作和系统调用

sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集(OS提供的),这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。

🐜信号集操作函数

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

cpp 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);  //所有比特位置0
int sigfillset(sigset_t *set);    //所有比特位置1
int sigaddset (sigset_t *set, int signo);   //指定的比特位置1
int sigdelset(sigset_t *set, int signo);     //指定的比特位置0
int sigismember(const sigset_t *set, int signo);   //判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1

函数sigemptyset``初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示 该信号集的有效信号包括系统支持的所有信号。
注意,使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含返回1,不包含返回0,出错返回-1。

示例代码:

cpp 复制代码
void Print(sigset_t s)
{
    for(int i = 31;i>=1;i--)
    {
        if(sigismember(&s, i)!=0)
            cout<<"1";
        else 
            cout<<"0";
    }
    cout<<endl;
}
int main()
{
    sigset_t s;

    sigemptyset(&s);  //清零
    
    sigfillset(&s);  //全部置1

    sigaddset(&s, 2);  //给2号bit置1
    
    sigdelset(&s, 2);  //给2号bit位取消置1,置为0

    sigismember(&s, 2);  //判断2号bit位是否为1   返回值为0,则表明不在

    Print(s); //打印pengding位图

    return 0;
}

🐸sigprocmask (操作block位图)

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

  • 返回值:若成功则为0,若出错则为-1 。

  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。

  • 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

    如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

注意:

  • 递达处理的时候会将pending位图中将特定的比特位清0;
  • 解除了屏蔽后,是先将block位图中的该信号的比特位置0,然后再递达处理;
能否屏蔽所有的信号呢?
  • 根据测试发现,9号信号SIGKILL与19号信号SIGSTOP无法屏蔽,18号信号SIGCONT会做特殊处理(18号信号屏蔽了会让20,21,22号信号解除屏蔽)。

🐟sigpending(操作pending位图)

int sigpending(sigset_t *set);

参数为输出型参数,获取当前进程的pending位图的32个比特位,获取成功返回0,失败返回-1。

示例代码:

  • 测试场景:
    1.屏蔽2号信号
    2.获取并打印pengding位图
    3.然后一会儿给目标进程发送2号信号-->不会被递达--->2号信号会在pending位图中
    4.取消对2号信号的阻塞
    5.获取并打印pengding位图
cpp 复制代码
void Print(sigset_t s)
{
    for(int i = 31;i>=1;i--)
    {
        if(sigismember(&s, i)!=0)
            cout<<"1";
        else 
            cout<<"0";
    }
    cout<<endl;
}
int main()
{
    // 1. 屏蔽2号信号
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigaddset(&block, 2); // SIGINT --- 没有设置进当前进程的PCB block位图中

    // 1.1 开始屏蔽2号信号,其实就是设置进入内核中
    int n = sigprocmask(SIG_SETMASK, &block, &oblock);
    assert(n == 0);
    std::cout << "block 2 signal success" << std::endl;
    std::cout << "pid: " << getpid() << std::endl;
    int cnt = 0;
    while (true)
    {
        // 2. 获取并且打印进程的pending位图
        sigset_t pending;
        sigemptyset(&pending);
        n = sigpending(&pending);
        assert(n == 0);
        Print(pending);
        
        cnt++;
        //3.给目标进程发送2号信号
        
        // 4. 解除对2号信号的屏蔽
        if (cnt == 20){
            std::cout << "解除对2号信号的屏蔽" << std::endl;
            n = sigprocmask(SIG_UNBLOCK, &block, &oblock); // 2号信号会被立即递达, 默认处理是终止进程
            assert(n == 0);
        }
        sleep(1);
    }
    
    return 0;
}

🐍signal系统调用(操作handler表)

函数介绍:

  • 功能:将系统对于指定信号的默认处理改为自定义方式处理。

  • 参数:参数一:信号名称/编号 ;参数二:自定义方法的函数指针(返回值void 参数为int)

  • 返回值:返回的是老处理方法。

注意:signal调用完后,不会立即执行handler方法,当收到指定信号的时候才会执行。

示例代码:

cpp 复制代码
// 2号信号的默认处理动作是终止进程
void handler(int sig)  //signo是收到的信号的编号
{
    cout << "receive a signal: " << sig << endl;
}

int main()
{
    signal(2, handler); //signal(SIGINT,handler);
    //signal(2,SIG_IGN); //忽略信号
    while(true)
    {
        cout<<"i am a process,pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

测试结果:


相关推荐
丁卯40414 分钟前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo15 分钟前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
人间打气筒(Ada)2 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231112 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
Moonnnn.3 小时前
51单片机学习——动态数码管显示
笔记·嵌入式硬件·学习·51单片机
落笔画忧愁e3 小时前
FastGPT快速将消息发送至飞书
服务器·数据库·飞书
小冷爱学习!3 小时前
华为动态路由-OSPF-完全末梢区域
服务器·网络·华为
南宫生3 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
技术小齐3 小时前
网络运维学习笔记 016网工初级(HCIA-Datacom与CCNA-EI)PPP点对点协议和PPPoE以太网上的点对点协议(此处只讲华为)
运维·网络·学习
竹言笙熙4 小时前
代码审计初探
学习·web安全