目录
[1. 生活中的信号](#1. 生活中的信号)
[2. 技术中的信号](#2. 技术中的信号)
[1. 信号定义](#1. 信号定义)
[2. 信号处理方式](#2. 信号处理方式)
[3. 常见信号](#3. 常见信号)
[1. 终端信号](#1. 终端信号)
[2. kill 命令](#2. kill 命令)
[3. kill 系统调用](#3. kill 系统调用)
[4. raise / abort](#4. raise / abort)
[5. alarm 机制](#5. alarm 机制)
[6. alarm 使用](#6. alarm 使用)
[7. 硬件异常信号](#7. 硬件异常信号)
[1. 信号递达 / 阻塞](#1. 信号递达 / 阻塞)
[2. pending 与 block](#2. pending 与 block)
[五、核心转储(Core Dump)](#五、核心转储(Core Dump))
[1. 标志位](#1. 标志位)
[2. 如何开启核心转储(ulimit)](#2. 如何开启核心转储(ulimit))
一、什么是信号
在深入 Linux 内核源码之前,首先需要明确:信号并非计算机体系所独有。人类社会能够维持高效运转,本身也依赖一套完备的信号传递与响应机制
1. 生活中的信号
什么是信号?简单来说,信号就是一种事件通知。它告诉你 "某件事发生了",而你根据预设的逻辑去做出反应。
-
红绿灯: 当你在过马路时,看到红灯亮起。你并不需要翻阅手册来思考这是什么意思。你的大脑中已经内置 了识别红灯的能力------红灯亮即代表停止
-
清晨的闹钟: 昨晚你设定了 7:00 的闹钟。当铃声响起时,它向你发送了一个起床信号
-
手机弹窗: 你正在全神贯注地刷短视频,手机上方弹出一个外卖送达的通知。这个通知就是信号,它打断了你当下的行为(或者被你暂时记在心里)

为了更透彻地理解进程信号,我们可以借用一个经典的快递模型:
-
内置识别能力: 哪怕快递还没发货,你也知道快递来了要签收/拆开。对于进程而言,识别信号是内置的特性,这是内核程序员在设计进程时就写好的逻辑
-
异步性: 你永远无法准确预判快递员什么时候给你打电话。信号的产生对于进程来说也是随机的,它可能在你执行任何一行代码时突然降临
-
信号的保存: 快递员给你打电话时,你可能正在打一把紧张的排位赛,无法立刻下楼。这时候你并不会忘记快递的事,而是记住了快递已经到了。在进程中,这叫信号的保存在位图中
-
处理的时机: 你会在游戏结束这个合适的时候 去取快递。进程收到信号后,也并不一定会立即处理,而是在从内核态切换回用户态的合适时机去执行
-
三种处理方式:
-
执行默认动作: 正常拆快递,使用商品(对应进程的默认处理行为)
-
执行自定义动作: 快递是给女朋友买的零食,你要转送出去(对应进程的信号捕捉)
-
忽略: 快递拿上来后看都不看扔在床头,继续打游戏(对应进程的忽略行为)
-

2. 技术中的信号
回到计算机当中。运行一个死循环程序(比如 while(1))时,终端就像被锁死了一样。此时当我们按下 Ctrl+C,程序瞬间退出
这背后发生了什么?
-
当我们按下 Ctrl+C,硬件中断被触发,键盘驱动程序识别到了组合键
-
操作系统将这个动作解释为 2 号信号(SIGINT)
-
内核将该信号发送给当前进程
-
进程收到信号后,由于默认动作是终止进程,于是死循环程序就此退出
核心结论: 信号 = 事件通知。 它不需要你时刻轮询检查,而是在事件发生时,由操作系统强行介入并通知进程
二、信号基本概念
在 Linux 系统中,信号是进程间通信机制中唯一的异步通信机制。理解信号,需要从它的定义以及进程对信号的响应逻辑入手
1. 信号定义
信号是 Linux 系统响应某些状况而产生的软件中断。它是一种向进程传递异步事件通知的机制
-
异步性: 信号可以在进程执行过程中的任何位置产生,进程无法预知信号到来的精确时刻
-
软件中断: 信号模拟了硬件中断的逻辑。当信号发生时,内核会暂时中断进程的正常执行流,转去执行相应的处理程序,处理完毕后再返回断点继续执行
2. 信号处理方式
在讨论进程如何响应信号之前,我们需要了解如何通过系统调用来改变进程对特定信号的处理方式
(1) signal 系统调用
signal函数是 Linux 信号处理中最基础的接口,用于在进程中注册信号的处理动作
原型:
cpp
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:设置的信号编号(如 SIGINT)
handler:这是一个回调函数指针,或者使用内核预定义的两个宏:
-SIG_DFL:执行信号的默认处理动作
-SIG_IGN:忽略该信号
(2) 三种处理方式
进程对信号的处理方式可以分为以下三种方式:
-
执行默认动作: 执行信号在内核中预定义的处理动作
cppvoid handler(int signumber) { std::cout << getpid() << "收到信号: " << signumber << std::endl; } int main() { std::cout << getpid() << std::endl; signal(SIGINT /*2号信号*/, SIG_DFL); while(true) { std::cout << "waiting signal!" << std::endl; sleep(1); } return 0; } -
忽略信号: 通过将处理函数设置为 SIG_IGN,进程在接收到该信号后会直接将其丢弃,不做任何响应
cppint main() { std::cout << getpid() << std::endl; signal(SIGINT, SIG_IGN); // 设置忽略信号的宏 while(true) { std::cout << "waiting signal!" << std::endl; sleep(1); } return 0; }注意: **SIGKILL (9) 和 SIGSTOP (19)**这两个信号是无法被忽略或拦截的,它们保证了系统管理员对失控进程的绝对控制权
-
捕捉信号: 这是一种自定义处理方式。通过 signal 函数为某个信号注册一个回调函数。 当信号递达时,内核会强行打断用户的主执行流,自动跳转到对应的回调函数中执行。执行完毕后,再通过特定的系统调用返回到被打断的位置
cppvoid handler(int signumber) { std::cout << getpid() << "收到了一个信号: " << signumber << std::endl; } int main() { std::cout << getpid() << std::endl; signal(SIGINT, handler); while(true) { std::cout << "waiting signal!" << std::endl; sleep(1); } return 0; }执行结果:

3. 常见信号
在 Linux 终端执行 kill -l 命令,可以查看系统支持的所有信号

每个信号都有一个编号和一个宏定义名称,这些宏定义路径如下:
-
include/asm-i386/signal.h(32位系统)
-
include/asm-x86_64/signal.h(64位系统)

1-31 号信号被称为标准信号 (不可靠信号),它们是传统 Unix 系统定义的信号;34-64 号则属于实时信号(可靠信号),在高级应用中用于确保信号不丢失
三、信号的产生
信号不会凭空产生,它们通常由特定的事件触发。在 Linux 中,信号的产生路径多样,既有用户手动干预,也有内核根据运行状态自动触发
1. 终端信号
这是用户最直观的信号产生方式。当我们在终端按下特定的组合键时,内核会将这些硬件动作解释为特定的信号并发送给当前的前台进程
-
Ctrl+C (SIGINT):
-
动作:发送 2 号信号,默认终止进程。
-
实例 :你在终端运行一个死循环脚本按下 Ctrl+C 后,脚本立即停止

-
-
Ctrl+\ (SIGQUIT):
-
动作 :发送 3 号信号,默认终止进程并产生 Core Dump
-
实例:相比于 SIGINT,SIGQUIT 的动作更重,它不仅杀掉进程,还会试图把内存镜像保存下来供程序员分析
-
2. kill 命令
kill 命令是系统管理员管理进程的利器。虽然名称中带有 kill,但它实际上是向进程发送特定信号的工具
-
基本用法:
bashkill -[信号编号/名称] [PID] -
实例 :运行一个 sleep 进程并杀掉该进程


3. kill 系统调用
在程序代码中,我们可以使用 kill 系统调用,让一个进程向另一个指定进程发送信号
函数原型:
cpp
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error,
-1 is returned, and errno is set appropriately
实例代码: 我们可以编写一个 mykill 程序,接收 PID 作为参数并杀掉对方:
cpp
// mykill.c
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
if (argc != 3) {
printf("Usage: %s [PID] [SIGNAL]\n", argv[0]);
return 1;
}
// 向 argv[1] 指定的进程发送 argv[2] 指定的信号
if (kill(atoi(argv[1]), atoi(argv[2])) != 0)
perror("kill");
return 0;
}
4. raise / abort
(1) raise 函数
功能:向当前进程(自己)发送指定的信号。其效果等同于 kill(getpid(), sig)
函数原型:
cpp
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
代码示例:
cpp
void handler(int signumber)
{
std::cout << "获取了一个信号: " << signumber << std::endl;
}
int main()
{
signal(2, handler); // 先对2号信号进行捕捉
raise(2);
return 0;
}

(2) abort 函数
功能 :向当前进程发送**SIGABRT (6)**信号,导致进程异常终止
函数原型:
cpp
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
// 就像exit函数一样,abort函数总是会成功的,所以没有返回值
代码示例:
cpp
void handler(int signumber)
{
std::cout << "获取了一个信号: " << signumber << std::endl;
}
int main()
{
signal(SIGABRT, handler);
abort();
return 0;
}

5. alarm 机制
alarm 函数允许进程在指定的秒数后接收到一个**SIGALRM (14)**信号。这在处理超时逻辑或周期性任务时非常有用
函数原型
cpp
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
RETURN VALUE
alarm() returns the number of seconds remaining until any previously
scheduled alarm was due to be delivered, or zero if there was no
previously scheduled alarm.
系统闹钟,其实本质是 OS 必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术
Linux 提供了定时机制,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是

6. alarm 使用
I/O 效率实战对比
为什么说 I/O 是昂贵的?我们可以通过一个 1 秒钟的定时器来直观感受:在 1 秒钟内,单纯累加数字和边累加边打印,其效率差别可达万倍以上
实验 1:有 I/O 输出
cpp
int count = 0;
void handler(int sig) {
printf("final count: %d\n", count); // 1秒后执行
exit(0);
}
int main() {
signal(SIGALRM, handler);
alarm(1); // 设定1秒闹钟
while(1) {
printf("count: %d\n", count++); // 频繁进行 I/O 访问
}
}
由于每次 printf 都涉及用户态到内核态的切换以及硬件驱动,最终 count 可能只累加到几万

实验 2:无 I/O 输出
cpp
// ... 其他逻辑同上 ...
int main() {
signal(SIGALRM, handler);
alarm(1);
while(1) { count++; } // 纯 CPU 计算
}
现象:count 瞬间飙升到了亿级

结论 :网络和终端 I/O 的速度远低于 CPU 和内存的处理速度
重复闹钟
alarm 默认是只生效一次。如果想实现类似每隔 1 秒执行一次的周期任务,我们需要在信号处理函数中再次设定闹钟
cpp
void timer_handler(int sig) {
printf("periodic task triggered!\n");
alarm(1); // 重新设定,实现循环
}
int main() {
signal(SIGALRM, timer_handler);
alarm(1); // 开启第一次闹钟
while(1) pause(); // 挂起等待信号
}
7. 硬件异常信号
有些信号并不是由代码显式发送的,而是因为进程执行了非法指令,导致硬件(CPU/MMU)出现异常,内核捕获后代为转发给进程
(1) 除 0 异常 (SIGFPE)
当 CPU 执行除法指令时,如果除数为 0,算术逻辑单元内部的状态寄存器会触发溢出异常
-
内核动作 :内核检测到 CPU 报错,识别出是当前进程所为,于是向其发送 8 号信号 SIGFPE
-
误区:很多人以为死循环打印除 0 是因为代码没退出,实际上是因为信号处理完返回后,CPU 依然停留在出错的那条指令上,导致重复报错
(2) 野指针/越界访问 (SIGSEGV)
当你试图访问一个不属于你的地址或向只读内存写入时:
-
MMU(内存管理单元) 会发现页表映射无效或权限不对
-
MMU 产生异常,内核捕获该异常
-
内核定位到引起问题的进程,发送 11 号信号 SIGSEGV(段错误)
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的
四、信号递达与阻塞
信号从产生到执行动作,并不是瞬间完成的。在内核中,信号要经历一个从 "产生" 到 "保存" 再到 "执行" 的过程。理解这个过程,需要厘清三个核心概念:递达、未决与阻塞

1. 信号递达 / 阻塞
信号递达(Delivery)
信号递达 是指执行信号处理动作的过程。 当一个信号被进程处理(无论是执行默认动作、忽略动作,还是调用自定义的捕捉函数),我们称该信号已经递达了
信号阻塞(Block)
信号阻塞是指进程可以选择屏蔽某些信号
-
一个信号被阻塞后,如果它产生了,将一直维持在未决状态,直到进程解除对该信号的阻塞,才执行递达的动作
-
注意: 阻塞不同于忽略。忽略是递达的一种处理方式;而阻塞是信号根本没机会递达
2. pending 与 block
内核通过三张位图或数组来管理信号的状态。为了方便理解,我们可以将它们想象成并行排列的结构:
| 表名 | 角色 | 逻辑含义 |
|---|---|---|
| Pending 表 | 未决位图 | 记录进程收到了哪些信号,但还没有来得及处理。收到信号对应位设为 1,处理后设为 0 |
| Block 表 | 阻塞位图 | 记录进程屏蔽了哪些信号。对应位为 1,则即便 Pending 收到该信号,也不会被处理 |
| Handler 表 | 处理方法数组 | 这是一个函数指针数组。下标对应信号编号,内容则是对应的处理函数地址 |
一个信号只有在 Pending 表中为 1 且在 Block 表中为 0 时,才会被内核检测并进行递达操作
五、核心转储(Core Dump)
在信号处理的默认动作中,除了终止进程,还有一个非常重要的概念叫作 Core 。这涉及 Linux 系统中一项关键的调试技术------核心转储
当一个进程接收到某些信号而异常终止时,内核可以将该进程此时在内存中的状态(包括调用栈、变量值、寄存器信息等)转存到磁盘上,生成一个名为 core 或 core.pid 的文件
核心转储允许程序员在程序崩溃后,通过调试器加载这个文件,直接定位到程序报错的那一行代码
1. 标志位
当子进程退出时,父进程通过 wait 或 waitpid 获取到的退出状态码是一个 16 位的整数。这个整数的内部结构如下:
-
高 8 位:进程正常退出的退出码。
-
低 7 位:导致进程退出的信号编号
-
第 7 位 (Core Dump Flag):这就是核心转储标志位

如果进程在退出时生成了 core 文件,该标志位会被设为 1,否则为 0
2. 如何开启核心转储(ulimit)
基于安全和存储空间的考量,大多数现代 Linux 发行版默认禁用了核心转储功能(将 core 文件大小限制设为 0 )
(1) 查看当前限制
可以使用 ulimit 命令查看当前资源限制
bash
ulimit -a # 查看所有限制
ulimit -c # 专门查看 core 文件的大小限制
(2) 开启/修改限制
如果 core file size 为 0,则无法生成文件。你可以手动设置其大小
bash
# 设置 core 文件最大为 10240 块
ulimit -c 10240
# 或者不限制大小
ulimit -c unlimited
这时运行一个会崩溃的代码时(除 0 或野指针),系统提示
Floating point exception (core dumped) 或 Segmentation fault (core dumped)
总结
综上所述,信号机制作为一种轻量级的异步通信方式,使得内核能够在关键时刻"打断"进程的正常执行流程,并传递事件通知。从 Ctrl+C 到 kill、alarm,再到硬件异常触发的信号,我们逐步理解了信号的产生来源以及三种处理方式(默认、忽略、自定义捕捉)
与此同时,通过对递达与阻塞概念的初步认识,我们也看到:信号并不是简单发出就处理,而是在内核中经历记录、屏蔽与调度等过程,最终在合适的时机被进程感知与响应
至此,我们已经建立了对信号机制的整体认知。但信号在内核中究竟是如何被组织与管理的?所谓的 "未决信号" "阻塞信号" 具体存储在哪里?这些问题都指向一个更底层的核心------内核中的三张关键数据结构表
在下一篇中,我们将深入内核源码,系统分析信号相关的三张表(pending、block、handler),从数据结构层面彻底理解信号的运行机制
