Linux:信号保存下(信号二)

欢迎回来,我们今天继续学习信号知识

书接上文,信号的产生除了上篇博客简绍的几种外,还有一些其他的,我们今天继续来探索

1.信号的产生(软件条件产生信号)

当我们在进行管道通信时,假如读端全部关闭,那么操作系统会将写端也关闭,这就是软件条件产生的信号

不过今天我们要简绍另一种软件条件产生的信号--alarm

alarm基础用法

• 调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终⽌当前进程

• 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个⽐⽅,某⼈要⼩睡⼀觉,设定闹钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡⼀会⼉,于是重新设定闹钟为15分钟之后响,"以前设定的闹钟时间还余下的时间"就是10分钟。如果seconds值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

下面我们来调用一下这个函数(程序的作⽤是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终⽌,必要的时候,对SIGALRM信号进⾏捕捉):

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

void headlerSign(int sign)
{
    std::cout << "获得了一个信号 : " << sign << std::endl;
    exit(13);
}

int main()
{
    for (int i = 1; i < 32; i++)
    {
        signal(i, headlerSign);
    }
    alarm(1);
    int cnt = 0;
    while (true)
    {
        std::cout << "cnt : " << cnt++ << std::endl;
    }
    return 0;
}

可以发现最后返回的是信号14

而14号信号就算SIGALRM

可是你或许有一个疑问,为什么这么慢呢,一秒的时间应该可以执行上亿次这样的操作呀,那是因为IO+网络操作(因为up使用的是云服务器)导致运行次数减少,下面我们可以改一下代码让你直观感受IO操作与网络操作效率上的影响

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

int cnt = 0;

void headlerSign(int sign)
{
    //std::cout << "获得了一个信号 : " << sign << std::endl;
    std::cout << "获得了一个信号 : " << sign << " cnt : " << cnt << std::endl;
    exit(13);
}

int main()
{
    signal(SIGALRM, headlerSign);
    alarm(1);
    while (true)
    {
        cnt++;
    }
    return 0;
}

/*int main()
{
    for (int i = 1; i < 32; i++)
    {
        signal(i, headlerSign);
    }
    alarm(1);
    int cnt = 0;
    while (true)
    {
        std::cout << "cnt : " << cnt++ << std::endl;
    }
    return 0;
}*/

1s5亿多次才是我们所熟悉的速度嘛~~

alarm循环发送信号

那么如果我们想要alarm一直发送信号怎么办呢?那么就要使用下面的代码了

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

void headlerSign(int sign)
{
    std::cout << "获得了一个信号 : " << sign << ", pid : " << getpid() << std::endl;
    alarm(1);
}

int main()
{
    signal(SIGALRM, headlerSign);
    alarm(1);
    while (true)
    {
        pause();
    }
    return 0;
}

使用alarm完成类似操作系统做任务的功能

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

/////////func/////////
void Sched()
{
    std::cout << "我是进程调度" << std::endl;
}
void MemManger()
{
    std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
    std::cout << "我是刷新程序,我在定期刷新内存数据到磁盘" << std::endl;
}
//////////////////////

std::vector<std::function<void()>> v;

void headlerSign(int sign)
{
    std::cout << "/////////////////////" << std::endl;
    for (auto func : v)
    {
        func();
    }
    std::cout << "/////////////////////" << std::endl;
    alarm(1);
}

int main()
{
    v.push_back(Sched);
    v.push_back(MemManger);
    v.push_back(Fflush);
    signal(SIGALRM, headlerSign);
    alarm(1);
    while (true)
    {
        pause();
    }
    return 0;
}

总结:信号的 5 种触发来源

信号可通过以下场景产生:

  1. 键盘操作(如Ctrl+C触发 SIGINT)
  2. 系统调用(如killraise等接口)
  3. 系统命令(如终端执行kill命令)
  4. 硬件异常(如内存越界触发 SIGSEGV)
  5. 软件条件(如管道破裂触发 SIGPIPE)

无论信号由哪种来源产生,信号的发送必须由操作系统(OS)完成,其底层本质是:修改目标进程在内核中对应的 "信号位图"(比特位),以此记录进程收到了该信号

2.信号的保存

1.为什么要保存

就像拿外卖一样,你不需要立马拿,但是你得知道他送到了,你不能忘了拿,进程也一样,接收到信号后不需要立马执行,但是进程需要记得有这个信号,需要记得之后处理这个信号

2.信号的保存流程

  • 信号递送(Delivery):是信号的 "实际处理阶段"------ 当进程执行信号对应的处理动作(比如执行自定义函数、触发默认终止、选择忽略),这个过程被称为 "信号递送"。
  • 信号未决(Pending):是信号从 "产生" 到 "递送" 之间的过渡状态 ------ 信号已经产生(操作系统已修改进程的信号位图标记该信号),但进程还未执行处理动作,对应图中 "信号在位图中,还没来得及处理" 的描述。

进程可以主动 "阻塞(Block)" 某个信号:

  • 被阻塞的信号产生后,会一直保持未决状态(不会执行递送)
  • 只有当进程解除对该信号的阻塞,这个未决的信号才会被递送(执行处理动作)

阻塞与忽略是不一样的

  • 阻塞:是 "信号递送前的拦截"------ 信号一直停留在未决状态,根本不会进入递送阶段
  • 忽略:是 "信号递送后的选择"------ 信号已经完成递送,只是在处理时选择不执行任何操作

信号产生后,会先进入未决状态(保存在进程的信号位图中)

  • 若该信号未被阻塞:进程会在安全时机执行信号递送,选择 "自定义处理 / 默认处理 / 忽略" 中的一种动作
  • 若该信号被阻塞:会一直保持未决状态,直到阻塞解除,才会触发递送流程

3.底层信号保存的实现

这张图展示了Linux 内核中进程信号管理的核心数据结构 ------ 进程控制块(task_struct)关联的 "三张表"(blockpendinghandler),它们通过位图 / 数组形式,协同实现信号的阻塞、未决、处理逻辑:

  1. 核心关联:task_struct与三张表

task_struct是进程的控制块(记录进程所有状态),其中关联了信号管理的三张表,这三张表是内核管理信号的核心载体

  1. 三张表的功能与规则

(1)block表:信号阻塞位图

  • 类型unsigned int(位图结构),每一位对应一个信号(比特位位置 = 信号编号,如第 2 位对应 SIGINT)。
  • 功能 :标记 "是否阻塞该信号"------ 比特位内容为1表示阻塞该信号,0表示不阻塞。
  • 示例 :图中 SIGINT (2) 的block位是1,说明该信号被进程阻塞;SIGHUP (1) 的block位是0,不阻塞。

(2)pending表:信号未决位图

  • 类型unsigned int(位图结构),比特位位置对应信号编号。
  • 功能 :标记 "是否收到信号且未处理"------ 比特位内容为1表示信号已收到但处于 "未决" 状态(未执行处理),0表示无未决信号。
  • 示例 :图中 SIGINT (2) 的pending位是1,说明该信号已被进程接收,但因被block阻塞,暂未执行处理(保持未决)。

(3)handler表:信号处理函数数组

  • 类型sighandler_t handler[31](函数指针数组),数组下标对应信号编号。
  • 功能 :定义信号的处理方式,每个元素对应一种动作:
    • SIG_DFL:执行信号的默认处理(如图中 SIGHUP (1) 的处理方式)
    • SIG_IGN:忽略该信号(如图中 SIGINT (2) 的处理方式)
    • 自定义函数指针:通过signal()系统调用注册的用户自定义处理函数(如图中 SIGALRM 通过signal(SIGALRM, handlerSig)绑定自定义函数)
  1. 信号产生后,内核将pending表中 SIGINT 对应的位设为1(标记未决);
  2. 检查block表:SIGINT 对应的位是1(被阻塞),因此信号保持未决状态;
  3. 当进程解除 SIGINT 的阻塞(block位设为0),内核会读取handler表:SIGINT 对应的处理方式是SIG_IGN,因此执行 "忽略" 动作,同时将pending位设为0(清除未决)。

这三张表的配合,是 Linux 内核实现 "信号阻塞、未决管理、处理方式定义" 的底层逻辑,通过位图 / 数组的轻量化结构,高效完成信号的状态管理

4.sigset_t 与信号集操作

sigset_t 是 Linux 内核为进程信号管理设计的核心数据类型------ 本质是一个位图结构,每一位对应一个信号的 "有效 / 无效" 状态,专门用来存储 "阻塞信号集(信号屏蔽字)" 和 "未决信号集"

从底层逻辑看,sigset_t 就是对 "信号位图" 的封装:

  • 阻塞信号集(信号屏蔽字)sigset_t 中某 bit 为 1 → 对应信号被阻塞,为 0 → 不阻塞;
  • 未决信号集sigset_t 中某 bit 为 1 → 对应信号处于未决状态,为 0 → 无未决信号;
  • 无需关心sigset_t内部存储细节(不同系统实现不同),仅需通过专用函数操作即可。

这 5 个函数是操作sigset_t的 "基础工具",必须先初始化再使用:

函数 功能
sigemptyset(&set) 初始化信号集,所有 bit 清零(无有效信号)
sigfillset(&set) 初始化信号集,所有 bit 置 1(包含系统所有信号)
sigaddset(&set, sig) 向信号集中添加指定信号sig(对应 bit 置 1)
sigdelset(&set, sig) 从信号集中删除指定信号sig(对应 bit 清零)
sigismember(&set, sig) 判断信号sig是否在信号集中(在→1,不在→0,出错→-1)

核心注意:

使用sigset_t必须先初始化sigemptyset/sigfillset),否则信号集状态不确定,操作会出问题

  1. sigprocmask:操作进程的信号屏蔽字(block 表)

sigprocmask是修改 / 读取 "阻塞信号集" 的核心函数,参数how决定修改方式:

how 参数 作用(假设当前屏蔽字为 mask)
SIG_BLOCK 新屏蔽字 = mask set(添加阻塞信号)
SIG_UNBLOCK 新屏蔽字 = mask & ~set(解除指定信号的阻塞)
SIG_SETMASK 新屏蔽字 = set(直接替换原有屏蔽字)
  1. sigpending:读取未决信号集(pending 表)

sigpending能读取当前进程所有处于 "未决状态" 的信号,通过sigset_t传出

3.用代码展示信号保存的三张表

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

void PrintPending(sigset_t& pending)
{
    printf("我是一个进程(%d), pend ing : ", getpid());
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(&pending, i))
        {
            std::cout << "1";
        }
        else 
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}
 
void headlerSign(int sign)
{
    std::cout << "/////////////////////" << std::endl;
    std::cout << "递达" << sign << "号信号" << std::endl;
    sigset_t pending;
    int m = sigpending(&pending);
    PrintPending(pending); //如果是... 0000 0010->先递达再置0, ... 0000 0000->先置0再递达 
    std::cout << "/////////////////////" << std::endl;
}

int main()
{
    signal(SIGINT, headlerSign);
    sigset_t block;
    sigset_t old_block;
    ////////屏蔽2号信号//////// 1
    //初始化
    sigemptyset(&block);
    sigemptyset(&old_block);
    sigaddset(&block, SIGINT); //此时并没有对2号信号进行屏蔽,因为我们只是在我们创造出来的block上进行操作
    int n = sigprocmask(SIG_SETMASK, &block, &old_block); //此时才屏蔽完毕
    //////////////////////////    
    int cnt = 0;
    //////重复获取打印过程///// 4
    while(true)
    {
        //////获取pending信号集合///// 2
        sigset_t pending;
        int m = sigpending(&pending);
        ///恢复对2号信号对block的情况// 5
        if (cnt == 10)
        {
            sigprocmask(SIG_SETMASK, &old_block, nullptr);
            std::cout << "解除对2号信号的屏蔽" << std::endl;
        }
        //////打印pending信号集合///// 3
        PrintPending(pending);
        sleep(1);
        cnt++;
    }
    //////////////////////////
    return 0;
}

好啦,这就是关于信号产生与保存的内容啦,我们下一篇博客将进攻信号捕捉,也就是信号处理,敬请期待啦~~

相关推荐
saoys1 小时前
Opencv 学习笔记:列表筛选(查找满足指定间距的数值)
笔记·opencv·学习
skywalk81631 小时前
阿里云的esc云服务器安装FreeBSD是否支持zfs文件系统
服务器·阿里云·云计算·freebsd
mjhcsp2 小时前
P14977 [USACO26JAN1] Lineup Queries S(题解)
数据结构·c++·算法
夜勤月2 小时前
拒绝线程死锁与调度延迟:深度实战 C++ 内存模型与无锁队列,构建高并发系统级中枢
java·c++·spring
Qiuner2 小时前
软件工程计算机网络WindowService2008 DNS DHCP网络策略和访问策略IIS 相关配置 期末考试实操题操作题windows运维
运维·网络·windows·计算机网络
安科瑞刘鸿鹏172 小时前
电量和碳量如何建立关系?企业能碳管理的关键一步
运维·网络·物联网·安全
小李独爱秋2 小时前
计算机网络经典问题透视:试述资源预留协议RSVP的工作原理?
运维·服务器·网络·网络协议·计算机网络·rsvp
2501_941982052 小时前
企微API外部群自动化:快速建立自己的护城河
运维·自动化·企业微信
独断万古他化2 小时前
Docker 入门前置:容器虚拟化基础之Namespace 空间隔离
linux·docker·容器