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

其他接口介绍

cpp 复制代码
#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表的变化

cpp 复制代码
//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,但是不建议手动清0
    sigset_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号信号的屏蔽呢? 怎么屏蔽就怎么解屏蔽

cpp 复制代码
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,但是不建议手动清0
    sigset_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不允许信号处理方法进行嵌套:

cpp 复制代码
#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;
}

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

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

cpp 复制代码
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);//对位图进行清0
    sigaddset(&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

cpp 复制代码
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);//对位图进行清0
    sigaddset(&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

cpp 复制代码
#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相反

cpp 复制代码
volatile int flag = 0;

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

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

cpp 复制代码
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()

cpp 复制代码
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)
    {
        //这里直接waitpid
        waitpid(-1, nullptr, 0);//回收任意子进程,阻塞等待
        sleep(2);
        std::cout<<"子进程退出成功"<<std::endl;
        exit(1);
    }
    return 0;
}

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

cpp 复制代码
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出来的⼦进程在终⽌时会⾃动清理掉,不 会产⽣僵⼫进程,也不会通知⽗进程。

cpp 复制代码
signal(SIGCHLD,SIG_IGN);
相关推荐
长流小哥40 分钟前
Linux 深入浅出信号量:从线程到进程的同步与互斥实战指南
linux·c语言·开发语言·bash
派阿喵搞电子1 小时前
Ubuntu 常用命令行指令
linux·ubuntu
库库林_沙琪马2 小时前
Linux 命令全解析:从零开始掌握 Linux 命令行
linux·运维·服务器
嘿rasa2 小时前
2025最新系统 Linux 教程(二)
linux·运维·服务器
浪淘沙jkp2 小时前
AI大模型学习十:‌Ubuntu 22.04.5 调整根目录大小,解决根目录磁盘不够问题
linux·学习·ubuntu
啊吧怪不啊吧2 小时前
Linux常见指令介绍上(入门级)
linux·运维·服务器
菜狗想要变强3 小时前
RVOS-7.实现抢占式多任务
linux·c语言·驱动开发·单片机·嵌入式硬件·risc-v
ALex_zry3 小时前
从源码到实战:深度解析`rsync`增量同步机制与高级应用
linux·网络·运维开发
余辉zmh3 小时前
【Linux系统篇】:从匿名管道到命名管道--如何理解进程通信中的管道?
linux·运维·microsoft
程序设计实验室3 小时前
Traefik,想说爱你不容易:一场动态反向代理的心累之旅
linux·docker·devops·traefik·caddy