Linux进程信号【信号保存】

简介: Linux进程信号【信号保存】

🌇前言

信号从产生到执行,并不会被立即处理,这就意味着需要一种 “方式” 记录信号是否产生,对于 31 个普通信号来说,一个 int 整型就足以表示所有普通信号的产生信息了;信号还有可能被 “阻塞”,对于这种多状态、多结果的事物,操作系统会将其进行描述、组织、管理,这一过程称为 信号保存 阶段

足球场上的计分系统,用于记录球队得分信息


🏙️正文

1、再次认识信号

补充 信号传递 的相关概念

1.1、概念

信号 传递过程:信号产生 -> 信号未决 -> 信号递达

  • 信号产生(Produce):由四种不同的方式发出信号
  • 信号未决(Pending):信号从 产生 到 执行 的中间状态
  • 信号递达(Delivery):进程收到信号后,对信号的处理动作

在这三种过程之前,均有可能出现 信号阻塞 的情况

  • 信号阻塞(Block):使信号传递 “停滞”,无论是否产生,都无法进行处理

信号递达后的三种处理方式:

  1. SIG_DFL 默认处理动作,大多数信号最终都是终止进程
  2. SIG_IGN 忽略动作,即进程收到信号后,不做任何处理动作
  3. handler 用户自定义的信号执行动作

注意:

  • 信号阻塞 是一种手段,可以发生在 信号处理 前的任意时段
  • 信号阻塞 与 忽略动作 不一样,虽然二者的效果差不多:什么都不干,但前者是 干不了,后者则是 不干了,需要注意区分

1.2、感性理解

信号传递 的过程比作 网上购物

可以抽象出以下概念:

  • 信号产生:在某某购物平台上下达了订单
  • 信号未决:订单下达后,快递的运输过程
  • 信号递达:快递到达驿站后,你对于快递的处理动作
  • 信号阻塞:快递运输过程中堵车了

只要你下单了,你的手机上肯定会有 物流信息(未决信息已记录),当 快递送达后(信号递达)物流记录 不再更新

堵车 是一件不可预料的事情,也就是说:在下单后,快递可能一会儿送达(没有阻塞),可能五天送达(阻塞 -> 解除阻塞),有可能永不送达,因为快递可能永远堵车(阻塞)

堵车也有可能在你下单前发生(信号产生前阻塞)

至于 信号递达后的处理动作 如何理解呢?

  1. 快递送达后,正常拆快递(默认动作)
  2. 快递送达后,啥也不干,就是玩(忽略)
  3. 快递送达后,直接把快递退回去(用户自定义)

当然,用户自定义的情况可以有很多种,也有可能是直接把快递扔了

综上,网购的整个过程可以看作 信号传递过程,本文探讨的是 信号保存阶段,即 物流信息

1.3、在内核中的表示

对于传递中的信号来说,需要存在三种状态表达:

  1. 信号是否阻塞
  2. 信号是否未决
  3. 信号递达时的执行动作

在内核中,每个进程都需要维护这三张与信号状态有关的表:block 表、pending 表、handler

所谓的 block 表 和 pending 其实就是 位图结构

一个 整型 int 就可以表示 31 个普通信号(实时信号这里不讨论)

  • 比如 1 号信号就是位图中的 0 位置处,0 表示 未被阻塞/未产生未决,1 则表示 阻塞/未决
  • 对于信号的状态修改,其实就是修改 位图 中对应位置的值(0/1
  • 对于多次产生的信号,只会记录一次信息(实时信号则会将冗余的信号通过队列组织)

如何记录信号已产生 -> 未决表中对应比特位置置为 1

  • 假设已经获取到了信号的 pending
  • 只需要进行位运算即可:pending |= (1 << (signo - 1))
  • 其中的 signo 表示信号编号,-1 是因为信号编号从 1 开始,需要进行偏移

如果想要取消 未决 状态也很简单:pending &= (~(1 << (signo - 1)))

至于 阻塞 block,与 pending 一模一样

对于上图的解读:

  1. SIGHUP 信号未被阻塞,未产生,一旦产生了该信号,pending 表对应的位置置为 1,当信号递达后,执行动作为默认
  2. SIGINT 信号被阻塞,已产生,pending 表中有记录,此时信号处于阻塞状态,无法递达,一旦解除阻塞状态,信号递达后,执行动作为忽略该信号
  3. SIGQUIT 信号被阻塞,未产生,即使产生了,也无法递达,除非解除阻塞状态,执行动作为自定义

阻塞 block 与 未决 pending 之间并没很强的关联性,阻塞不过是信号未决的延缓剂

  • 信号在 产生 之前,可以将其 阻塞,信号在 产生 之后(未决),依然可以将其 阻塞

至于 handler 表是一个 函数指针表,格式为:返回值为空,参数为 int 的函数

可以看看 默认动作 SIG_DEL忽略动作 SIG_IGN 的定义

/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);
/* Fake signal functions.  */
#define SIG_ERR ((__sighandler_t) -1)   /* Error return.  */
#define SIG_DFL ((__sighandler_t) 0)    /* Default action.  */
#define SIG_IGN ((__sighandler_t) 1)    /* Ignore signal.  */


默认动作就是将 0 强转为函数指针类型,忽略动作则是将 1 强转为函数指针类型,分别对应 handler 函数指针数组表中的 01 下标位置;除此之外,还有一个 错误 SIG_ERR 表示执行动作为 出错

简单对这三张表作一个总结,task_struct 中存在:

  • block 表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 对应信号被阻塞
  • pending 表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 收到该信号
  • handler 表(函数指针数组)该数组的下标,表示信号编号;数组的特定下标的内容,表示该信号递达后的执行动作

1.4、sigset_t 信号集

无论是 block 还是 pending,都是一个位图结构,依靠 除、余 完成操作,为了确保不同平台中位图操作的兼容性,将信号操作所需要的 位图 结构封装成了一个结构体类型,其中是一个 无符号长整型数组

/* A `sigset_t' has a bit for each signal.  */
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;
#endif


注:_SIGSET_NWORDS 大小为 32,所以这是一个可以包含 32 个 无符号长整型 的数组,而每个 无符号长整型 大小为 4 字节,即 32 比特,至多可以使用 1024 个比特位

sigset_t 是信号集,其中既可以表示 block 表信息,也可以表示 pending 表信息,可以通过信号集操作函数进行获取对应的信号集信息;信号集 的主要功能是表示每个信号的 “有效” 或 “无效” 状态

block 表 通过信号集称为 阻塞信号集或信号屏蔽字(屏蔽表示阻塞),pending 表 通过信号集中称为 未决信号集

如何根据 sigset_t 位图结构进行比特位的操作?

  • 假设现在要获取第 127 个比特位
  • 首先定位数组下标(对哪个数组操作):127 / (8 * sizeof (unsigned long int)) = 3
  • 求余获取比特位(对哪个比特位操作):127 % (8 * sizeof (unsigned long int)) = 31
  • 对比特位进行操作即可
  • 假设待操作对象为 XXX
  • 1XXX._val[3] |= (1 << 31)
  • 0XXX._val[3] &= (~(1 << 31))

所以可以仅凭 sigset_t 信号集,对 1024 个比特位进行任意操作,关于 位图 结构的实现后续介绍


2、信号集操作函数

对于 信号产生或阻塞 其实就是对 blockpending 两张表的 增删改查

2.1、增删改查

对于 位图增删改查 是这样操作的:

  • 增:| 操作,将比特位置为 1
  • 删:& 操作,将比特位置为 0
  • 改:|& 操作,灵活变动
  • 查:判断指定比特位是否为 1 即可

比特作为基本单位,不推荐让我们直接进行操作,操作系统也不同意,于是提供了一批 系统接口,用于对 信号集 进行操作

#include <signal.h>
int sigemptyset(sigset_t *set); //初始化信号集
int sigfillset(sigset_t *set);  //初识化信号集
int sigaddset(sigset_t *set, int signum); //增
int sigdelset(sigset_t *set, int signum); //删
int sigismember(const sigset_t *set, int signum); //查  


这些函数都是 成功返回 0,失败返回 -1

至于参数,非常简单,无非就是 待操作的信号集变量、待操作的比特位

注意:在创建 信号集 sigset_t 类型后,需要使用 sigemptysetsigfillset 函数进行初始化,确保 信号集 是合法可用的

2.2、sigprocmask

sigprocmask 函数可用用来对 block 进行操作

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


返回值:成功返回 0,失败返回 -1 并将错误码设置

参数1:对 屏蔽信号集 的操作

  • SIG_BLOCK 希望添加至当前进程 block 表 中阻塞信号,从 set 信号集中获取,相当于 mask |= set
  • SIG_UNBLOCK 解除阻塞状态,也是从 set 信号集中获取,相当于 mask &= (~set)
  • SIG_SETMASK 设置当前进程的 block 表为 set 信号集中的 block 表,相当于 mask = set

参数2:就是一个信号集,主要从此信号集中获取屏蔽信号信息

参数3:也是一个信号集,保存进程中原来的 block 表(相当于给你操作后,反悔的机会)

这个函数就是 参数 1 比较有讲究,主打的就是一个 set 信号集 中获取阻塞信号相关信息,然后对进程中的 block 表进行操作,并且有三种不同的操作方式

演示程序1:2 号信号阻塞,尝试通过 键盘键入 发出 2 信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
    //创建信号集
    sigset_t set, oset;
    //初始化信号集
    sigemptyset(&set);
    sigemptyset(&oset);
    //阻塞2号信号
    sigaddset(&set, 2); //2 号信号被记录
    //设置当前进程的 block 表
    sigprocmask(SIG_BLOCK, &set, &oset);
    //死循环
    while(true)
    {
        cout << "我是一个进程,我正在运行" << endl;
        sleep(1);
    }
    return 0;
}


显然,当 2 号信号被阻塞后,是 无法被递达 的,进程也就无法终止了

演示程序2:在程序运行五秒后,解除阻塞状态

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
    // 创建信号集
    sigset_t set, oset;
    // 初始化信号集
    sigemptyset(&set);
    sigemptyset(&oset);
    // 阻塞2号信号
    sigaddset(&set, 2); //2 号信号被记录
    // 设置当前进程的 屏蔽信号集
    sigprocmask(SIG_BLOCK, &set, &oset);
    // 死循环
    int n = 0;
    while (true)
    {
        if (n == 5)
        {
            // 采用 SIG_SETMASK 的方式,覆盖进程的 block 表
            sigprocmask(SIG_SETMASK, &oset, nullptr); // 不接收进程的 block 表
        }
        cout << "我是一个进程,我正在运行" << endl;
        n++;
        sleep(1);
    }
    return 0;
}


现象:2 号信号发出、程序运行五秒解除阻塞后,信号才被递达,进程被终止

如何证明信号已递达?

  • n == 5 时,解除阻塞状态,程序立马结束
  • 并只打印了 五条 语句,证明在第六秒时,程序就被终止了
  • 至于如何进一步证明,需要借助 未决信号表

2.3、sigpending

这个函数很简单,获取当前进程中的 未决信号集

#include <signal.h>
int sigpending(sigset_t *set);

返回值:成功返回 0,失败返回 -1 并将错误码设置

参数:待获取的 未决信号集

如何根据 未决信号集 打印 pending

  • 使用函数 sigismember 判断当前信号集中是否存在该信号,如果存在,输出 1,否则输出 0
  • 如此重复,将 31 个信号全部判断打印输出即可

所以可以将上面的 演示程序 修改下:

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;
static void DisplayPending(const sigset_t pending)
{
    //打印 pending 表
    cout << "当前进程的 pending 表为: ";
    int i = 1;
    while(i < 32)
    {
        if(sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
        i++;
    }
    cout << endl;
}
int main()
{
    // 创建信号集
    sigset_t set, oset;
    // 初始化信号集
    sigemptyset(&set);
    sigemptyset(&oset);
    // 阻塞2号信号
    sigaddset(&set, 2); //记录 2 号信号
    // 设置当前进程的 屏蔽信号集
    sigprocmask(SIG_BLOCK, &set, &oset);
    // 死循环
    int n = 0;
    while (true)
    {
        if (n == 5)
        {
            // 采用 SIG_SETMASK 的方式,覆盖进程的 block 表
            sigprocmask(SIG_SETMASK, &oset, nullptr);   // 不接收进程的 block 表
        }
        //获取进程的 未决信号集
        sigset_t pending;
        sigemptyset(&pending);
        int ret = sigpending(&pending);
        assert(ret == 0);
        (void)ret;    //欺骗编译器,避免 release 模式中出错
        DisplayPending(pending);
        n++;
        sleep(1);
    }
    return 0;
}


结果:2 号信号发出后,当前进程的 pending 表中的 2 号信号位被置为 1,表示该信号属于 未决 状态,并且在五秒之后,阻塞结束,信号递达,进程终止

疑问:当阻塞解除后,信号递达,应该看见 pending 表中对应位置的值由 1 变为 0,但为什么没有看到?

  • 很简单,因为当前 2 号信号的执行动作为终止进程,进程都终止了,当然看不到
  • 解决方法:给 2 号信号先注册一个自定义动作(别急着退出进程)

所以改进后的代码如下:

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;
static void handler(int signo)
{
    cout << signo << " 号信号确实递达了" << endl;
    //最终不退出进程
}
static void DisplayPending(const sigset_t pending)
{
    // 打印 pending 表
    cout << "当前进程的 pending 表为: ";
    int i = 1;
    while (i < 32)
    {
        if (sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
        i++;
    }
    cout << endl;
}
int main()
{
    // 更改 2 号信号的执行动作
    signal(2, handler);
    // 创建信号集
    sigset_t set, oset;
    // 初始化信号集
    sigemptyset(&set);
    sigemptyset(&oset);
    // 阻塞2号信号
    sigaddset(&set, 2); //记录 2 号信号
    // 设置当前进程的 屏蔽信号集
    sigprocmask(SIG_BLOCK, &set, &oset);
    // 死循环
    int n = 0;
    while (true)
    {
        if (n == 5)
        {
            // 采用 SIG_SETMASK 的方式,覆盖进程的 block 表
            sigprocmask(SIG_SETMASK, &oset, nullptr); // 不接收进程的 block 表
        }
        // 获取进程的 未决信号集
        sigset_t pending;
        sigemptyset(&pending);
        int ret = sigpending(&pending);
        assert(ret == 0);
        (void)ret; // 欺骗编译器,避免 release 模式中出错
        DisplayPending(pending);
        n++;
        sleep(1);
    }
    return 0;
}


显然,这就是我们想要的最终结果

先将信号 阻塞,信号发出后,无法 递达,始终属于 未决 状态,当阻塞解除后,信号可以 递达,信号处理之后,未决 表中不再保存信号相关信息,因为已经处理了

综上,信号在发出后,在处理前,都是保存在 未决表 中的

注意:

  • 针对信号的 增删改查 都需要通过 系统调用 来完成,不能擅自使用位运算
  • sigprocmasksigpending 这两个函数的参数都是 信号集,前者是 屏蔽信号集,后者是 未决信号集
  • 在对 信号集 进行增删改查前,一定要先初始化
  • 信号在被解除 阻塞状态 后,很快就会 递达 了
  • 关于信号何时递达、以及递达后的处理动作,在下一篇文章中揭晓

以上关于 信号、信号集 的操作都是在进程中进行的,不影响操作系统


🌆总结

以上就是本次关于 Linux进程信号【信号保存】的全部内容了,在本文中,我们首先再一次对信号有了较深的理解,知道了在内核中存在三张表记录信号的处理流程,然后我们学习了信号集的操作函数,模拟实现了 阻塞信号 - 产生信号 - 未决信号 - 解除阻塞 - 递达信号 的全过程,最终证明 信号在产生之后是保存在 未决表 中的


相关文章推荐

Linux进程信号 ===== :>

【信号产生】

Linux进程间通信 ===== :>

【消息队列、信号量】【共享内存】【命名管道】【匿名管道】

Linux基础IO ===== :>

【软硬链接与动静态库】【深入理解文件系统】【模拟实现C语言文件流】【重定向及缓冲区理解】【文件理解与操作】

Linux进程控制 ===== :>

【简易版bash】【进程程序替换】【创建、终止、等待】

Linux进程学习 ===== :>

【进程地址】【环境变量】【进程状态】【基本认知】

Linux基础 ===== :>

【gdb】【git】【gcc/g++】【vim】Linux 权限理解和学习听说Linux基础指令很多?这里都帮你总结好了

目录
相关文章
|
8月前
|
安全 Linux
【Linux】阻塞信号|信号原理
本教程从信号的基本概念入手,逐步讲解了阻塞信号的实现方法及其应用场景。通过对这些技术的掌握,您可以更好地控制进程在处理信号时的行为,确保应用程序在复杂的多任务环境中正常运行。
304 84
|
7月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
284 67
|
6月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
200 16
|
6月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
141 20
|
5月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
123 0
|
5月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
181 0
|
5月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
126 0
|
5月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
159 0
|
8月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
324 4
|
8月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。