【Linux】信号的保存-LMLPHP

与其期盼着你的下一个假期什么时候来临,
不如开始一种你不需要逃避的生活.
-- 塞思·戈丁

1 前言

上一篇文章讲到信号的是怎样产生的:

  1. 通过kill命令:向指定进程发送指定的信号
  2. 键盘可以产生信号:我们常用的ctrl + c (2号信号)和 ctrl + (3号信号)都可以向进程发送信号
  3. 系统调用:该系统调用可以向pid对应的进程,发送sig信号。发送成功返回 1 反之返回 0。
    还有 int raise(int sig); 系统调用是向当前进程发送指定信号。比较简单奥。
    还有 void abort(void); 库函数异常终止当前进程。是对应的6号信号(终止会打印Aborted!)其特殊的性质是可以被捕捉,但是进程还是会被终止掉,就是为了防止发生所有信号都被捕捉,没有信号可以终止的情况,9号信号和19号信号不能被自定义捕捉!!!
  4. 软件条件:我们回忆一下:管道的读端关闭、写端一直进行时 — 系统就会关闭管道(因为该管道无意义)发送13号信号SIGPIPE。也就在软件层面某些条件不满足而产生的信号!着重介绍alarm系统调用。
  5. 异常 :进程非法操作的时候,OS会发送信号!让进程崩溃(默认是终止进程,也可以进行捕捉异常信号。推荐终止进程!)

我们也介绍了core term两种默认操作,core在执行信号后会形成一份core文件(默认是关闭的,因为原本core文件的后缀是pid,运行出错后会创建core文件,导致磁盘空间不足),该文件里存储了出错原因,可以再gdb调试时进行使用。

进程退出会产生status,里面储存退出信息。其中的core dump位记录是否产生core文件。

【Linux】信号的保存-LMLPHP
通过这样的代码可以获取子进程退出信息:

#include <signal.h>
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>
#include <cstdio>

int sum(int start, int end)
{
    int sum = 0;
    for (int i = 0; i < end; i++)
    {
        sum /= i;
        sum += i;
    }
    return sum;
}
int main()
{
    pid_t id = fork();
    
    if(id == 0)
    {
        sleep(1);
        //子进程
        sum(0 , 100);
        exit(0);
    }
    //父进程
    int status = 0;
    pid_t rid = waitpid(id , &status , 0);
    if(rid == id)
    {
        printf("exit code : %d , exit sig : %d , core dump : %d" , (status >> 8) & 0xFF , status & 0x7F , (status >> 7) & 1);
    }

    return 0;
}

这五种方式是信号产生的基本方式,上一篇文章我们初步尝试了使用signal系统调用对信号进行捕捉。今天我们一起来看看信号时如何进行保存。

信号的保存

在认识信号的保存之前,我们先来熟悉几个概念

  1. 实际执行信号的处理动作称为信号递达(Delivery):递达动作: 默认 , 忽略和自定义。
  2. 信号从产生到递达之间的状态,称为信号未决(Pending):因为信号是在合适情况才处理,处理之前就要在进程PCB中进行保存。这时就叫未决状态
  3. 进程可以选择阻塞 (Block )某个信号:阻塞一个信号,对应的信号永远不递达,一致处于未决状态,直到主动解除阻塞。阻塞与未决互不影响!!!
  4. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

阻塞,未决和抵达

接下来我们就来介绍三张表,这三张表就能实现阻塞,未决和抵达:
【Linux】信号的保存-LMLPHP
在进程的内核数据结构中会维护三张表:

  • pending表是通过位图来储存,一共31位 , 每个比特位代表信号编号,比特位的内容代表信号是否收到!

接下来,可以来深入理解一下signal系统调用了, sighandler_t signal(int signum, sighandler_t handler);是对信号进行自定义捕捉,进程收到signum信号后将会执行handler方法。那么为什么它就可以做到自定义捕捉呢???这就和handler表有关系了。

  • hanlder表是函数指针数组。handler表中的下标是信号编号,内容是收到对应信号会执行的方法。 sighandler_t signal(int signum, sighandler_t handler);方法就是将signum信号对应的方法修改为handler,这样就完成了自定义捕捉。

  • block表也是通过位图来储存(和pending一样)。每个比特位代表信号编号,比特位的内容代表信号是否阻塞!

对于这三张表,我们要横着看,对于 1 号信号:是否阻塞-是否收到-执行方法。这样通过两张位图和一张指针数组就对于一个信号可以进行完美识别!

再次注意:

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

对于一个信号要不要进行处理由blockpending表来决定,如何执行由handler表决定!

对信号集的操作

我们认识了内核数据结构中的三张表,那么如果对它们进行操作呢?Linux操作系统为我们提供了用户级别的位图!:

sigset_t
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集

这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略

sigset_t的内部结构类似:

struct bits
{
    uint32_t bits[400]; // 400 * 32个比特位
};

如何读取指定位置的比特位呢?进行运算即可
比如第40位:

i = 40 / (sizeof(uint32_t)*8) ;// -> bits[i]第几个数字中
j = 40 % (sizeof(uint32_t)*8) ;// -> bits[i]:j该数字中的第几位
  • sigset_t是Linux操作系统提供的一个用户级数据类型,禁止用户直接修改位图!!!所以就提供位图操作函数:
#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

除了对位图的操作还有:
系统调用sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集block表)

#include <signal.h>
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

其中参数:

  1. how :一般有三种选择
    【Linux】信号的保存-LMLPHP
  2. set :这里面包含我们想要修改的位置
  3. oldset:输出型参数,会把原来的位图拷贝一份。

block表可以修改了,那pending表怎么进行修改呢?其实pending表不需要我们进行修改,信号产生的5种方式都会对进程的pending表进行修改!我们只需要获取pending表就行

#include <signal.h>
int sigpending(sigset_t *set);//获取当前进程的pending位图

接下来我们来做一个实验:

#include <signal.h>
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>
#include <cstdio>

// 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
//int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

void PrintPending(sigset_t pending)
{
    std::cout << "cur process :" << getpid() << "pending ->";  
    for(int signo = 31 ; signo > 0 ; signo--)
    {
        if(sigismember(&pending , signo))
            std::cout << 1 ;
        else
            std::cout << 0;
    }
    std::cout << std::endl;
    sleep(1);
}


int main()
{
    sigset_t block_set , old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set, 2);//将2号信号存入block_set中
    //更改进程的block()表
    sigprocmask(SIG_BLOCK , &block_set , &old_set);//真正完成修改内核block

    int cnt = 10;
    while (true)
    {
        //获取当前的pending信号集
        sigset_t pending;
        sigpending(&pending);
        //打印pending信号集
        PrintPending(pending);
        cnt--;
        if(cnt == 0)
        {
            sigprocmask(SIG_SETMASK , &old_set , &block_set);
        }
    }
    
    return 0;
}

来看:
【Linux】信号的保存-LMLPHP
我们将 2 号信号进行了阻塞,这样向进程发送2号信号就不会被递达!!!会处在pending未决中!!!

并且我们发现当cnt变为0时解除了对2号信号的阻塞,这时候进程就退出了,因为2号信号解除阻塞后,就会执行2号信号的对应动作 — 终止!!!

  • 解除屏蔽,一般会立刻处理当前被解除的信号(如果处于pending中)
  • pending位图中对应的信号也要被清零!那是递达之前还是递达之后呢? — 递达之后清零(通过自定义捕捉可以验证)

这就是信号保存的方式!通过三张表来做到对信号的操作是十分的巧妙!!!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

06-22 23:17