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

1.信号的产生(软件条件产生信号)
当我们在进行管道通信时,假如读端全部关闭,那么操作系统会将写端也关闭,这就是软件条件产生的信号
不过今天我们要简绍另一种软件条件产生的信号–alarm
alarm基础用法
• 调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终⽌当前进程
• 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个⽐⽅,某⼈要⼩睡⼀觉,设定闹钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡⼀会⼉,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

下面我们来调用一下这个函数(程序的作⽤是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终⽌,必要的时候,对SIGALRM信号进⾏捕捉):
#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操作与网络操作效率上的影响
#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一直发送信号怎么办呢?那么就要使用下面的代码了
#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完成类似操作系统做任务的功能
#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 种触发来源
信号可通过以下场景产生:
无论信号由哪种来源产生,信号的发送必须由操作系统(OS)完成,其底层本质是:修改目标进程在内核中对应的 “信号位图”(比特位),以此记录进程收到了该信号
2.信号的保存
1.为什么要保存
就像拿外卖一样,你不需要立马拿,但是你得知道他送到了,你不能忘了拿,进程也一样,接收到信号后不需要立马执行,但是进程需要记得有这个信号,需要记得之后处理这个信号
2.信号的保存流程

- 信号递送(Delivery):是信号的 “实际处理阶段”—— 当进程执行信号对应的处理动作(比如执行自定义函数、触发默认终止、选择忽略),这个过程被称为 “信号递送”。
- 信号未决(Pending):是信号从 “产生” 到 “递送” 之间的过渡状态 —— 信号已经产生(操作系统已修改进程的信号位图标记该信号),但进程还未执行处理动作,对应图中 “信号在位图中,还没来得及处理” 的描述。
进程可以主动 “阻塞(Block)” 某个信号:
- 被阻塞的信号产生后,会一直保持未决状态(不会执行递送)
- 只有当进程解除对该信号的阻塞,这个未决的信号才会被递送(执行处理动作)
阻塞与忽略是不一样的
- 阻塞:是 “信号递送前的拦截”—— 信号一直停留在未决状态,根本不会进入递送阶段
- 忽略:是 “信号递送后的选择”—— 信号已经完成递送,只是在处理时选择不执行任何操作
信号产生后,会先进入未决状态(保存在进程的信号位图中)
- 若该信号未被阻塞:进程会在安全时机执行信号递送,选择 “自定义处理 / 默认处理 / 忽略” 中的一种动作
- 若该信号被阻塞:会一直保持未决状态,直到阻塞解除,才会触发递送流程
3.底层信号保存的实现

这张图展示了Linux 内核中进程信号管理的核心数据结构—— 进程控制块(task_struct)关联的 “三张表”(block、pending、handler),它们通过位图 / 数组形式,协同实现信号的阻塞、未决、处理逻辑:
1. 核心关联:task_struct与三张表
task_struct是进程的控制块(记录进程所有状态),其中关联了信号管理的三张表,这三张表是内核管理信号的核心载体
2. 三张表的功能与规则
(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)绑定自定义函数)
这三张表的配合,是 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决定修改方式:
| SIG_BLOCK | 新屏蔽字 = mask | set(添加阻塞信号) |
| SIG_UNBLOCK | 新屏蔽字 = mask & ~set(解除指定信号的阻塞) | |
| SIG_SETMASK | 新屏蔽字 = set(直接替换原有屏蔽字) |
2. sigpending:读取未决信号集(pending 表)
sigpending能读取当前进程所有处于 “未决状态” 的信号,通过sigset_t传出
3.用代码展示信号保存的三张表
#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;
}






