一、什么是信号
信号在日常中很常见,最常见的就像红绿灯。红绿灯亮起的时候路人行车都会接受到信号,继而采取相对应的措施。
在Linux中,也会有类似于红绿灯这类的信号。当进程捕捉到信号的时候,进程就会采取相应的行动。Linux中有62个信号。
注意32、33号信号没有。
kill指令
kill [-信号编号] 进程PID
kill 指令是用来给进程发信号的,内核负责传递,进程自己处理。
信号的两大类
普通信号(1~31):普通信号是 Linux 早期提供的不可靠信号 ,同一信号多次触发时不排队、只保留一次、可能丢失 ,仅用于通知简单事件,无法携带附加数据。
实时信号(34~64):实时信号是 POSIX 标准定义的可靠信号 ,支持排队、不丢失、按发送顺序递达 ,可通过sigqueue携带附加数据,适用于需要可靠通知的进程间通信。
学习中我们更关注普通信号。
进程收到信号的 3 种反应
执行默认动作:执行信号默认的行为,如现实中车看到红灯需要停下。
忽略信号:忽略该信号,如救护车执行任务时可以忽略红绿灯
自定义处理:捕获信号但不执行默认行为,如街头小丑在红灯时跳舞。
但总的来说,信号的这三种处理方式都是建立在信号被捕获的情况下。信号是外部或硬件发送给进程的一种异步(多事件互不影响,同时出现)的事件(异常、终止......)通知机制(告诉进程发生了什么)。
信号声明周期

对于为什么要将信号保存起来,是因为存在信号发出后不一定需要立刻生效,而是需要满足一些条件才行,这时候就需要将信号保存起来。
二、进程的产生方式
signal函数
signal函数是Linux系统专门用于将自定义函数绑定到一个信号上的。

参数说明:
signum表示被绑定的信号编码(1~31、34~64,但我们通常不处理34~64号信号);
sighandler是一个返回类型为void,参数类型为int的函数指针,其指向绑定的函数。
实例:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void exc(int signum)
{
std::cout << "信号" << signum << "被替换" << std::endl;
}
int main()
{
signal(SIGINT, exc);
while (1)
{
std::cout << getpid() << std::endl;
sleep(1);
}
return 0;
}


我们可以看到该进程的SIGINT(2号)信号被我们替换为自己的函数,并且执行完函数后该进程依旧没有退出。当我们摁下Ctrl+c的时候该进程竟然依旧调用我们自己的函数并没有退出。
这是因为Ctrl+c就是向前台进程发送SIGINT信号,而SIGINT信号的默认作用就是终止进程。
signal函数的返回值
signal() 函数返回值的核心作用是返回「之前」该信号的处理方式,相当于给一个 "操作记录",方便备份、恢复信号的原有行为。其返回类型也是函数指针类型。
void exc(int signum)
{
std::cout << "\n临时处理信号" << signum << ",3秒后恢复默认行为" << std::endl;
}
int main()
{
// 1. 备份SIGINT原来的处理方式
sighandler_t old_handler = signal(SIGINT, exc);
std::cout << "阶段1:按Ctrl+C触发自定义处理..." << std::endl;
sleep(5);
// 2. 恢复SIGINT的原有处理方式(默认终止)
signal(SIGINT, old_handler);
std::cout << "\n阶段2:已恢复默认行为,按Ctrl+C会终止程序" << std::endl;
while (1)
{
sleep(1);
} // 等待测试
return 0;
}

注意事项:
signal函数推荐放在程序开始位置,不要放到循环内部。
while (1)
{
signal(SIGINT, exc);
std::cout << getpid() << std::endl;
sleep(1);
}

signal 函数的作用是修改进程对指定信号的处理方式,这个修改是全局且持久的(只要进程不退出,除非主动改回去)。将signal函数放在循环内部完全没有新作用。
把 signal 放在循环里:第一次按 Ctrl+C → 执行 exc,但系统自动把 SIGINT 重置为 "默认终止";
循环下一次执行 → 又调用 signal(SIGINT, exc),重新注册为自定义处理;
结果:看似 "正常",但每次处理信号后都有一个 "重置 - 重新注册" 的无效循环,且可能在 "重置瞬间" 收到信号导致程序终止。
小巧思
既然我们可以使用signal函数替换信号,而信号又是控制进程的方式,那我们可不可以把所有信号都替换掉,这样一来这个进程不就永远不会退出吗?
for (int i = 1; i <= 32; i++)
{
signal(i, exc);
}


欸,我不是把所有信号都替换掉了吗,怎么进程还是会被9号信号SIGKILL杀死呢。这是因为OS在设计的时候能想到会有人钻这个空子,所以专门设计了一些无法被替换的信号确保安全,如SIGKILL就是典型的例子。
这样的信号有2个,也被称为管理员信号:
| 信号 | 编号 | 名称 | 核心特性 | 用途 |
|---|---|---|---|---|
SIGKILL |
9 | 杀死进程 | 不可捕获、不可忽略、不可替换 | 强制终止任何进程(哪怕进程卡死) |
SIGSTOP |
19 | 暂停进程 | 不可捕获、不可忽略、不可替换 | 强制暂停进程(用fg/bg可恢复) |
信号与进程间的关联
在 PCB(task_struct) 数据结构中,存在专门管理信号的结构。当信号产生时:
-
内核负责将信号发送给目标进程 信号被保存在进程的
task_struct(PCB)中,使用位图(unsigned int sigs) 进行高效管理:- 位图的比特位位置:对应信号编号(1~31)
- 位图的比特位内容 :
1表示进程已收到该信号,0表示未收到 - 只有操作系统内核(OS) 有权修改进程
task_struct中的信号位图 - 无论信号来源是
kill指令、键盘操作还是程序运行错误,最终都由内核向目标进程的信号位图写入标记(将对应比特位置为1) - 向进程 "发送信号" 的本质,就是内核修改目标进程
task_struct中的信号位图
-
进程根据自身 PCB 记录的处理方式处理信号进程对信号的处理方式分为三种:
- 默认处理:由系统定义(如终止进程、生成 core dump)
- 忽略处理:进程收到信号后不执行任何操作
- 自定义处理:进程注册自定义函数,由该函数处理信号
-
信号由进程自身完成处理 信号处理在当前进程的上下文中执行,不会创建新的进程或线程,PID 保持不变。进程只是被内核临时打断,跳转到信号处理函数执行,执行完毕后回到原代码位置继续运行。
通过上面的解释,我们可以总结信号的产生方式分为:kill指令、键盘、程序报错崩溃这三种。
键盘中常见的信号:
| 键盘操作 | 信号名称 | 信号编号 | 作用 |
|---|---|---|---|
| Ctrl + c | SIGINT | 2 | 终止前台进程 |
| Ctrl + \ | SIGQUIT | 3 | 终止进程并生成 core dump |
| Ctrl + z | SIGTSTP | 20 | 暂停 / 挂起前台进程 |
需要注意的是,因为只有前台进程才能从键盘上获取信息,所以键盘产生的信号都只能用于前台进程。
另外,虽然我们都是通过bash输入指令的,但bash本身是能够忽略全部信号的。
核心转储core dumped
core dumped是程序崩溃时,把内存现场保存成一个文件(core 文件)。因为每报错一次就会生成一个core文件(把报错信息存入到磁盘),这在某些场景下会导致core文件大量占用空间导致OS挂掉的情况,故一般情况下云服务器会把这个功能关掉。


这时候发现并没有产生core文件,这就是因为OS把这个功能关了,不过我们可以使用下面指令打开。
ulimit -c unlimited
不过新版本的OS可能会把这个文件藏得比较深,OS并不推荐使用这个文件,日常开发中用的也不多。不过,使用core文件确实能快速定位到出错地方,使用方法如下:
gdb ./可执行程序名 core文件名
# 示例:gdb ./sig core (若core文件是core.12345,就写core.12345)
OS是怎么知道出错,崩溃的

一、CPU 层面出错
- 代码
int a = 10; a /= 0;会被编译成 CPU 能执行的除法指令 (比如div指令)。 - CPU 执行这条指令时,会从寄存器里拿到两个数:被除数
10(存在eax)、除数0(存在ebx),送入算术逻辑单元(ALU) 计算。 - CPU 硬件电路在设计时就规定:除数不能为 0,一旦检测到除数是 0,这个除法操作无法完成,硬件会直接触发一个 "除法错误异常"(硬件级中断)。
- 触发异常后,CPU 会做两件事:
- 在状态寄存器(EFlags) 里标记 "当前发生了除法错误";
- 暂停当前进程的执行,强制切换到内核态 ,把控制权交给操作系统内核。
二、OS 层面:内核捕获并识别异常
- CPU 把控制权交给内核后,内核会先读取 CPU 提供的异常编号,这个编号明确告诉内核:"刚才发生了除零错误"。
- 内核通过当前 CPU 上下文(比如
cr3寄存器,它保存了当前进程页表的物理地址),精准定位到触发这个异常的进程(就是刚才在 CPU 上运行的那个进程)。 - 内核会在这个进程的
task_struct(PCB)里,找到专门管理信号的位图结构 ,把对应SIGFPE(浮点异常信号)的那一位从0改成1------ 这就是 "向进程发送信号" 的本质。
三、进程层面:后续处理信号
- 当这个进程再次被调度到 CPU 上运行时,内核会先检查它的信号位图。
- 发现
SIGFPE对应的位是1,就会让进程执行对应的处理逻辑:- 默认处理:终止进程,输出
Floating point exception (core dumped); - 忽略处理:进程假装没收到这个信号,继续运行(通常不推荐);
- 自定义处理:执行你提前注册好的信号处理函数。
- 默认处理:终止进程,输出
当发生错误时,进程往往锁定的是当前进程,因为前面的进程已经离开,后面的进程还没开始。并且OS中有专门的结构用于指向当前进程,不用担心OS找不到。
使用函数产生信号
kill函数
kill 函数是 Linux 系统编程中用于进程间发送信号的系统调用(和终端的 kill 指令本质是一回事,kill 指令底层就是调用这个函数实现的)。
简单说:终端输入 kill -9 1234 是 "用户层面给进程发信号",而代码里调用 kill() 函数是 "程序层面给进程发信号"。
#include <signal.h>
// 返回值:成功返回0,失败返回-1(并设置errno)
int kill(pid_t pid, int sig);
pid是目标进程PID,sig是所需要执行的信号。
raise函数
raise 函数是 Linux 系统编程中专门用于 "进程给自己发信号" 的函数(可以理解为 kill 函数的 "简化版"------ 不用指定目标 PID,默认发给当前进程)。
简单说:kill(getpid(), sig) 等价于 raise(sig),前者是 "手动指定自己的 PID 发信号",后者是 "直接给自己发信号",底层最终都会调用内核的信号发送逻辑。
#include <signal.h>
// 返回值:成功返回0,失败返回非0(注意:和kill函数返回值规则不同)
int raise(int sig);
abort函数
abort 函数是 Linux 系统编程中专门用于让进程 "主动异常终止" 的函数,它的核心作用是:强制当前进程崩溃退出,并生成 core dump 文件(默认行为)。
简单说:abort() = 进程主动给自己发送无法被捕获 / 忽略的 SIGABRT 信号,最终效果是 "程序崩溃 + 生成 core 文件",常用于程序检测到致命错误(如参数非法、资源初始化失败)时主动终止。
#include <stdlib.h>
// 无参数,无返回值(因为调用后进程必然终止,不会返回)
void abort(void);
由软件产生信号
简单闹钟程序
alarm函数
alarm 函数是 Linux 系统中给当前进程设置 "定时器" 的函数 ------ 你指定一个秒数,内核会在这个时间到后,给当前进程发送 SIGALRM(14 号信号,也叫闹钟信号,默认直接终止进程)。
简单说:alarm(5) = 告诉内核 "5 秒后给我这个进程发一个 SIGALRM 信号",本质是内核层面的定时提醒,常用于实现 "超时控制"(比如让程序执行某操作最多等 5 秒,超时就终止)。
#include <unistd.h>
// 返回值:成功返回"上一个定时器剩余的秒数",失败返回-1(几乎不会失败)
unsigned int alarm(unsigned int seconds);
seconds > 0时设置定时器,seconds 秒后发 SIGALRM; seconds = 0时取消当前所有定时器。
alarm 函数的返回是返回上一个定时器剩余的秒数。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 14号信号处理函数:闹钟响了就打印提醒
void alarm_ring(int sig) {
printf("\n 时间到了\n");
}
int main() {
int sec;
printf("输入定时秒数:");
scanf("%d", &sec);
signal(SIGALRM, alarm_ring); // 注册14号信号处理函数
alarm(sec); // 设置定时器(sec秒后发14号信号)
printf("等待%d秒...\n", sec);
while(1); // 死循环等待信号(不退出)
return 0;
}
在 SIGALRM 信号处理函数里重新调用 alarm(),就能实现 "响铃后自动重置定时器",达到重复提醒的效果。
int alarm_sec;
void alarm_ring(int sig) {
printf("\n 时间到了!%d秒后再次提醒\n", alarm_sec);
alarm(alarm_sec); // 重新设置定时器,实现重复提醒
}