跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux 信号产生机制详解:从终端按键到内核原理

Linux 信号是进程间异步通信的信使,由操作系统负责产生和投递。基于 Linux 内核原理,详细解析了五种核心信号产生场景:终端按键触发(如 Ctrl+C 发送 SIGINT)、系统命令触发(如 kill 命令)、函数调用触发(kill/raise/abort)、软件条件触发(如 alarm 定时器超时或管道写失败)以及硬件异常触发(如除零操作或非法内存访问)。通过代码实战验证了各信号的默认处理动作及自定义处理方法,帮助开发者深入理解信号产生的底层逻辑与内核机制。

乱七八糟发布于 2026/3/26更新于 2026/10/22105 浏览
Linux 信号产生机制详解:从终端按键到内核原理

文章配图


前言

在 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:默认动作 —— 终止进程
// sig_int_default.cpp
#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 # 按下 Ctrl+C
代码验证 2:自定义信号处理 —— 让 Ctrl+C 不终止进程

我们可以通过**signal函数自定义SIGINT**信号的处理动作,让按下 Ctrl+C 后进程不终止,而是执行我们定义的逻辑。

// sig_int_catch.cpp
#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;
    // 注册 SIGINT 信号的处理函数
    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 信号的默认动作
// sig_quit_core.cpp
#include <iostream>
#include <unistd.h>
using namespace std;

int main() {
    cout << "进程 PID:" << getpid() << ",正在运行...(按下 Ctrl+\ 生成 Core 文件)" << endl;
    while (true) {
        sleep(1);
        cout << "进程正常运行中..." << endl;
    }
    return 0;
}
编译运行与调试
# 开启 Core Dump 功能(临时生效)
ulimit -c 1024
# 编译
g++ sig_quit_core.cpp -o sig_quit_core
# 运行
./sig_quit_core

运行后按下 Ctrl+\,进程终止并生成 Core 文件:

进程 PID:12347,正在运行...(按下 Ctrl+\ 生成 Core 文件)
进程正常运行中...
进程正常运行中...
^\Quit (core dumped) # 按下 Ctrl+\
# 查看生成的 Core 文件
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 信号的默认动作
// sig_tstp_stop.cpp
#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

运行后按下 Ctrl+Z,进程被暂停并转入后台:

进程 PID:12348,正在运行...(按下 Ctrl+Z 暂停)
进程正常运行中...
进程正常运行中...
^Z[1]+ Stopped ./sig_tstp_stop # 进程被暂停

后续操作命令:

# 查看后台暂停的进程
jobs
# 将暂停的进程恢复到前台运行
fg %1
# 将暂停的进程在后台继续运行
bg %1
# 终止后台进程
kill -9 12348

2.3 终端信号的核心总结

组合键对应信号信号编号默认动作核心用途
Ctrl+CSIGINT2终止进程快速终止前台进程
*Ctrl+*SIGQUIT3终止进程 + Core Dump调试时生成内存镜像
Ctrl+ZSIGTSTP20暂停进程临时暂停前台进程

关键规则:终端信号仅发送给前台进程,后台进程(& 启动)不受终端组合键影响。

三、场景 2:系统命令触发 —— 通过 Shell 命令发送信号

除了终端组合键,我们还可以通过 Linux 系统提供的命令主动向进程发送信号,最常用的命令是**kill和pkill**。这种方式的核心是:通过命令告知 OS'向指定进程发送某个信号',由 OS 完成信号的产生与投递。

3.1 核心命令:kill 命令的用法

kill 命令的本质是调用系统调用**kill()**函数,向指定进程发送信号。其基本语法如下:

# 格式 1:通过信号名称发送
kill -信号名 进程 PID
# 格式 2:通过信号编号发送
kill -信号编号 进程 PID
# 格式 3:列出所有信号
kill -l

常用信号与 kill 命令组合:

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:编写一个后台运行的死循环程序
// sig_backend.cpp
#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 &
# 查看进程 PID(确认进程正在运行)
ps aux | grep sig_backend

终端输出类似如下(PID 为 12349):

user 12349 0.0 0.0 4384 820 pts/0 S 10:00 0:00 ./sig_backend
步骤 3:用 kill 命令发送不同信号
验证 1:发送 SIGINT 信号(2 号)
kill -SIGINT 12349 # 或 kill -2 12349

由于后台进程默认不会处理 SIGINT 信号(除非自定义),进程会继续运行,我们可以通过 jobs 命令查看:

jobs

输出显示进程仍在运行:

[1]+ Running ./sig_backend &
验证 2:发送 SIGKILL 信号(9 号)—— 强制终止进程
kill -SIGKILL 12349 # 或 kill -9 12349

再次查看进程,发现进程已被终止:

ps aux | grep sig_backend # 无相关进程输出(或显示<defunct>,表示僵尸进程,后续会被系统清理)
验证 3:发送 SIGSEGV 信号(11 号)—— 触发段错误

SIGSEGV 信号的默认动作是'终止进程并生成 Core Dump',通常由非法内存访问触发,但我们也可以通过 kill 命令主动发送:

# 先开启 Core Dump 功能
ulimit -c 1024
# 重新启动后台进程
./sig_backend &
# 发送 SIGSEGV 信号
kill -SIGSEGV 12350 # 或 kill -11 12350

终端输出如下,进程被终止并生成 Core 文件:

[1]+ Segmentation fault (core dumped) ./sig_backend

3.3 扩展命令:pkill 命令 —— 按进程名发送信号

pkill 命令可以根据进程名发送信号,无需手动查询 PID,更方便快捷:

# 终止所有名为 sig_backend 的进程
pkill -f sig_backend
# 向所有名为 sig_backend 的进程发送 SIGINT 信号
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:目标进程的 PID,有三种取值:

pid > 0:发送信号给 PID 为 pid 的进程;pid = 0:发送信号给当前进程所在进程组的所有进程;pid = -1:发送信号给当前用户有权限发送的所有进程(除了 init 进程);

实战:实现自己的'kill 命令'

我们可以用**kill()**函数实现一个简单的自定义 kill 命令,支持通过'- 信号编号 进程 PID'的格式发送信号:

// mykill.cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <cstdlib>
using namespace std;

int main(int argc, char *argv[]) {
    // 检查参数个数:./mykill -signumber pid
    if (argc != 3) {
        cerr << "用法错误!正确格式:" << argv[0] << " -signumber pid" << endl;
        cerr << "示例:" << argv[0] << " -2 12345(发送 SIGINT 信号给 PID 为 12345 的进程)" << endl;
        return 1;
    }
    // 解析信号编号(去掉 argv[1] 的 '-' 前缀)
    int sig = stoi(argv[1] + 1);
    // 解析目标进程 PID
    pid_t pid = stoi(argv[2]);
    // 调用 kill() 函数发送信号
    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)
./sig_backend &
# 用自定义 mykill 发送 SIGINT 信号(2 号)
./mykill -2 12351
# 用自定义 mykill 发送 SIGKILL 信号(9 号),强制终止进程
./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 信号
// sig_raise.cpp
#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;
    // 注册 SIGINT 信号的处理函数
    signal(SIGINT, sig_handler);
    // 每隔 1 秒,向当前进程发送 SIGINT 信号
    while (true) {
        sleep(1);
        // 调用 raise() 函数发送信号
        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 () 函数的作用
// sig_abort.cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
using namespace std;

// 自定义 SIGABRT 信号处理函数
void sigabrt_handler(int signum) {
    cout << "捕获到信号:" << signum << "(SIGABRT),abort() 函数被调用!" << endl;
    cout << "处理函数执行完毕,进程即将终止..." << endl;
}

int main() {
    cout << "进程 PID:" << getpid() << ",开始运行..." << endl;
    // 注册 SIGABRT 信号的处理函数
    signal(SIGABRT, sigabrt_handler);
    cout << "3 秒后调用 abort() 函数..." << endl;
    sleep(3);
    // 调用 abort() 函数,强制终止进程
    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 秒后终止进程
// sig_alarm_basic.cpp
#include <iostream>
#include <unistd.h>
using namespace std;

int main() {
    cout << "进程 PID:" << getpid() << ",设置 1 秒后触发闹钟..." << endl;
    // 设置定时器:1 秒后发送 SIGALRM 信号
    alarm(1);
    // 死循环,等待信号触发
    int count = 0;
    while (true) {
        count++;
        // 不输出过多信息,避免 IO 影响计数
    }
    return 0;
}
编译运行
g++ sig_alarm_basic.cpp -o sig_alarm_basic
./sig_alarm_basic

1 秒后,进程被 SIGALRM 信号终止,终端输出:

进程 PID:12354,设置 1 秒后触发闹钟...
Alarm clock # SIGALRM 信号的默认终止信息
实战 2:捕捉 SIGALRM 信号 —— 统计 1 秒内的循环次数

我们可以自定义 SIGALRM 信号的处理函数,让定时器超时后不终止进程,而是执行统计逻辑:

// sig_alarm_catch.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

int count = 0;

// 自定义 SIGALRM 信号处理函数
void sigalrm_handler(int signum) {
    cout << "1 秒时间到!捕获到信号:" << signum << "(SIGALRM)" << endl;
    cout << "1 秒内循环执行次数:" << count << endl;
    // 退出进程
    exit(0);
}

int main() {
    cout << "进程 PID:" << getpid() << ",设置 1 秒后触发闹钟..." << endl;
    // 注册 SIGALRM 信号的处理函数
    signal(SIGALRM, sigalrm_handler);
    // 设置定时器:1 秒后发送 SIGALRM 信号
    alarm(1);
    // 死循环计数
    while (true) {
        count++;
    }
    return 0;
}
编译运行
g++ sig_alarm_catch.cpp -o sig_alarm_catch
./sig_alarm_catch

终端输出如下,1 秒后进程打印统计结果并退出:

进程 PID:12355,设置 1 秒后触发闹钟...
1 秒时间到!捕获到信号:14(SIGALRM)
1 秒内循环执行次数:492333713 # 数值因 CPU 性能而异
实战 3:重复闹钟 —— 每隔 1 秒触发一次 SIGALRM 信号

alarm() 函数是'一次性闹钟',触发后会自动取消。如果想要实现重复触发,可以在信号处理函数中重新调用**alarm()**:

// sig_alarm_repeat.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

int g_count = 0;

// 自定义 SIGALRM 信号处理函数
void sigalrm_handler(int signum) {
    g_count++;
    cout << "第" << g_count << "次触发闹钟,信号编号:" << signum << endl;
    // 重新设置闹钟:1 秒后再次触发
    alarm(1);
}

int main() {
    cout << "进程 PID:" << getpid() << ",设置重复闹钟(每隔 1 秒触发)..." << endl;
    // 注册 SIGALRM 信号的处理函数
    signal(SIGALRM, sigalrm_handler);
    // 第一次设置闹钟:1 秒后触发
    alarm(1);
    // 暂停进程,等待信号触发(避免死循环占用 CPU)
    while (true) {
        pause(); // 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 信号
// sig_pipe.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;

// 自定义 SIGPIPE 信号处理函数
void sigpipe_handler(int signum) {
    cout << "捕获到信号:" << signum << "(SIGPIPE),向已关闭的管道写数据!" << endl;
    exit(1);
}

int main() {
    int pipefd[2];
    // pipefd[0]:读端,pipefd[1]:写端
    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe 创建失败");
        return 1;
    }
    cout << "进程 PID:" << getpid() << ",管道创建成功(读端:" << pipefd[0] << ",写端:" << pipefd[1] << ")" << endl;
    // 注册 SIGPIPE 信号的处理函数
    signal(SIGPIPE, sigpipe_handler);
    // 关闭读端(模拟读端已关闭的场景)
    close(pipefd[0]);
    cout << "已关闭管道读端,尝试向写端写入数据..." << endl;
    // 向管道写端写入数据(此时读端已关闭,会触发 SIGPIPE 信号)
    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

终端输出如下,触发了 SIGPIPE 信号:

进程 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 信号
// sig_fpe_divzero.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 自定义 SIGFPE 信号处理函数
void sigfpe_handler(int signum) {
    cout << "捕获到信号:" << signum << "(SIGFPE),发生除零错误!" << endl;
    // 退出进程(避免无限循环触发信号)
    exit(1);
}

int main() {
    cout << "进程 PID:" << getpid() << ",尝试执行除零操作..." << endl;
    // 注册 SIGFPE 信号的处理函数
    signal(SIGFPE, sigfpe_handler);
    sleep(1); // 延迟 1 秒,便于观察
    // 执行除零操作
    int a = 10;
    int b = 0;
    int c = a / b; // 除零错误,触发 SIGFPE 信号
    // 以下代码不会执行
    cout << "计算结果:" << c << endl;
    return 0;
}
编译运行
g++ sig_fpe_divzero.cpp -o sig_fpe_divzero
./sig_fpe_divzero

终端输出如下,触发了 SIGFPE 信号:

进程 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 信号
// sig_segv_nullptr.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 自定义 SIGSEGV 信号处理函数
void sigsegv_handler(int signum) {
    cout << "捕获到信号:" << signum << "(SIGSEGV),非法内存访问!" << endl;
    exit(1);
}

int main() {
    cout << "进程 PID:" << getpid() << ",尝试访问空指针..." << endl;
    // 注册 SIGSEGV 信号的处理函数
    signal(SIGSEGV, sigsegv_handler);
    sleep(1); // 访问空指针(非法内存访问)
    int *p = nullptr;
    *p = 100; // 触发 SIGSEGV 信号
    // 以下代码不会执行
    cout << "赋值成功:" << *p << endl;
    return 0;
}
编译运行
g++ sig_segv_nullptr.cpp -o sig_segv_nullptr
./sig_segv_nullptr

终端输出如下,触发了 SIGSEGV 信号:

进程 PID:12359,尝试访问空指针...
捕获到信号:11(SIGSEGV),非法内存访问!
实战 2:数组越界访问触发 SIGSEGV 信号
// sig_segv_array.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 自定义 SIGSEGV 信号处理函数
void sigsegv_handler(int signum) {
    cout << "捕获到信号:" << signum << "(SIGSEGV),数组越界访问!" << endl;
    exit(1);
}

int main() {
    cout << "进程 PID:" << getpid() << ",尝试数组越界访问..." << endl;
    // 注册 SIGSEGV 信号的处理函数
    signal(SIGSEGV, sigsegv_handler);
    sleep(1);
    // 数组越界访问(非法内存访问)
    int arr[5] = {1, 2, 3, 4, 5};
    cout << "arr[10] = " << arr[10] << endl; // 触发 SIGSEGV 信号
    return 0;
}
编译运行
g++ sig_segv_array.cpp -o sig_segv_array
./sig_segv_array

终端输出如下,触发了 SIGSEGV 信号:

进程 PID:12360,尝试数组越界访问...
捕获到信号:11(SIGSEGV),数组越界访问!

6.3 核心案例 3:总线错误 —— 触发 SIGBUS 信号(10 号)

SIGBUS 信号(Bus error)的产生条件是:进程访问的内存地址是有效的,但访问方式不正确(如对齐错误、内存映射失败)。与 SIGSEGV 信号(非法地址)的区别在于:SIGBUS 是'地址有效但访问方式错误',SIGSEGV 是'地址本身无效'。

实战:内存对齐错误触发 SIGBUS 信号

在某些 CPU 架构(如 ARM)中,访问未对齐的内存地址会触发 SIGBUS 信号。以下代码在 x86 架构中可能不会触发,但在 ARM 架构中会触发:

// sig_bus_align.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;

// 自定义 SIGBUS 信号处理函数
void sigbus_handler(int signum) {
    cout << "捕获到信号:" << signum << "(SIGBUS),总线错误(内存对齐错误)!" << endl;
    exit(1);
}

int main() {
    cout << "进程 PID:" << getpid() << ",尝试访问未对齐的内存地址..." << endl;
    // 注册 SIGBUS 信号的处理函数
    signal(SIGBUS, sigbus_handler);
    sleep(1);
    // 内存对齐错误:char 数组的地址是 1 字节对齐,强制转换为 int*(4 字节对齐)
    char buf[10];
    int *p = (int *)(buf + 1); // buf+1 的地址不是 4 的倍数,未对齐
    *p = 0x12345678; // 触发 SIGBUS 信号
    // 以下代码不会执行
    cout << "赋值成功:" << *p << endl;
    return 0;
}
编译运行(ARM 架构)
# 在 ARM 架构的 Linux 系统中编译运行
g++ sig_bus_align.cpp -o sig_bus_align
./sig_bus_align

终端输出如下,触发了 SIGBUS 信号:

进程 PID:12361,尝试访问未对齐的内存地址...
捕获到信号:10(SIGBUS),总线错误(内存对齐错误)!

6.4 硬件异常触发信号的核心总结

硬件异常对应信号信号编号触发原因默认动作
除零操作SIGFPE8算术运算错误(除以零、浮点溢出)终止 + Core Dump
非法内存访问SIGSEGV11访问无效内存地址(空指针、数组越界)终止 + Core Dump
总线错误SIGBUS10访问方式错误(内存对齐错误、映射失败)终止 + Core Dump

关键区别:

SIGSEGV:地址无效('地址不存在');SIGBUS:地址有效,但访问方式错误('地址存在但进不去')。


总结

信号产生是 Linux 信号机制的基础,理解了不同场景下信号的产生逻辑,才能更好地掌握信号的处理与应用。本文的所有代码都经过实战验证,建议大家亲手编译运行,感受信号产生的过程。

目录

  1. 前言
  2. 一、信号产生的核心本质:谁在“发送”信号?
  3. 二、场景 1:终端按键触发 —— 最直观的信号产生方式
  4. 2.1 核心原理:终端按键如何触发信号?
  5. 2.2 三大常用终端信号:实战验证
  6. 2.2.1 Ctrl+C:SIGINT 信号(2 号)—— 终止进程
  7. 代码验证 1:默认动作 —— 终止进程
  8. 编译运行
  9. 代码验证 2:自定义信号处理 —— 让 Ctrl+C 不终止进程
  10. 编译运行
  11. 2.2.2 Ctrl+\:SIGQUIT 信号(3 号)—— 终止进程并生成 Core Dump
  12. 核心知识点:Core Dump 文件
  13. 代码验证:SIGQUIT 信号的默认动作
  14. 编译运行与调试
  15. 开启 Core Dump 功能(临时生效)
  16. 编译
  17. 运行
  18. 查看生成的 Core 文件
  19. 2.2.3 Ctrl+Z:SIGTSTP 信号(20 号)—— 暂停前台进程
  20. 代码验证:SIGTSTP 信号的默认动作
  21. 编译运行与操作
  22. 查看后台暂停的进程
  23. 将暂停的进程恢复到前台运行
  24. 将暂停的进程在后台继续运行
  25. 终止后台进程
  26. 2.3 终端信号的核心总结
  27. 三、场景 2:系统命令触发 —— 通过 Shell 命令发送信号
  28. 3.1 核心命令:kill 命令的用法
  29. 格式 1:通过信号名称发送
  30. 格式 2:通过信号编号发送
  31. 格式 3:列出所有信号
  32. 3.2 实战验证:用 kill 命令发送信号
  33. 步骤 1:编写一个后台运行的死循环程序
  34. 步骤 2:编译运行并查看进程 PID
  35. 编译
  36. 后台运行
  37. 查看进程 PID(确认进程正在运行)
  38. 步骤 3:用 kill 命令发送不同信号
  39. 验证 1:发送 SIGINT 信号(2 号)
  40. 验证 2:发送 SIGKILL 信号(9 号)—— 强制终止进程
  41. 验证 3:发送 SIGSEGV 信号(11 号)—— 触发段错误
  42. 先开启 Core Dump 功能
  43. 重新启动后台进程
  44. 发送 SIGSEGV 信号
  45. 3.3 扩展命令:pkill 命令 —— 按进程名发送信号
  46. 终止所有名为 sig_backend 的进程
  47. 向所有名为 sig_backend 的进程发送 SIGINT 信号
  48. 3.4 系统命令触发信号的核心总结
  49. 四、场景 3:函数调用触发 —— 在代码中主动产生信号
  50. 4.1 kill () 函数:向指定进程发送信号
  51. 参数说明
  52. 实战:实现自己的“kill 命令”
  53. 编译运行与测试
  54. 编译
  55. 先启动一个后台进程(如之前的 sig_backend)
  56. 用自定义 mykill 发送 SIGINT 信号(2 号)
  57. 用自定义 mykill 发送 SIGKILL 信号(9 号),强制终止进程
  58. 4.2 raise () 函数:向当前进程发送信号
  59. 参数说明
  60. 实战:每隔 1 秒向自己发送 SIGINT 信号
  61. 编译运行
  62. 4.3 abort () 函数:强制当前进程异常终止
  63. 核心特点
  64. 实战:验证 abort () 函数的作用
  65. 编译运行
  66. 4.4 函数调用触发信号的核心总结
  67. 五、场景 4:软件条件触发 —— 由程序运行状态产生信号
  68. 5.1 核心案例 1:alarm () 函数 —— 定时器超时触发 SIGALRM 信号
  69. 参数与返回值
  70. 通俗理解 alarm () 函数
  71. 实战 1:基本用法 ——1 秒后终止进程
  72. 编译运行
  73. 实战 2:捕捉 SIGALRM 信号 —— 统计 1 秒内的循环次数
  74. 编译运行
  75. 实战 3:重复闹钟 —— 每隔 1 秒触发一次 SIGALRM 信号
  76. 编译运行
  77. 5.2 核心案例 2:SIGPIPE 信号 —— 向已关闭的管道写数据
  78. 管道的核心特性
  79. 实战:触发 SIGPIPE 信号
  80. 编译运行
  81. 5.3 软件条件触发信号的核心总结
  82. 六、场景 5:硬件异常触发 —— 由硬件错误产生信号
  83. 6.1 核心案例 1:除零操作 —— 触发 SIGFPE 信号(8 号)
  84. 实战:模拟除零操作触发 SIGFPE 信号
  85. 编译运行
  86. 关键注意:为什么会无限触发信号?
  87. 6.2 核心案例 2:非法内存访问 —— 触发 SIGSEGV 信号(11 号)
  88. 实战 1:空指针访问触发 SIGSEGV 信号
  89. 编译运行
  90. 实战 2:数组越界访问触发 SIGSEGV 信号
  91. 编译运行
  92. 6.3 核心案例 3:总线错误 —— 触发 SIGBUS 信号(10 号)
  93. 实战:内存对齐错误触发 SIGBUS 信号
  94. 编译运行(ARM 架构)
  95. 在 ARM 架构的 Linux 系统中编译运行
  96. 6.4 硬件异常触发信号的核心总结
  97. 总结

更多推荐文章

查看全部
  • 基于 Java 的外卖点餐系统设计与实现
  • 大模型 AI 产品经理学习路线:从零基础到精通
  • 基于 Python+Django+MySQL 的民宿预订及评论情感分析系统
  • OpenClaw Web UI 访问报错 Not Found 排查与修复
  • Coze 工作流实战:将文章链接转为思维导图
  • MySQL 权限管理与 C/C++ 客户端开发实战指南
  • 基于 SQL 与 AI 的 ER 图自动生成技术解析
  • Llama 3 大模型发布总结与核心特性分析
  • C++ 多线程同步:原子操作 atomic 实战
  • 2026 年 6 款主流免费 AI 写作工具实测与去 AI 味方案
  • 基于 Leaflet 和天地图的免费运动场所 WebGIS 可视化
  • 程序员 Python 副业方向与接单实战建议
  • VS Code 前端开发必备插件指南:10 款工具与配置详解
  • Mac 下 iPhone 镜像连接设置方法
  • 算法实战:Z 字形变换与外观数列解析
  • 实战:用 Claude Code 重构 Jakarta EE 消息队列生产者代码
  • 2025 年 AI 在 25 个核心领域的发展趋势与创新展望
  • ComfyUI 实战:AI 抠图与背景替换(图片/视频)
  • 二级 Python 考试真题与基础代码解析
  • AI 大型语言模型在商业智能(BI)领域的实际应用

相关免费在线工具

  • 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