跳到主要内容
Linux 信号产生机制详解:从终端按键到内核原理 | 极客日志
C++
Linux 信号产生机制详解:从终端按键到内核原理 综述由AI生成 Linux 信号是进程间异步通信的信使,由操作系统负责产生和投递。基于 Linux 内核原理,详细解析了五种核心信号产生场景:终端按键触发(如 Ctrl+C 发送 SIGINT)、系统命令触发(如 kill 命令)、函数调用触发(kill/raise/abort)、软件条件触发(如 alarm 定时器超时或管道写失败)以及硬件异常触发(如除零操作或非法内存访问)。通过代码实战验证了各信号的默认处理动作及自定义处理方法,帮助开发者深入理解信号产生的底层逻辑与内核机制。
乱七八糟 发布于 2026/3/26 更新于 2026/5/28 28 浏览
前言
在 Linux 系统中,信号是进程间异步通信的'信使',而'信号产生'则是这个通信过程的起点。无论是我们熟悉的 Ctrl+C 终止进程,还是程序运行中出现的段错误、定时器超时,本质上都是信号被触发产生的过程。很多开发者只知道'信号能终止进程',却不清楚信号到底是怎么来的——是用户操作触发的?还是系统自动产生的?不同场景下信号的产生机制有何不同?
本文将基于 Linux 内核原理,结合 5 种核心信号产生场景(终端按键、系统命令、函数调用、软件条件、硬件异常),用通俗的语言,带你全方位揭秘信号产生的底层逻辑。
一、信号产生的核心本质:谁在'发送'信号?
在深入具体场景之前,我们先明确一个核心问题:信号是由谁产生并发送的?答案是操作系统(OS) 。
无论信号的触发源头是用户按键、函数调用还是硬件异常,最终都必须经过 OS 的'中转'——OS 会将这些触发事件解释为对应的信号,再发送给目标进程。这是因为 OS 是进程的'管理者',只有 OS 拥有操作进程 PCB(进程控制块)的权限,能够修改进程的未决信号集,完成信号的'投递'。
举个通俗的例子:信号就像快递,触发信号的源头(用户、函数、硬件)是'寄件人',OS 是'快递员',目标进程是'收件人'。寄件人不会直接把快递交给收件人,而是交给快递员,由快递员负责投递到收件人手中,信号的产生与发送也是如此。
信号产生的完整链路可以总结为:
触发事件(用户/函数/ 硬件等)→ OS 识别事件 → OS 将事件映射为对应信号 → OS 修改目标进程 PCB 的未决信号集 → 信号产生并等待递达
这一链路是所有信号产生场景的共同底层逻辑,接下来我们将针对不同的'触发事件',逐一拆解具体场景。
二、场景 1:终端按键触发 —— 最直观的信号产生方式
**终端(Terminal)**是用户与 Linux 系统交互的主要界面,我们日常使用的 Ctrl+C、Ctrl+\、Ctrl+Z 等组合键,本质上都是通过终端触发信号产生的。这种方式最直观,也是我们接触最多的信号产生场景。
2.1 核心原理:终端按键如何触发信号?
当我们在终端中按下组合键时,会发生以下一系列动作:
键盘按键产生硬件中断 ,终端驱动程序捕获该中断;终端驱动程序将按键事件转换为对应的信号(如 对应 信号);终端将信号发送给 OS,告知 OS'需要向当前前台进程发送某个信号';OS 接收请求后,找到当前前台进程,修改其 PCB 中的未决信号集,完成信号产生。
Ctrl+C
SIGINT
这里有一个关键规则:终端组合键产生的信号,只能发送给当前前台进程 。后台进程(通过 & 启动的进程)无法接收终端组合键产生的信号,这是为了避免后台进程被用户误操作中断。
2.2 三大常用终端信号:实战验证 Linux 终端中最常用的三个组合键对应的信号分别是:Ctrl+C(SIGINT) 、Ctrl+\(SIGQUIT) 、Ctrl+Z(SIGTSTP) ,我们通过实战代码逐一验证它们的产生与作用。
2.2.1 Ctrl+C:SIGINT 信号(2 号)—— 终止进程 SIGINT 信号的默认处理动作是'终止进程',这是我们最常用的'强制终止进程'的方式。
代码验证 1:默认动作 —— 终止进程
#include <iostream>
#include <unistd.h>
using namespace std;
int main () {
cout << "进程 PID:" << getpid () << ",正在运行...(按下 Ctrl+C 终止)" << endl;
while (true ) {
sleep (1 );
cout << "进程正常运行中..." << endl;
}
return 0 ;
}
编译运行 g++ sig_int_default.cpp -o sig_int_default
./sig_int_default
运行后,终端会持续打印'进程正常运行中...',此时按下 Ctrl+C,进程会立即终止,终端输出如下:
进程 PID:12345,正在运行...(按下 Ctrl+C 终止)
进程正常运行中...
进程正常运行中...
^C
代码验证 2:自定义信号处理 —— 让 Ctrl+C 不终止进程 我们可以通过**signal函数自定义 SIGINT**信号的处理动作,让按下 Ctrl+C 后进程不终止,而是执行我们定义的逻辑。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sigint_handler (int signum) {
cout << "\n捕获到信号:" << signum << "(SIGINT),Ctrl+C 无效!" << endl;
cout << "进程继续运行..." << endl;
}
int main () {
cout << "进程 PID:" << getpid () << ",正在运行...(按下 Ctrl+C 测试)" << endl;
signal (SIGINT, sigint_handler);
while (true ) {
sleep (1 );
cout << "进程正常运行中..." << endl;
}
return 0 ;
}
编译运行 g++ sig_int_catch.cpp -o sig_int_catch
./sig_int_catch
运行后按下 Ctrl+C,进程不会终止,而是打印自定义信息:
进程 PID:12346,正在运行...(按下 Ctrl+C 测试)
进程正常运行中...
进程正常运行中...
^C
捕获到信号:2(SIGINT),Ctrl+C 无效!
进程继续运行...
进程正常运行中...
2.2.2 Ctrl+\:SIGQUIT 信号(3 号)—— 终止进程并生成 Core Dump SIGQUIT 信号的默认处理动作是'终止进程并生成 Core Dump 文件'。Core Dump 文件是进程异常终止时的内存镜像文件,包含进程终止时的内存数据、寄存器状态等信息,用于事后调试(Post-mortem Debug)。
核心知识点:Core Dump 文件
默认情况下,Linux 系统会禁用 Core Dump 功能(避免泄露敏感信息),可以通过 ulimit -c 1024 命令临时开启(允许生成最大 1024KB 的 Core 文件);Core 文件的默认名称为 core.进程 PID,存储在进程运行目录下;可以通过 gdb 程序名 core 文件名 命令调试 Core 文件,定位进程崩溃原因。
代码验证:SIGQUIT 信号的默认动作
#include <iostream>
#include <unistd.h>
using namespace std;
int main () {
cout << "进程 PID:" << getpid () << ",正在运行...(按下 Ctrl+\ 生成 Core 文件)" << endl;
while (true ) {
sleep (1 );
cout << "进程正常运行中..." << endl;
}
return 0 ;
}
编译运行与调试
ulimit -c 1024
g++ sig_quit_core.cpp -o sig_quit_core
./sig_quit_core
运行后按下 Ctrl+\ ,进程终止并生成 Core 文件:
进程 PID:12347,正在运行...(按下 Ctrl+\ 生成 Core 文件)
进程正常运行中...
进程正常运行中...
^\Quit (core dumped)
ls -l core*
终端会显示类似 core.12347 的文件,使用 gdb 调试:
gdb sig_quit_core core.12347
调试输出会显示进程终止的原因(收到 SIGQUIT 信号),验证了信号的产生与作用。
2.2.3 Ctrl+Z:SIGTSTP 信号(20 号)—— 暂停前台进程 SIGTSTP 信号的默认处理动作是'暂停前台进程',将进程从'运行态'切换为'停止态(Stopped)',并将其转入后台。暂停的进程可以通过 fg 命令恢复到前台,或通过 bg 命令让其在后台继续运行。
代码验证:SIGTSTP 信号的默认动作
#include <iostream>
#include <unistd.h>
using namespace std;
int main () {
cout << "进程 PID:" << getpid () << ",正在运行...(按下 Ctrl+Z 暂停)" << endl;
while (true ) {
sleep (1 );
cout << "进程正常运行中..." << endl;
}
return 0 ;
}
编译运行与操作 g++ sig_tstp_stop.cpp -o sig_tstp_stop
./sig_tstp_stop
进程 PID:12348,正在运行...(按下 Ctrl+Z 暂停)
进程正常运行中...
进程正常运行中...
^Z[1]+ Stopped ./sig_tstp_stop
jobs
fg %1
bg %1
kill -9 12348
2.3 终端信号的核心总结 组合键 对应信号 信号编号 默认动作 核心用途 Ctrl+C SIGINT 2 终止进程 快速终止前台进程 *Ctrl+* SIGQUIT 3 终止进程 + Core Dump 调试时生成内存镜像 Ctrl+Z SIGTSTP 20 暂停进程 临时暂停前台进程
关键规则:终端信号仅发送给前台进程 ,后台进程(& 启动)不受终端组合键影响。
三、场景 2:系统命令触发 —— 通过 Shell 命令发送信号 除了终端组合键,我们还可以通过 Linux 系统提供的命令主动向进程发送信号,最常用的命令是**kill和 pkill**。这种方式的核心是:通过命令告知 OS'向指定进程发送某个信号',由 OS 完成信号的产生与投递。
3.1 核心命令:kill 命令的用法 kill 命令的本质是调用系统调用**kill()**函数,向指定进程发送信号。其基本语法如下:
kill -信号名 进程 PID
kill -信号编号 进程 PID
kill -l
kill -SIGINT 进程 PID :等价于 Ctrl+C,终止进程;kill -SIGQUIT 进程 PID :终止进程并生成 Core Dump;kill -SIGKILL 进程 PID :强制终止进程(9 号信号,不可捕捉、不可忽略);kill -SIGSTOP 进程 PID :暂停进程(19 号信号,不可捕捉、不可忽略);kill -SIGCONT 进程 PID :恢复暂停的进程。
3.2 实战验证:用 kill 命令发送信号
步骤 1:编写一个后台运行的死循环程序
#include <iostream>
#include <unistd.h>
using namespace std;
int main () {
cout << "后台进程 PID:" << getpid () << ",正在运行..." << endl;
while (true ) {
sleep (1 );
}
return 0 ;
}
步骤 2:编译运行并查看进程 PID
g++ sig_backend.cpp -o sig_backend
./sig_backend &
ps aux | grep sig_backend
user 12349 0.0 0.0 4384 820 pts/0 S 10:00 0:00 ./sig_backend
步骤 3:用 kill 命令发送不同信号
验证 1:发送 SIGINT 信号(2 号) 由于后台进程默认不会处理 SIGINT 信号(除非自定义),进程会继续运行,我们可以通过 jobs 命令查看:
[1]+ Running ./sig_backend &
验证 2:发送 SIGKILL 信号(9 号)—— 强制终止进程 ps aux | grep sig_backend
验证 3:发送 SIGSEGV 信号(11 号)—— 触发段错误 SIGSEGV 信号的默认动作是'终止进程并生成 Core Dump',通常由非法内存访问触发,但我们也可以通过 kill 命令主动发送:
ulimit -c 1024
./sig_backend &
kill -SIGSEGV 12350
[1] + Segmentation fault (core dumped) ./sig_backend
3.3 扩展命令:pkill 命令 —— 按进程名发送信号 pkill 命令可以根据进程名发送信号,无需手动查询 PID,更方便快捷:
pkill -f sig_backend
pkill -SIGINT -f sig_backend
3.4 系统命令触发信号的核心总结
系统命令(kill/pkill)是用户主动发送信号的手段,本质是通过命令调用**kill()**系统调用;信号的产生仍由 OS 完成,命令仅负责传递'发送信号'的请求;9 号信号(SIGKILL)和 19 号信号(SIGSTOP)不可捕捉、不可忽略,是 OS 强制控制进程的终极手段。
四、场景 3:函数调用触发 —— 在代码中主动产生信号 除了通过终端和命令,我们还可以在 C/C++ 代码中调用特定函数,主动产生信号并发送给进程。Linux 系统提供了三个核心函数:kill()、raise()、abort() ,分别用于'向指定进程发送信号'、'向当前进程发送信号'、'强制当前进程异常终止'。
4.1 kill () 函数:向指定进程发送信号 kill() 函数是**kill**命令的底层实现,允许进程向另一个进程发送信号,其函数原型如下:
#include <sys/types.h>
#include <signal.h>
int kill (pid_t pid, int sig) ;
参数说明
sig :要发送的信号编号或宏定义(如 SIGINT、SIGKILL);
返回值 :成功返回 0,失败返回 -1,并设置 errno。
pid > 0 :发送信号给 PID 为 pid 的进程;pid = 0 :发送信号给当前进程所在进程组的所有进程;pid = -1 :发送信号给当前用户有权限发送的所有进程(除了 init 进程);
实战:实现自己的'kill 命令' 我们可以用**kill()**函数实现一个简单的自定义 kill 命令,支持通过'- 信号编号 进程 PID'的格式发送信号:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <cstdlib>
using namespace std;
int main (int argc, char *argv[]) {
if (argc != 3 ) {
cerr << "用法错误!正确格式:" << argv[0 ] << " -signumber pid" << endl;
cerr << "示例:" << argv[0 ] << " -2 12345(发送 SIGINT 信号给 PID 为 12345 的进程)" << endl;
return 1 ;
}
int sig = stoi (argv[1 ] + 1 );
pid_t pid = stoi (argv[2 ]);
int ret = kill (pid, sig);
if (ret == 0 ) {
cout << "成功向进程 PID=" << pid << "发送信号:" << sig << endl;
} else {
cerr << "发送信号失败!可能原因:进程不存在、无权限发送信号" << endl;
return 1 ;
}
return 0 ;
}
编译运行与测试
g++ mykill.cpp -o mykill
./sig_backend &
./mykill -2 12351
./mykill -9 12351
终端输出如下,验证了**kill()**函数的功能:
成功向进程 PID =12351 发送信号:2
成功向进程 PID =12351 发送信号:9
4.2 raise () 函数:向当前进程发送信号 raise() 函数用于向当前进程 发送信号,等价于**kill(getpid(), sig)**,函数原型如下:
#include <signal.h>
int raise (int sig) ;
参数说明
sig :要发送的信号编号或宏定义;返回值 :成功返回 0,失败返回非 0。
实战:每隔 1 秒向自己发送 SIGINT 信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sig_handler (int signum) {
cout << "当前进程 PID:" << getpid () << ",捕获到信号:" << signum << endl;
}
int main () {
cout << "进程 PID:" << getpid () << ",开始运行..." << endl;
signal (SIGINT, sig_handler);
while (true ) {
sleep (1 );
raise (SIGINT);
}
return 0 ;
}
编译运行 g++ sig_raise.cpp -o sig_raise
./sig_raise
终端输出如下,进程每隔 1 秒捕获到一次 SIGINT 信号:
进程 PID:12352,开始运行...
当前进程 PID:12352,捕获到信号:2
当前进程 PID:12352,捕获到信号:2
当前进程 PID:12352,捕获到信号:2
...
4.3 abort () 函数:强制当前进程异常终止 abort() 函数用于强制当前进程异常终止,其本质是向当前进程发送**SIGABRT**信号(6 号),函数原型如下:
#include <stdlib.h>
void abort (void ) ;
核心特点
abort()函数 永远不会返回 ,调用后进程必然终止;即使进程自定义了**SIGABRT信号的处理函数, abort()**函数仍会强制终止进程(处理函数会执行,但执行完毕后进程仍会退出);默认动作是'终止进程并生成 Core Dump 文件'。
实战:验证 abort () 函数的作用
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
using namespace std;
void sigabrt_handler (int signum) {
cout << "捕获到信号:" << signum << "(SIGABRT),abort() 函数被调用!" << endl;
cout << "处理函数执行完毕,进程即将终止..." << endl;
}
int main () {
cout << "进程 PID:" << getpid () << ",开始运行..." << endl;
signal (SIGABRT, sigabrt_handler);
cout << "3 秒后调用 abort() 函数..." << endl;
sleep (3 );
abort ();
cout << "进程继续运行..." << endl;
return 0 ;
}
编译运行 g++ sig_abort.cpp -o sig_abort
./sig_abort
终端输出如下,验证了**abort()**函数的强制终止特性:
进程 PID:12353 ,开始运行...
3 秒后调用 abort () 函数...
捕获到信号:6 (SIGABRT),abort () 函数被调用!
处理函数执行完毕,进程即将终止...
Aborted (core dumped)
4.4 函数调用触发信号的核心总结 函数 功能 核心特点 适用场景 kill() 向指定进程发送信号 支持跨进程发送,需要目标 PID 进程间信号通信 raise() 向当前进程发送信号 仅能向自身发送,等价于 kill (getpid (), sig) 进程自我触发信号 abort() 强制当前进程异常终止 发送 SIGABRT 信号,不可避免终止 程序异常时主动退出
五、场景 4:软件条件触发 —— 由程序运行状态产生信号 软件条件触发是指:信号的产生源于程序的运行状态或软件逻辑,而非用户操作或硬件异常。
最典型的例子是**alarm()**函数设置的定时器超时(触发 SIGALRM 信号),以及向已关闭的管道写数据(触发 SIGPIPE 信号)。
5.1 核心案例 1:alarm () 函数 —— 定时器超时触发 SIGALRM 信号 alarm() 函数用于设置一个定时器,当定时器超时后,OS 会向当前进程发送**SIGALRM**信号(14 号),其默认处理动作是'终止进程'。函数原型如下:
#include <unistd.h>
unsigned int alarm (unsigned int seconds) ;
参数与返回值
seconds :定时器超时时间(秒),若为 0 则取消之前设置的定时器;返回值 :若之前已设置定时器,返回剩余超时时间;若之前无定时器,返回 0。
通俗理解 alarm () 函数 alarm() 函数就像一个'闹钟':你设定一个时间(seconds),时间到后闹钟响起(OS 发送 SIGALRM 信号)。如果在闹钟响之前你重新设定了一个新时间,那么旧的闹钟会被取消,返回值是旧闹钟剩余的时间。
实战 1:基本用法 ——1 秒后终止进程
#include <iostream>
#include <unistd.h>
using namespace std;
int main () {
cout << "进程 PID:" << getpid () << ",设置 1 秒后触发闹钟..." << endl;
alarm (1 );
int count = 0 ;
while (true ) {
count++;
}
return 0 ;
}
编译运行 g++ sig_alarm_basic.cpp -o sig_alarm_basic
./sig_alarm_basic
1 秒后,进程被 SIGALRM 信号终止,终端输出:
进程 PID:12354,设置 1 秒后触发闹钟...
Alarm clock
实战 2:捕捉 SIGALRM 信号 —— 统计 1 秒内的循环次数 我们可以自定义 SIGALRM 信号的处理函数,让定时器超时后不终止进程,而是执行统计逻辑:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int count = 0 ;
void sigalrm_handler (int signum) {
cout << "1 秒时间到!捕获到信号:" << signum << "(SIGALRM)" << endl;
cout << "1 秒内循环执行次数:" << count << endl;
exit (0 );
}
int main () {
cout << "进程 PID:" << getpid () << ",设置 1 秒后触发闹钟..." << endl;
signal (SIGALRM, sigalrm_handler);
alarm (1 );
while (true ) {
count++;
}
return 0 ;
}
编译运行 g++ sig_alarm_catch.cpp -o sig_alarm_catch
./sig_alarm_catch
进程 PID:12355,设置 1 秒后触发闹钟...
1 秒时间到!捕获到信号:14(SIGALRM)
1 秒内循环执行次数:492333713
实战 3:重复闹钟 —— 每隔 1 秒触发一次 SIGALRM 信号 alarm() 函数是'一次性闹钟',触发后会自动取消。如果想要实现重复触发,可以在信号处理函数中重新调用**alarm()**:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int g_count = 0 ;
void sigalrm_handler (int signum) {
g_count++;
cout << "第" << g_count << "次触发闹钟,信号编号:" << signum << endl;
alarm (1 );
}
int main () {
cout << "进程 PID:" << getpid () << ",设置重复闹钟(每隔 1 秒触发)..." << endl;
signal (SIGALRM, sigalrm_handler);
alarm (1 );
while (true ) {
pause ();
}
return 0 ;
}
编译运行 g++ sig_alarm_repeat.cpp -o sig_alarm_repeat
./sig_alarm_repeat
终端输出如下,每隔 1 秒触发一次 SIGALRM 信号:
进程 PID:12356,设置重复闹钟(每隔 1 秒触发)...
第 1 次触发闹钟,信号编号:14
第 2 次触发闹钟,信号编号:14
第 3 次触发闹钟,信号编号:14
...
5.2 核心案例 2:SIGPIPE 信号 —— 向已关闭的管道写数据 SIGPIPE 信号(13 号)的产生条件是:当进程向一个'读端已关闭'的管道(pipe)写入数据时,OS 会向该进程发送 SIGPIPE 信号,默认处理动作是'终止进程'。
管道的核心特性
管道是半双工 的,分为读端(r)和写端(w);当所有读端关闭后,写端进程向管道写入数据时,OS 会发送 SIGPIPE 信号终止写端进程;这是为了避免写端进程无意义地写入数据(没有进程读取,数据会丢失)。
实战:触发 SIGPIPE 信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;
void sigpipe_handler (int signum) {
cout << "捕获到信号:" << signum << "(SIGPIPE),向已关闭的管道写数据!" << endl;
exit (1 );
}
int main () {
int pipefd[2 ];
if (pipe (pipefd) == -1 ) {
perror ("pipe 创建失败" );
return 1 ;
}
cout << "进程 PID:" << getpid () << ",管道创建成功(读端:" << pipefd[0 ] << ",写端:" << pipefd[1 ] << ")" << endl;
signal (SIGPIPE, sigpipe_handler);
close (pipefd[0 ]);
cout << "已关闭管道读端,尝试向写端写入数据..." << endl;
const char *msg = "Hello, Pipe!" ;
while (true ) {
ssize_t ret = write (pipefd[1 ], msg, strlen (msg));
if (ret == -1 ) {
perror ("write 失败" );
sleep (1 );
} else {
cout << "成功写入" << ret << "字节数据:" << msg << endl;
}
sleep (1 );
}
close (pipefd[1 ]);
return 0 ;
}
编译运行 g++ sig_pipe.cpp -o sig_pipe
./sig_pipe
进程 PID:12357,管道创建成功(读端:3,写端:4)
已关闭管道读端,尝试向写端写入数据...
捕获到信号:13(SIGPIPE),向已关闭的管道写数据!
5.3 软件条件触发信号的核心总结
软件条件信号的产生源于程序的运行状态(如定时器超时、管道读写异常);这类信号是 OS 对程序运行逻辑的'反馈',用于告知程序'某个软件事件已发生';常见的软件条件信号包括 SIGALRM (定时器超时)、SIGPIPE (管道写失败)、SIGCHLD (子进程终止)等。
六、场景 5:硬件异常触发 —— 由硬件错误产生信号 硬件异常触发是指:信号的产生源于 CPU 或其他硬件设备的错误,如除零操作、非法内存访问、总线错误等。硬件检测到错误后,会通知 OS,OS 将其映射为对应的信号,发送给当前进程。
这类信号的本质是:硬件错误通过 OS 转换为软件层面的信号,让进程有机会处理错误(如打印日志、保存数据),若不处理则执行默认动作(通常是终止进程并生成 Core Dump)。
6.1 核心案例 1:除零操作 —— 触发 SIGFPE 信号(8 号) 当进程执行'除以零'的算术运算时,CPU 的运算单元会检测到该错误,通知 OS,OS 将其映射为**SIGFPE**信号(Floating-point exception,浮点异常),默认处理动作是'终止进程并生成 Core Dump'。
实战:模拟除零操作触发 SIGFPE 信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sigfpe_handler (int signum) {
cout << "捕获到信号:" << signum << "(SIGFPE),发生除零错误!" << endl;
exit (1 );
}
int main () {
cout << "进程 PID:" << getpid () << ",尝试执行除零操作..." << endl;
signal (SIGFPE, sigfpe_handler);
sleep (1 );
int a = 10 ;
int b = 0 ;
int c = a / b;
cout << "计算结果:" << c << endl;
return 0 ;
}
编译运行 g++ sig_fpe_divzero.cpp -o sig_fpe_divzero
./sig_fpe_divzero
进程 PID:12358,尝试执行除零操作...
捕获到信号:8(SIGFPE),发生除零错误!
关键注意:为什么会无限触发信号? 如果我们不在处理函数中退出进程,会发现 SIGFPE 信号会被无限触发。原因是:除零错误发生后,CPU 的状态寄存器会记录该错误状态,若不清理该状态,OS 会持续检测到错误,不断发送 SIGFPE 信号。
因此,在处理 SIGFPE 信号时,通常需要在处理函数中调用 exit() 或 _exit() 终止进程,避免无限循环。
6.2 核心案例 2:非法内存访问 —— 触发 SIGSEGV 信号(11 号) 当进程访问非法内存地址(如空指针、数组越界)时,MMU(内存管理单元)会检测到该错误,通知 OS,OS 将其映射为**SIGSEGV**信号(Segmentation fault,段错误),默认处理动作是'终止进程并生成 Core Dump'。
实战 1:空指针访问触发 SIGSEGV 信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sigsegv_handler (int signum) {
cout << "捕获到信号:" << signum << "(SIGSEGV),非法内存访问!" << endl;
exit (1 );
}
int main () {
cout << "进程 PID:" << getpid () << ",尝试访问空指针..." << endl;
signal (SIGSEGV, sigsegv_handler);
sleep (1 );
int *p = nullptr ;
*p = 100 ;
cout << "赋值成功:" << *p << endl;
return 0 ;
}
编译运行 g++ sig_segv_nullptr.cpp -o sig_segv_nullptr
./sig_segv_nullptr
进程 PID:12359,尝试访问空指针...
捕获到信号:11(SIGSEGV),非法内存访问!
实战 2:数组越界访问触发 SIGSEGV 信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sigsegv_handler (int signum) {
cout << "捕获到信号:" << signum << "(SIGSEGV),数组越界访问!" << endl;
exit (1 );
}
int main () {
cout << "进程 PID:" << getpid () << ",尝试数组越界访问..." << endl;
signal (SIGSEGV, sigsegv_handler);
sleep (1 );
int arr[5 ] = {1 , 2 , 3 , 4 , 5 };
cout << "arr[10] = " << arr[10 ] << endl;
return 0 ;
}
编译运行 g++ sig_segv_array.cpp -o sig_segv_array
./sig_segv_array
进程 PID:12360,尝试数组越界访问...
捕获到信号:11(SIGSEGV),数组越界访问!
6.3 核心案例 3:总线错误 —— 触发 SIGBUS 信号(10 号) SIGBUS 信号(Bus error)的产生条件是:进程访问的内存地址是有效的,但访问方式不正确(如对齐错误、内存映射失败)。与 SIGSEGV 信号(非法地址)的区别在于:SIGBUS 是'地址有效但访问方式错误',SIGSEGV 是'地址本身无效'。
实战:内存对齐错误触发 SIGBUS 信号 在某些 CPU 架构(如 ARM)中,访问未对齐的内存地址会触发 SIGBUS 信号。以下代码在 x86 架构中可能不会触发,但在 ARM 架构中会触发:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;
void sigbus_handler (int signum) {
cout << "捕获到信号:" << signum << "(SIGBUS),总线错误(内存对齐错误)!" << endl;
exit (1 );
}
int main () {
cout << "进程 PID:" << getpid () << ",尝试访问未对齐的内存地址..." << endl;
signal (SIGBUS, sigbus_handler);
sleep (1 );
char buf[10 ];
int *p = (int *)(buf + 1 );
*p = 0x12345678 ;
cout << "赋值成功:" << *p << endl;
return 0 ;
}
编译运行(ARM 架构)
g++ sig_bus_align.cpp -o sig_bus_align
./sig_bus_align
进程 PID:12361,尝试访问未对齐的内存地址...
捕获到信号:10(SIGBUS),总线错误(内存对齐错误)!
6.4 硬件异常触发信号的核心总结 硬件异常 对应信号 信号编号 触发原因 默认动作 除零操作 SIGFPE 8 算术运算错误(除以零、浮点溢出) 终止 + Core Dump 非法内存访问 SIGSEGV 11 访问无效内存地址(空指针、数组越界) 终止 + Core Dump 总线错误 SIGBUS 10 访问方式错误(内存对齐错误、映射失败) 终止 + Core Dump
SIGSEGV :地址无效('地址不存在');SIGBUS :地址有效,但访问方式错误('地址存在但进不去')。
总结 信号产生是 Linux 信号机制的基础,理解了不同场景下信号的产生逻辑,才能更好地掌握信号的处理与应用。本文的所有代码都经过实战验证,建议大家亲手编译运行,感受信号产生的过程。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online