🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹:【Linux笔记】——进程信号的产生
🔖流水不争,争的是滔滔不


一、信号的相关概念

  1. 实际执行信号的处理动作称为信号递达(Delivery)。
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)。
  3. 进程可以选择**阻塞(Block)**某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才能执行递达的动作。
  5. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

这些概念都是内核完成的
在这里插入图片描述
如图,block就是阻塞,pending就是未决,handler是信号处理函数就是信号来了以后“干具体事情的地方”,它才是最终决定信号怎么被响应的“执行动作”。

对block和pending进行一个比喻,block可以理解为拦着不让外卖员敲门(不想收外卖时挂个禁止通行的牌子),而pending可以理解为外卖员到了,但是被门外拦着(外卖员在门口堵着)。

简单梳理一下handler触发的整个流程:

  1. 信号来了(内核识别到,不管是硬件还是软件发来的信号)
  2. 判断block:block是一个信号信号阻塞位图,每一位对应一个信号标号1表示阻塞(block了,不让送达)0表示不阻塞(可以送达)。
  3. 检测block位图中,信号对应的那一位,如果是1,则信号是挂到pending位图中,若是0,则理解开始信号处理流程,调用用户注册的handler函数。

注意:
信号来了。不管block了没有,这个信号都会被挂到pending位图中(置为1),也就是说信号来了就记下来了pending代表“来了”。
然后才会去看block位图,如果这个信号在block中被阻塞,信号就会一直挂在pending中,等待解封。如果block没有阻塞,就交个handler执行啦。

还是看上图,pending位图保存收到的信号位图,比特位的位置:表示的是第几个信号,比特位的内容:表示是否收到。block位图表示是否阻塞,比特位的位置:表示的是第几个信号,比特位的内容表示:是否阻塞。

二、信号集操作函数

sigset_t

从上图来看,每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样
表示的。因此, 未决和阻塞标志可以勇相同的数据类型sigset_t来存储, , 这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞, 而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的 这里的“屏蔽”应该理解为阻塞⽽不是忽略。

信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo)

操作信号集(sigset_t) 的一堆函数,干的都是“组装、修改、判断信号集”的活。
这些信号集其实是用户态的临时数据结构,本质上是位图,跟内核里block位图的存储形式类似。

典型函数:
sigemptyset(清空信号集,全是0)
sigfillset(全填1)
sigaddset(添加某个信号)
sigdelset(删除某个信号)
sigismember(判断某信号在不在信号集中)

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

sigprocmask

调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。 这个系统调用才是真正去修改内核中当前进程的 block 位图。
它会用你准备好的信号集(sigset_t)去 “替换、添加、删除” block 位图中对应的位。所有这里要注意上面的信号集操作函数只是工具,你拿着信号集草稿去找 sigprocmask 办手续,sigprocmask 帮你真正去 kernel 里改。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

参数有:
how:表示是“添加(阻塞)”、“删除(解除阻塞)”、“直接替换成新的”。
set:你准备好的信号集。
oldset:可以把修改前的block值保存下来。

在这里插入图片描述
这些都是修改block位图。

sigpending

#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1

不管你爱不爱搭理(阻塞不阻塞),信号一到,pending 位图对应的位置就会被置 1。
不会因为 block、不会因为 handler 正忙,就不记。→ 来一个挂一个, pending 只负责“有信号来了”的事实,不负责“要不要执行”。

那sigpending() 是干嘛的?

查询“当前进程”哪些信号已经挂在 pending 位图上(那些“已经到了但是还没被响应”的信号)。
用于排查信号是否被 block 阻塞住了。
诊断信号处理的状态,是个“监测手段”,不是“处理动作”。

实验

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

// 打印 pending 集
void PrintPending(sigset_t &pending) {
    std::cout << "curr process[" << getpid() << "] pending: ";
    for (int signo = 31; signo >= 1; signo--) {
        if (sigismember(&pending, signo)) {
            std::cout << 1;
        } else {
            std::cout << 0;
        }
    }
    std::cout << "\n";
}

// 信号处理函数
void handler(int signo) {
    std::cout << signo << " 号信号被递达!!!" << std::endl;
    std::cout << "-------------------------------" << std::endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
    std::cout << "-------------------------------" << std::endl;
}

int main() {
    // 0. 捕捉 2 号信号
    signal(SIGINT, handler);  // 自定义捕捉 SIGINT (Ctrl+C)

    // 1. 屏蔽 2 号信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set, SIGINT);

    // 1.1 设置进程的 Block 表
    sigprocmask(SIG_BLOCK, &block_set, &old_set);

    int cnt = 15;
    while (true) {
        // 2. 获取当前进程的 pending 信号集
        sigset_t pending;
        sigpending(&pending);

        // 3. 打印 pending 信号集
        PrintPending(pending);

        cnt--;

        // 4. 解除对 2 号信号的屏蔽
        if (cnt == 0) {
            std::cout << "解除对 2 号信号的屏蔽!!!" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }

        sleep(1);
    }

    return 0;
}

前面 15 秒,Ctrl+C 打了很多次,都不会触发 handler,因为 SIGINT 被 block 了,但 pending 位图记录了。

你看到的:

curr process[448336] pending: 0000000000000000000000000000010

代表第 2 号信号 (SIGINT) 被挂在 pending 里。

cnt==0 时解除阻塞,pending 里的 SIGINT 会被马上递达,调用 handler,打印信号收到。

term 和 core

子进程退出时产生的 core 和 term 现象,本质上也是由信号触发的结果,不过它们不单纯是“信号”本身,而是信号带来的退出状态。

core会在当前路径下,形成一个文件,进程异常退出的时候,进程在内存中的核心数据,会从内存拷贝到磁盘,形成一个文件的核心转储,并且支持debug。然后在进程退出
trem就直接进程退出了。

子进程的 term / core 是“信号导致的退出状态”,本质是信号事件的结果表现。term 代表被终止,core 代表还顺便扔了个尸体(core dump 文件)出来。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐