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

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 种触发来源
信号可通过以下场景产生:
- 键盘操作(如
Ctrl+C触发 SIGINT) - 系统调用(如
kill、raise等接口) - 系统命令(如终端执行
kill命令) - 硬件异常(如内存越界触发 SIGSEGV)
- 软件条件(如管道破裂触发 SIGPIPE)
无论信号由哪种来源产生,信号的发送必须由操作系统(OS)完成,其底层本质是:修改目标进程在内核中对应的 "信号位图"(比特位),以此记录进程收到了该信号
2.信号的保存
1.为什么要保存
就像拿外卖一样,你不需要立马拿,但是你得知道他送到了,你不能忘了拿,进程也一样,接收到信号后不需要立马执行,但是进程需要记得有这个信号,需要记得之后处理这个信号
2.信号的保存流程

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

这张图展示了Linux 内核中进程信号管理的核心数据结构 ------ 进程控制块(task_struct)关联的 "三张表"(block、pending、handler),它们通过位图 / 数组形式,协同实现信号的阻塞、未决、处理逻辑:
- 核心关联:
task_struct与三张表
task_struct是进程的控制块(记录进程所有状态),其中关联了信号管理的三张表,这三张表是内核管理信号的核心载体
- 三张表的功能与规则
(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)绑定自定义函数)
- 信号产生后,内核将
pending表中 SIGINT 对应的位设为1(标记未决); - 检查
block表:SIGINT 对应的位是1(被阻塞),因此信号保持未决状态; - 当进程解除 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),否则信号集状态不确定,操作会出问题
- sigprocmask:操作进程的信号屏蔽字(block 表)
sigprocmask是修改 / 读取 "阻塞信号集" 的核心函数,参数how决定修改方式:
| how 参数 | 作用(假设当前屏蔽字为 mask) | |
|---|---|---|
SIG_BLOCK |
新屏蔽字 = mask | set(添加阻塞信号) |
SIG_UNBLOCK |
新屏蔽字 = mask & ~set(解除指定信号的阻塞) | |
SIG_SETMASK |
新屏蔽字 = set(直接替换原有屏蔽字) |
- 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;
}