Linux进程信号(2)--信号的保存

目录

1.阻塞信号

[1.1 信号其他相关常见概念](#1.1 信号其他相关常见概念)

[1.实际执行信号的处理动作称为信号递达(Delivery)](#1.实际执行信号的处理动作称为信号递达(Delivery))

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

[3.进程可以选择阻塞 (Block )某个信号。](#3.进程可以选择阻塞 (Block )某个信号。)

1.2信号在内核中的表示

sigset_t

信号集操作函数

使用sigprocmask函数修改block表

使用sigpending函数查看pending表


1.阻塞信号

在解释信号的保存时,我们要先了解一下信号中的专有名词及其概念。

1.1****信号其他相关常见概念

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

信号递达,通俗来说就是处理信号。

处理信号的三种方式:

1.信号的忽略

2.信号的默认

3.信号的自定义捕捉

信号的默认

这里我们先编写一个程序:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>

int main()
{
    while(true)
    {
        std::cout<<"getpid():"<<getpid()<<"    running..."<<std::endl;
        sleep(1);
    }
    return 0;
}

运行起来,然后再使用kill -信号编号 进程pid。给进程发送信号。

这里就是信号9的默认。

信号的自定义

这里我们要使用函数:

cpp 复制代码
sighandler_t signal(int sugnum,sighandler_t handler)

这里signum是你想自定义信号的编号,handler一个函数指针,指向的是返回值为void,参数为一个int的函数。(这里也可以用一个宏SIG_DFL恢复信号的默认处理)

作用:收到信号,不按照默认方式,自定义处理信号。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>


void handler(int signal)//自定义处理信号2的内容
{
    std::cout<<"handler:"<<signal<<std::endl;
    exit(2);
}

int main()
{
    signal(2,handler);//自定义信号2.

    while(true)
    {
        std::cout<<"getpid():"<<getpid()<<"    running..."<<std::endl;
        sleep(1);
    }
    return 0;
}

使用宏SIG_DFL恢复默认处理:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>


void handler(int signal)//自定义处理信号2的内容
{
    std::cout<<"handler:"<<signal<<std::endl;
    exit(2);
}

int main()
{
    signal(2,handler);//自定义信号2.

    signal(2,SIG_DFL);//恢复信号2,默认处理

    while(true)
    {
        std::cout<<"getpid():"<<getpid()<<"    running..."<<std::endl;
        sleep(1);
    }
    return 0;
}

信号的忽略

这里还是使用函数signal,但是要使用宏SIG_IGN才能实现函数忽略。

代码:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>


void handler(int signal)//自定义处理信号2的内容
{
    std::cout<<"handler:"<<signal<<std::endl;
    exit(2);
}

int main()
{
    signal(2,SIG_IGN);//忽略信号2.

   
    while(true)
    {
        std::cout<<"getpid():"<<getpid()<<"    running..."<<std::endl;
        sleep(1);
    }
    return 0;
}

这里我们可以看到当我们向进程发送信号2时,它并没有退出进程,所以它忽略了信号2.

这里可能大家会有这样的疑问?

signal函数的第二个参数是一个函数指针,为什么可以使用宏SIG_DFL与SIG_ING呢?

这里我们可以直接看这两个宏的定义:

这里我们可以看出,将0强转成了函数指针类型,1也强转成了函数指针类型。

如何看待信号的忽略?

这里忽略,就是处理信号。

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

这里我们知道,如果当进程收到信号时,正在执行更为重要的事务,进程并不会立马处理信号,而是先将信号保存下来,等待合适的时机再处理。所以在信号产生,到信号处理(信号递达)这段时间里我们叫做信号未决。

这里我们知道,保存信号是使用位图来保存的,信号未决具体点可以理解为:在信号位图中,就叫做信号未决。

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

信号未决之后,暂时不递达,直到接触信号的阻塞。

举例理解:

在上课前老师留下了放假作业(接受信号,保存),但是因为上课要听老师讲课,我们不能在课堂上写作业(被阻塞),我们只能在放学后才能写作业(接触阻塞)。

注:

1.信号未决不一定阻塞,但收到信号并阻塞,一定未决。

2.没有收到信号,可以设置信号的递达动作。

3.没有收到信号,可以设置信号阻塞,也可以解除阻塞。

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

1.2信号在内核中的表示

上面,我们介绍了信号的递达,未决,阻塞。在OS里是这样实现的。
信号在内核中的表示示意图

首先有block,pending,handler三张表,其中:

pending表:是信号未决表,表的下标代表着是几号信号,表的内容代表着是否收到信号。(使用位图的方式记录)
handler表:对应信号的处理方法,里面可以存放函数指针,或者是SIG_DFL/SIG_ING这样的宏。数组的下标代表着信号编号-1。

这里在进程启动时,就形成了这两张表。因此在进程收到信号前,就认识信号(如何处理信号)

block表:信号阻塞表,pending表一样,使用了位图。比特位的位置:表示信号编号。比特位的内容表示,是否对特定的信号进行屏蔽(阻塞)。

对于一个信号的识别,使用这三张表,要横着看

比如以1号信号为列:

当我们收到1号信号时,我们先将pending表中对应的比特位置为1,然后再看block表中对应的比特位的内容,为0,表示不阻塞,在合适的时候,会在handler表中找到对应的方法处理信号。若为1,则表示信号阻塞,不会对信号进行任何处理,等到比特位的内容变为0,会在合适的时候,在handler表中找到对应的方法处理信号。

因此在收到信号时,能不能被处理,首先取决于对因block表中内容。

上面我们知道对于信号的处理取决于PCB中的这三张表,因此如果我们要控制进程对信号的处理,那么就需要改变这三张表的内容,但是对于我们用户来说,我们并不清楚这三张是如何存储。因此操作系统为我们提供了一系列的系统调用函数,来帮助我们。

sigset_t

从上图来看 , 每个信号只有一个 bit 的未决标志 , 非 0 即 1, 不记录该信号产生了多少次 , 阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号 的" 有效 " 或 " 无效 " 状态 , 在阻塞信号集中 " 有效 " 和 " 无效 " 的含义是该信号是否被阻塞 , 而在未决信号集中 " 有效" 和 " 无效 " 的含义是该信号是否处于未决状态。这里将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的 " 屏蔽 " 应该理解为阻塞而不是忽略。
在Linux中是这样定义sigset_t类型的:

信号集操作函数

cpp 复制代码
#include <signal.h>

//将位图里的比特位全部清零
int sigemptyset(sigset_t *set);

//对指定的位图全部置1
int sigfillset(sigset_t *set);

//将指定信号集中的指定比特位置1
int sigaddset (sigset_t *set, int signo);

//将指定信号集中的指定比特位置0
int sigdelset(sigset_t *set, int signo);

//判定一个信号是否在信号集中
int sigismember(const sigset_t *set, int signo);

使用sigprocmask函数修改block表

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

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 

set为想要修改的信号,
oset记录调用前的block,用于恢复原来的block

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

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

代码演示:屏蔽2号信号。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>


void handler(int signal)//自定义处理信号2的内容
{
    std::cout<<"handler:"<<signal<<std::endl;
    exit(2);
}

int main()
{
    signal(2,handler);
    sigset_t block,oblock;
    sigemptyset(&block);//位图置0操作
    sigemptyset(&oblock);

    sigaddset(&block,2);//这里并没有对2号信号做屏蔽

    sigprocmask(SIG_BLOCK,&block,&oblock);//调用系统接口,对2号信号进行屏蔽

    while(true)
    {
        sleep(1);
        std::cout<<"getpid: "<<getpid()<<std::endl;
    }

    return 0;
}

这里可能有人会这样问,既然我们可以控制屏蔽信号,那么如果我们把所有信号都屏蔽了,是不是一旦这样的死循环进程运行起来,是不是就不会被杀掉了?

这里我们直接上代码演示:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>


void handler(int signal)
{
    std::cout<<"handler:"<<signal<<std::endl;
    exit(2);
}

int main()
{
    signal(2,handler);
    sigset_t block,oblock;
    sigemptyset(&block);//位图置0操作
    sigemptyset(&oblock);

    for(int i=1;i<=31;i++)
    sigaddset(&block,i);//将block位图上的所有比特位全部置为1.

    sigprocmask(SIG_BLOCK,&block,&oblock);//对所有信号进行屏蔽

    while(true)
    {
        sleep(1);
        std::cout<<"getpid: "<<getpid()<<std::cout<<"  我已经屏蔽掉了所有信号,你来打我啊!"<<std::endl;
    }

    return 0;
}

这里我们可以发现当我们对进程发送除9之外的信号,进程并不会停止(信号被屏蔽),而当收到9信号时,进程就直接执行信号9对应的默认动作。(19号信号也没有被屏蔽)

9号信号我们成为管理员信号,它并不会被屏蔽。

使用sigpending函数查看pending表

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set)

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程
序如下

这里我们直接上代码:

解释程序目的,不断打印pendling表中比特位的内容。在最开始的前10秒,阻塞信号2。然后将信号2从block表中删除,我们要在10秒前向进程发送2号信号。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>


void handler(int signal)
{
    std::cout<<"handler:"<<signal<<std::endl;
    //exit(2);
}

void PrintPending(sigset_t& pending)//打印pending中每一个比特位的内容
{
    for(int signo=21;signo>=1;signo--)
    {
        if(sigismember(&pending,signo))
        {
            std::cout<<'1';
        }
        else 
        {
            std::cout<<'0';
        }
    }
    std::cout<<std::endl;
}

int main()
{

    signal(2,handler);//自定义2号信号

    //屏蔽2号信号
    sigset_t block,oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigaddset(&block,2);
    sigprocmask(SIG_BLOCK,&block,&oblock);

    std::cout<<"getpid: "<<getpid()<<std::endl;
    
    int cnt=0;

    //让进程不断获取当前进程的pending表
    sigset_t pending;
    while(true)
    {
        cnt++;
        sigpending(&pending);//获取pending表中的内容
        PrintPending(pending);
        if(cnt==10)
        {
            std::cout<<"解除对2号信号的屏蔽,2号信号已递达。"<<std::endl;
            sigprocmask(SIG_SETMASK,&oblock,nullptr);
        }
        sleep(1);
    }
    return 0;
}

这里我们可以看到一旦信号2,不阻塞,就立即递达了。并且pending表中对应的比特位置0.

这里可能有人会问pending表置0这个操作是在处理信号前,还是后执行的?

这里我们可以直接用代码验证:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>

void PrintPending(sigset_t& pending);
void handler(int signal)
{
    std::cout<<"#####################"<<std::endl;
    sigset_t sig;
    sigpending(&sig);
    PrintPending(sig);
    std::cout<<"######################"<<std::endl;
    std::cout<<"handler:"<<signal<<std::endl;
    //exit(2);
}

void PrintPending(sigset_t& pending)//打印pending中每一个比特位的内容
{
    for(int signo=21;signo>=1;signo--)
    {
        if(sigismember(&pending,signo))
        {
            std::cout<<'1';
        }
        else 
        {
            std::cout<<'0';
        }
    }
    std::cout<<std::endl;
}

int main()
{
    signal(2,handler);
    std::cout<<"getpid: "<<getpid()<<std::endl;
    while(true)
    {
        std::cout<<"running..."<<std::endl;
        sleep(1);
    }
        return 0;
}

这里我们采用的是,自定义信号2,在处理时,将pending表的内容打印出来,如果全部位0,则在修改pending实在递达前实行的,否则实在递达之后修改的。

相关推荐
渡我白衣3 分钟前
Linux网络:应用层协议http
linux·网络·http
pofenx16 分钟前
使用nps创建隧道,进行内网穿透
linux·网络·内网穿透·nps
Ronin30517 分钟前
【Linux系统】单例式线程池
linux·服务器·单例模式·线程池·线程安全·死锁
desssq40 分钟前
ubuntu 18.04 泰山派编译报错
linux·运维·ubuntu
Lzc77444 分钟前
Linux的多线程
linux·linux的多线程
清风笑烟语1 小时前
Ubuntu 24.04 搭建k8s 1.33.4
linux·ubuntu·kubernetes
Dovis(誓平步青云)1 小时前
《Linux 基础指令实战:新手入门的命令行操作核心教程(第一篇)》
linux·运维·服务器
好名字更能让你们记住我1 小时前
MYSQL数据库初阶 之 MYSQL用户管理
linux·数据库·sql·mysql·adb·数据库开发·数据库架构
半桔2 小时前
【网络编程】TCP 服务器并发编程:多进程、线程池与守护进程实践
linux·服务器·网络·c++·tcp/ip
维尔切2 小时前
Shell 脚本编程:函数
linux·运维·自动化