前言:信号是Linux系统中最古老的进程间通信方式之一,理解信号对于系统编程至关重要。本文将从生活实例出发,逐步深入到内核实现,涵盖信号概念、产生、保存、处理、常见陷阱以及工程实践,帮你彻底掌握Linux进程信号。
Linux进程信号:从基础概念到内核底层原理
-
- 一、信号的基本概念
-
- [1.1 什么是信号](#1.1 什么是信号)
- [1.2 信号的三大处理方式](#1.2 信号的三大处理方式)
- [1.3 常用信号速查表](#1.3 常用信号速查表)
- [1.4 标准信号 vs 实时信号](#1.4 标准信号 vs 实时信号)
- [1.5 可靠信号与不可靠信号](#1.5 可靠信号与不可靠信号)
- 二、信号的完整生命周期
- 三、信号的产生方式
-
- [3.1 终端按键产生(仅限前台进程)](#3.1 终端按键产生(仅限前台进程))
- [3.2 硬件异常产生](#3.2 硬件异常产生)
- [3.3 系统调用/函数产生](#3.3 系统调用/函数产生)
-
- [3.3.1 `kill()` --- 向任意进程发送信号](#3.3.1
kill()— 向任意进程发送信号) - [3.3.2 `raise()` --- 给当前进程发信号](#3.3.2
raise()— 给当前进程发信号) - [3.3.3 `abort()` --- 异常终止(给自己发SIGABRT)](#3.3.3
abort()— 异常终止(给自己发SIGABRT))
- [3.3.1 `kill()` --- 向任意进程发送信号](#3.3.1
- [3.4 软件条件产生](#3.4 软件条件产生)
-
- [3.4.1 `alarm()` --- 闹钟信号](#3.4.1
alarm()— 闹钟信号) - [3.4.2 SIGPIPE --- 管道断裂](#3.4.2 SIGPIPE — 管道断裂)
- [3.4.1 `alarm()` --- 闹钟信号](#3.4.1
- [3.5 命令行发送信号](#3.5 命令行发送信号)
- 四、信号的保存(Pending与Block)
-
- [4.1 内核数据结构](#4.1 内核数据结构)
- [4.2 信号集操作函数](#4.2 信号集操作函数)
- [4.3 修改阻塞掩码:`sigprocmask`](#4.3 修改阻塞掩码:
sigprocmask) - [4.4 获取pending信号集:`sigpending`](#4.4 获取pending信号集:
sigpending) - [4.5 完整示例:阻塞2号信号并观察pending变化](#4.5 完整示例:阻塞2号信号并观察pending变化)
- 五、信号的捕捉与处理
-
- [5.1 信号捕捉函数](#5.1 信号捕捉函数)
-
- [5.1.1 `signal()`(简单但不可靠)](#5.1.1
signal()(简单但不可靠)) - [5.1.2 `sigaction()`(推荐,POSIX标准)](#5.1.2
sigaction()(推荐,POSIX标准)) - [5.1.3 信号嵌套与 sa_mask 的深入解释](#5.1.3 信号嵌套与 sa_mask 的深入解释)
- [5.1.4 `sigqueue()` --- 发送带数据的信号(实时信号专用)](#5.1.4
sigqueue()— 发送带数据的信号(实时信号专用))
- [5.1.1 `signal()`(简单但不可靠)](#5.1.1
- [5.2 暂停进程等待信号:`pause()` 与 `sigsuspend()`](#5.2 暂停进程等待信号:
pause()与sigsuspend()) -
- [5.2.1 `pause()` --- 简单挂起](#5.2.1
pause()— 简单挂起) - [5.2.2 `sigsuspend()` --- 原子挂起(解决竞态)](#5.2.2
sigsuspend()— 原子挂起(解决竞态))
- [5.2.1 `pause()` --- 简单挂起](#5.2.1
- [5.3 信号处理流程(内核态↔用户态切换)](#5.3 信号处理流程(内核态↔用户态切换))
- [5.4 穿插话题 - 操作系统是怎么运行的?](#5.4 穿插话题 - 操作系统是怎么运行的?)
-
- [5.4.1 硬件中断](#5.4.1 硬件中断)
- [5.4.2 时钟中断](#5.4.2 时钟中断)
- [5.4.3 死循环](#5.4.3 死循环)
- [5.4.4 软中断(系统调用)](#5.4.4 软中断(系统调用))
- [5.4.5 缺页中断与异常](#5.4.5 缺页中断与异常)
- [5.5 如何理解内核态和用户态](#5.5 如何理解内核态和用户态)
-
- [5.5.1 特权级别 (Ring 0 & Ring 3)](#5.5.1 特权级别 (Ring 0 & Ring 3))
- [5.5.2 虚拟地址空间划分 (32位为例)](#5.5.2 虚拟地址空间划分 (32位为例))
- [5.5.3 用户态与内核态的切换流程](#5.5.3 用户态与内核态的切换流程)
- [5.5.4 追踪 `fopen` 调用 (系统调用封装的体现)](#5.5.4 追踪
fopen调用 (系统调用封装的体现))
- 六、可重入函数与volatile
-
- [6.1 不可重入函数](#6.1 不可重入函数)
- [6.2 volatile关键字](#6.2 volatile关键字)
-
- [6.2.1 `volatile` 的局限性](#6.2.1
volatile的局限性)
- [6.2.1 `volatile` 的局限性](#6.2.1
- 七、SIGCHLD信号与僵尸进程回收
-
- [7.1 问题背景](#7.1 问题背景)
- [7.2 SIGCHLD机制](#7.2 SIGCHLD机制)
-
- [`waitpid()` 详解](#
waitpid()详解)
- [`waitpid()` 详解](#
- [7.3 SA_NOCLDSTOP 与 SIGCONT 的联动](#7.3 SA_NOCLDSTOP 与 SIGCONT 的联动)
- [7.4 Linux扩展:忽略SIGCHLD自动回收](#7.4 Linux扩展:忽略SIGCHLD自动回收)
- [7.5 查看子进程退出状态](#7.5 查看子进程退出状态)
- 八、常见易错点与最佳实践
-
- [8.1 易错点总结](#8.1 易错点总结)
- [8.2 最佳实践清单](#8.2 最佳实践清单)
- 九、总结
-
- [9.1 Linux进程信号知识点汇总表](#9.1 Linux进程信号知识点汇总表)
- [9.2 经典代码模板](#9.2 经典代码模板)
一、信号的基本概念
1.1 什么是信号
信号(Signal)是操作系统向进程发送的异步通知事件,属于软中断机制。当进程收到信号时,操作系统会中断当前执行流,转而执行预设的信号处理动作。
🚚 生活类比:你在网上买了快递,快递员打电话通知你(信号产生),你正在打游戏不能立刻取(不立即处理),但你记下了有快递到了(信号保存),等游戏结束再去取(合适时机处理)。整个通知过程是异步的------你无法精确预判快递员何时打电话。
1.2 信号的三大处理方式
每个信号都预先绑定了默认行为,但用户可以修改(除了SIGKILL和SIGSTOP):
| 处理方式 | 说明 | 示例 |
|---|---|---|
| 默认动作 (SIG_DFL) | 系统预定义行为:终止、忽略、暂停、Core Dump | SIGINT默认终止进程 |
| 忽略 (SIG_IGN) | 收到信号后直接丢弃,不执行任何操作 | signal(SIGINT, SIG_IGN) |
| 自定义捕捉 | 注册回调函数,收到信号时执行用户代码 | signal(SIGINT, handler) |
⚠️ 特权信号 :
SIGKILL(9)和SIGSTOP(19)既不能被忽略,也不能被捕获,更不能被阻塞------这是系统留给管理员最后的"核武器"。
1.3 常用信号速查表
| 信号宏 | 编号 | 触发场景 | 默认动作 |
|---|---|---|---|
| SIGHUP | 1 | 终端挂起或控制进程死亡 | Term |
| SIGINT | 2 | Ctrl+C | Term |
| SIGQUIT | 3 | Ctrl+\ | Core |
| SIGILL | 4 | 非法指令 | Core |
| SIGTRAP | 5 | 断点/调试陷阱 | Core |
| SIGABRT | 6 | abort() 调用 | Core |
| SIGFPE | 8 | 除零、浮点溢出 | Core |
| SIGKILL | 9 | kill -9(不可捕获) | Term |
| SIGSEGV | 11 | 段错误(野指针) | Core |
| SIGPIPE | 13 | 向关闭的管道写数据 | Term |
| SIGALRM | 14 | alarm() 超时 | Term |
| SIGTERM | 15 | kill默认信号(优雅终止) | Term |
| SIGCHLD | 17 | 子进程状态变化 | Ignore |
| SIGCONT | 18 | 继续暂停的进程 | Cont |
| SIGSTOP | 19 | Ctrl+Z(不可捕获) | Stop |
Core vs Term:Core表示进程崩溃时会生成core dump文件(内存镜像),可用于gdb事后调试;Term仅表示正常终止。
1.4 标准信号 vs 实时信号
Linux 共有 64 个信号(1-64),分为两类:
| 特性 | 标准信号(1-31) | 实时信号(34-64) |
|---|---|---|
| 排队机制 | 不排队,多个相同信号可能合并为 1 个 | 排队,每个信号都独立保存 |
| 传递顺序 | 无保证 | 优先级:编号小的先递达 |
| 附带数据 | 不能携带额外数据 | 可通过 sigqueue() 附带 union sigval 数据 |
| 用途 | 系统预定义含义 | 应用程序自定义 |
cpp
// 实时信号发送(可附带数据)
union sigval value;
value.sival_int = 42;
sigqueue(pid, SIGRTMIN, value); // 需要包含 <signal.h>
// 接收时取出数据(需使用 SA_SIGINFO)
void handler(int sig, siginfo_t *info, void *ctx) {
int data = info->si_value.sival_int; // 取出附带数据
}
1.5 可靠信号与不可靠信号
这是理解信号行为的核心概念,直接影响程序的健壮性。
| 特性 | 不可靠信号 | 可靠信号 |
|---|---|---|
| 信号范围 | 1~31(标准信号) | 34~64(实时信号) |
| 排队行为 | 不排队。同一信号多次产生,pending位图只记录1次(可能丢失) | 排队。每次产生都会独立记录,不会丢失 |
| 历史来源 | 早期Unix只有pending位图,无法计数 | POSIX.1b扩展,改用队列结构 |
| 典型问题 | 快速连续发送3次SIGINT,进程可能只收到1次 | 快速连续发送3次SIGRTMIN,进程收到3次 |
工程启示:
- 对于关键通知(如进程退出指令),不应依赖标准信号的"多发必达"
- 若需要可靠通知,使用实时信号 +
sigqueue() - 在标准信号处理中,应采用容错设计(如循环检查、标志位 + 轮询)
二、信号的完整生命周期
信号从产生到处理,经历三个核心阶段:
产生 → 保存(未决)→ 处理(递达)
- 产生 :触发条件生成信号,内核将其记录到目标进程的
task_struct中 - 保存(未决Pending) :信号已抵达但尚未处理,内核用pending位图 标记(1 bit/信号)。对于实时信号,内核使用队列存储
- 处理(递达Delivery):进程从内核态返回用户态前,内核检查pending位图,执行对应处理函数
📌 关键理解:信号不是立即处理的!进程可能正在执行更高优先级的任务(如内核态操作),所以信号先被"保存",等"合适时机"再处理。
信号生命周期示意图:

三、信号的产生方式
3.1 终端按键产生(仅限前台进程)
| 按键 | 信号 | 作用 |
|---|---|---|
Ctrl+C |
SIGINT(2) | 中断前台进程 |
Ctrl+\ |
SIGQUIT(3) | 退出并生成core文件 |
Ctrl+Z |
SIGSTOP(19) | 暂停前台进程(挂起) |
bash
# 前台运行程序
./a.out
# 后台运行(无法接收键盘信号)
./a.out &
# 查看后台任务
jobs
# 将后台任务切到前台
fg %1
# 让暂停的任务后台继续运行
bg %1
键盘硬件中断/CPU/OS 处理流程图:

3.2 硬件异常产生
当CPU或MMU检测到异常时,内核会向当前进程发送相应信号:
cpp
// 除零错误 → SIGFPE(8)
int a = 10;
a /= 0; // 触发SIGFPE
// 野指针 → SIGSEGV(11)
int *p = nullptr;
*p = 100; // 触发SIGSEGV
原理:CPU内部有状态寄存器(如溢出标志),当除零发生时,CPU触发陷阱(trap),进入内核异常处理程序,内核识别异常类型后向当前进程的pending位图写入对应信号。
3.3 系统调用/函数产生
3.3.1 kill() --- 向任意进程发送信号
c
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数详解:
| 参数 | 说明 |
|---|---|
pid > 0 |
信号发送给进程ID为pid的进程 |
pid == 0 |
信号发送给与调用进程属于同一进程组的所有进程(包括调用进程自身) |
pid == -1 |
信号发送给调用进程有权发送信号的所有进程(除init(PID=1)和自身外,需有权限) |
pid < -1 |
信号发送给**进程组ID等于 |
sig |
要发送的信号编号。若sig==0,则不发送信号,仅用于检测进程是否存在(空信号) |
返回值 :成功返回 0;失败返回 -1,并设置 errno(常见错误:ESRCH进程不存在、EPERM权限不足)。
cpp
// 示例:实现自己的kill命令
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
int main(int argc, char *argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " -signum pid" << std::endl;
return 1;
}
int signum = std::stoi(argv[1] + 1); // 去掉'-'前缀
pid_t pid = std::stoi(argv[2]);
if (kill(pid, signum) == -1) {
perror("kill failed");
return 1;
}
return 0;
}
3.3.2 raise() --- 给当前进程发信号
c
#include <signal.h>
int raise(int sig);
参数 :sig --- 要发送的信号编号。
返回值:成功返回 0;失败返回非零值(注意:不是 -1,而是非零)。
raise(sig)等价于kill(getpid(), sig)。
3.3.3 abort() --- 异常终止(给自己发SIGABRT)
c
#include <stdlib.h>
void abort(void);
说明:
- 向调用进程发送
SIGABRT(6)信号 - 该函数从不返回(除非捕获SIGABRT且处理函数不退出)
- 若SIGABRT被捕获且处理函数返回,
abort()仍会再次发送SIGABRT并终止进程(保证最终退出) - 会刷新stdio缓冲区并关闭流(未捕获时)
cpp
// abort 的典型行为
void handler(int sig) {
// 做一些清理...
// 即使返回,abort() 仍会终止进程
}
signal(SIGABRT, handler);
abort(); // 永远不会返回
3.4 软件条件产生
3.4.1 alarm() --- 闹钟信号
c
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数 :seconds --- 定时秒数。若为0,则取消当前闹钟。
返回值:
- 返回上一次闹钟剩余的秒数(无论新设置是否成功)
- 若之前没有闹钟,返回0
重要特性:
- 一个进程同一时间只能有一个闹钟,重复调用会覆盖上一次
- 闹钟超时后,内核向当前进程发送
SIGALRM(14)信号,默认终止进程 - 闹钟超时是一次性的,不会重复触发
返回值的使用场景:
cpp
// 场景:先设置10秒闹钟,3秒后修改为5秒闹钟
alarm(10); // 设置10秒倒计时
sleep(3); // 3秒后...
int remain = alarm(5); // 重置为5秒,remain = 7(10-3)
// 此时闹钟还有5秒
// 场景:取消闹钟并获取剩余时间
int remain = alarm(0); // remain = 上一次闹钟剩余秒数,同时闹钟被取消
// 这在实现"限时操作但允许提前完成"时很有用
3.4.2 SIGPIPE --- 管道断裂
当管道的读端关闭,写端继续调用write()时,内核会向写进程发送SIGPIPE(13),默认终止进程。这个信号在网络编程 和管道通信中非常常见。
典型场景 :客户端向已关闭的 socket 连接执行 send()/write()。
解决方案(三选一):
cpp
// 方法1:忽略 SIGPIPE,通过 write 返回 -1 和 errno=EPIPE 处理
signal(SIGPIPE, SIG_IGN);
// 方法2:设置 send 的 flags 为 MSG_NOSIGNAL(仅限Linux socket)
send(fd, buf, len, MSG_NOSIGNAL);
// 方法3:捕获信号,自行处理(可记录日志)
void pipe_handler(int sig) {
// 记录错误,然后可以忽略或优雅关闭
}
signal(SIGPIPE, pipe_handler);
推荐做法 :对于服务器程序,通常使用方法1(全局忽略),然后依据 write 的返回值进行错误处理。
3.5 命令行发送信号
bash
kill -9 1234 # 发送SIGKILL强制杀死
kill -15 1234 # 发送SIGTERM优雅终止(默认)
kill -STOP 1234 # 发送SIGSTOP暂停
kill -CONT 1234 # 发送SIGCONT继续
kill -SIGSEGV 1234 # 发送段错误信号(模拟崩溃)
四、信号的保存(Pending与Block)
4.1 内核数据结构
每个进程的task_struct(PCB)中维护了两个关键位图和一个处理函数表:
┌─────────────────────────────────────────┐
│ task_struct (PCB) │
├─────────────────────────────────────────┤
│ pending 位图 │ block 位图 │ handler[] │
│ (未决信号) │ (阻塞掩码) │ (处理动作) │
└─────────────────────────────────────────┘
- pending位图 :记录哪些信号已经产生但尚未递达。bit=1表示该信号处于未决状态。对于实时信号(34~64),内核使用额外的队列来保证排队。
- block位图(信号屏蔽字):记录哪些信号被阻塞。bit=1表示该信号被阻塞,即使产生也不会递达,直到解除阻塞。
- handler数组:存储每个信号的处理函数指针(SIG_DFL、SIG_IGN或自定义函数)。
⚠️ 阻塞 ≠ 忽略:忽略是信号递达后直接丢弃;阻塞是信号暂时不能递达,依然保存在pending中,解除阻塞后才会递达。
内核数据结构示意图 task_struct 指向 pending/block/handler:

4.2 信号集操作函数
sigset_t 是信号集合类型,使用前必须初始化。所有操作函数返回 0 表示成功,-1 表示失败(并设置 errno),sigismember 返回 1/0/-1。
c
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空集合
int sigfillset(sigset_t *set); // 填满所有信号(包括实时信号)
int sigaddset(sigset_t *set, int signo); // 添加指定信号
int sigdelset(sigset_t *set, int signo); // 删除指定信号
int sigismember(const sigset_t *set, int signo); // 判断:1在集合中,0不在,-1错误
4.3 修改阻塞掩码:sigprocmask
c
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:修改方式SIG_BLOCK:将set中的信号添加到 当前阻塞集(block |= set)SIG_UNBLOCK:将set中的信号从 当前阻塞集中移除(block &= ~set)SIG_SETMASK:直接将阻塞集设置为set(block = set)
set:新设置的信号集(若为NULL,则不修改,仅通过oldset获取当前值)oldset:非空时,返回旧的阻塞集(用于事后恢复)
返回值:成功返回 0;失败返回 -1 并设置 errno。
⚠️ 若调用
sigprocmask解除了某些信号的阻塞,且这些信号正处于pending状态,那么在返回前会至少递达其中一个信号。
4.4 获取pending信号集:sigpending
c
int sigpending(sigset_t *set);
- 参数 :
set输出参数,返回当前进程的未决信号集 - 返回值:成功返回 0;失败返回 -1
4.5 完整示例:阻塞2号信号并观察pending变化
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t &pending) {
std::cout << "pending: ";
for (int signo = 31; signo >= 1; signo--) {
std::cout << (sigismember(&pending, signo) ? "1" : "0");
}
std::cout << std::endl;
}
void handler(int signo) {
std::cout << "捕获到信号 " << signo << ",正在处理..." << std::endl;
}
int main() {
// 1. 自定义捕捉2号信号
signal(SIGINT, handler);
// 2. 设置阻塞集,阻塞2号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, &old_set);
std::cout << "已屏蔽SIGINT(Ctrl+C),请尝试按Ctrl+C..." << std::endl;
int cnt = 10;
while (cnt--) {
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
if (cnt == 5) {
std::cout << "5秒后解除屏蔽..." << std::endl;
sigprocmask(SIG_SETMASK, &old_set, nullptr);
}
sleep(1);
}
return 0;
}
运行效果:前5秒按Ctrl+C,pending位图中2号信号对应的bit会变为1;解除屏蔽后,pending位图清零,并执行自定义handler。
五、信号的捕捉与处理
5.1 信号捕捉函数
5.1.1 signal()(简单但不可靠)
c
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:
signum:信号编号handler:可以是SIG_IGN(忽略)、SIG_DFL(默认)或自定义函数指针
返回值 :成功返回之前的 信号处理函数指针(或SIG_ERR),失败返回 SIG_ERR 并设置 errno。
⚠️
signal()在不同Unix系统上行为不一致(有的系统在handler执行后会自动重置为默认),且缺乏sa_mask等精细控制,推荐使用sigaction()。
cpp
// 示例:捕获SIGINT,不退出程序
#include <iostream>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
std::cout << "收到SIGINT(" << sig << "),但我不退出!" << std::endl;
}
int main() {
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("signal");
return 1;
}
while (true) {
std::cout << "Running... pid=" << getpid() << std::endl;
sleep(1);
}
return 0;
}
5.1.2 sigaction()(推荐,POSIX标准)
c
#include <signal.h>
struct sigaction {
void (*sa_handler)(int); // 简单处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数
sigset_t sa_mask; // 处理期间额外屏蔽的信号集
int sa_flags; // 行为标志
};
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
signum:要设置的信号编号(不可为 SIGKILL 或 SIGSTOP)act:新的配置(非NULL时)oldact:输出旧的配置(非NULL时)
返回值:成功返回 0,失败返回 -1 并设置 errno。
结构体成员详解:
| 成员 | 说明 |
|---|---|
sa_handler |
信号处理函数,取值 SIG_IGN, SIG_DFL 或自定义函数 |
sa_sigaction |
当 sa_flags 包含 SA_SIGINFO 时使用此字段,可接收更多信息(来源、数据等) |
sa_mask |
在执行信号处理函数期间 ,内核会额外阻塞 sa_mask 中的信号(加上当前信号本身) |
sa_flags |
见下表 |
sa_flags 常用选项:
| 标志 | 含义 |
|---|---|
SA_RESTART |
被信号中断的系统调用自动重启(如read、write、accept等) |
SA_NODEFER |
处理信号期间不自动阻塞当前信号(允许递归/嵌套) |
SA_SIGINFO |
使用 sa_sigaction 代替 sa_handler,可获得更多信息 |
SA_RESETHAND |
处理完信号后恢复默认动作(类似旧版signal的自动重置行为) |
SA_NOCLDSTOP |
若信号为 SIGCHLD,则子进程停止/继续时不产生信号(仅终止时产生) |
cpp
// sigaction 完整示例 + 参数/返回值说明
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
void handler(int sig) {
std::cout << "捕获信号: " << sig << std::endl;
sleep(3); // 模拟耗时处理
std::cout << "处理完毕" << std::endl;
}
int main() {
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 处理SIGINT时额外屏蔽SIGQUIT
act.sa_flags = SA_RESTART; // 自动重启被中断的系统调用
if (sigaction(SIGINT, &act, &oldact) == -1) {
perror("sigaction");
return 1;
}
// 可查看旧的处理方式
if (oldact.sa_handler != SIG_DFL) {
std::cout << "原处理方式不是默认" << std::endl;
}
while (true) {
std::cout << "主循环... pid=" << getpid() << std::endl;
sleep(1);
}
return 0;
}
5.1.3 信号嵌套与 sa_mask 的深入解释
当信号处理函数执行时,当前信号会被自动屏蔽 (防止递归触发)。但其他信号仍可打断当前处理函数:
主程序 ──> SIGINT handler 执行中
│
├──> SIGQUIT 到达
│ └──> 中断当前 handler,执行 SIGQUIT handler
│ └──> 返回继续执行 SIGINT handler
└──> SIGINT handler 继续
这会导致问题:如果两个 handler 操作了共享数据,就会出现不可重入问题。
sa_mask 的真正威力 :在处理 SIGINT 期间,内核会自动将 sa_mask 中指定的信号临时加入进程阻塞掩码,防止它们打断当前处理函数。
cpp
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 处理期间也屏蔽 SIGQUIT
act.sa_flags = 0;
sigaction(SIGINT, &act, nullptr);
若需要允许嵌套 (不推荐,除非明确需要),可设置 SA_NODEFER:
cpp
act.sa_flags = SA_NODEFER; // 当前信号也不屏蔽,允许递归
5.1.4 sigqueue() --- 发送带数据的信号(实时信号专用)
c
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
参数:
pid:目标进程ID(与kill相同规则)sig:要发送的信号编号(通常为实时信号 SIGRTMIN 至 SIGRTMAX)value:附带数据,类型为union sigval(可包含整型或指针)
c
union sigval {
int sival_int;
void *sival_ptr;
};
返回值:成功返回 0;失败返回 -1 并设置 errno(常见:EINVAL 信号无效、EPERM 权限不足等)。
特点 :信号会进入目标进程的队列(排队),并附带用户数据。
cpp
// 发送端
union sigval val;
val.sival_int = 42;
sigqueue(target_pid, SIGRTMIN+1, val);
5.2 暂停进程等待信号:pause() 与 sigsuspend()
5.2.1 pause() --- 简单挂起
c
#include <unistd.h>
int pause(void);
说明 :使调用进程挂起 (睡眠),直到捕获到一个信号。当信号处理函数返回后,pause() 返回 -1,并设置 errno 为 EINTR。
⚠️
pause()存在竞态条件 (见下文),推荐使用sigsuspend()。
cpp
// 配合alarm实现定时等待
alarm(5);
pause(); // 等待5秒,直到SIGALRM唤醒
5.2.2 sigsuspend() --- 原子挂起(解决竞态)
c
#include <signal.h>
int sigsuspend(const sigset_t *mask);
参数 :mask --- 临时使用的信号屏蔽字(调用期间替代当前屏蔽字)
说明 :sigsuspend() 执行以下原子操作:
- 将进程的信号屏蔽字设置为
mask - 挂起进程直到收到未被屏蔽的信号
- 信号处理函数返回后,恢复原来的屏蔽字,然后返回 -1(errno = EINTR)
为什么需要它? pause() 存在竞态窗口:
cpp
// 危险代码:信号可能在 sigprocmask 和 pause 之间到达
sigprocmask(SIG_UNBLOCK, &set, nullptr); // 解除阻塞
// ⚡ 若信号在这里到达并处理完毕
pause(); // 将永远挂起!
安全做法:
cpp
sigset_t new_mask, old_mask;
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigprocmask(SIG_BLOCK, &new_mask, &old_mask); // 阻塞 SIGINT
// 做一些需要被保护的操作...
// 原子地:将信号掩码替换为 old_mask 并挂起进程
sigsuspend(&old_mask);
sigsuspend 是编写可靠信号处理逻辑的基石,常用于等待特定信号的同时不丢失其他信号。
5.3 信号处理流程(内核态↔用户态切换)
用户态 main() ──┬──> 系统调用/异常/中断 ──> 内核态处理
│ │
│ ↓
│ 处理完成,准备返回用户态
│ │
│ 检查 pending & block
│ │
│ ┌─────────┴─────────┐
│ │ 有未阻塞的信号? │
│ └─────────┬─────────┘
│ yes │
│ ↓
│ ┌─────────────────┐
│ │ 执行信号处理动作 │
│ └────────┬────────┘
│ │
│ ┌──────────────┼──────────────┐
│ │ │ │
│ SIG_DFL SIG_IGN 自定义handler
│ │ │ │
│ 终止/暂停 丢弃 ┌─────┴─────┐
│ │ 切回用户态 │
│ │ 执行handler│
│ │ 返回内核 │
│ └─────┬─────┘
│ │
└───────────────────────────────────────────────┘
↓
恢复main函数上下文继续执行
关键点:
- 信号处理函数在用户态执行(安全原因)
- 处理函数返回后,通过特殊的
sigreturn系统调用再次进入内核,然后恢复原执行流
信号捕捉流程 用户态↔内核态切换示意图:

5.4 穿插话题 - 操作系统是怎么运行的?
说明:理解这部分内容,您将洞悉信号产生的底层硬件机制,以及为什么信号处理要从内核态切回用户态。
5.4.1 硬件中断
操作系统不需要轮询外设。外部设备(键盘、网卡等)就绪时,会通过中断控制器向CPU发送硬件中断信号。CPU接受到中断信号后,停止当前工作,根据中断号(如键盘对应IRQ1)去查询中断向量表(Interrupt Descriptor Table, IDT),找到对应的处理函数并执行。这是操作系统"活"起来的根本动力。
中断控制器与操作系统服务图:

5.4.2 时钟中断
操作系统自己不会被自动指挥,它需要时钟(Timer)来推动。在Linux 0.11内核中,调度程序初始化会设置时钟中断:
c
// 来自 Linux 0.11 源码
void sched_init(void) {
set_intr_gate(0x20, &timer_interrupt); // 设置时钟中断
outb(inb_p(0x21) & ~0x01, 0x21); // 允许时钟中断
}
每次时钟中断发生时,CPU会自动执行 timer_interrupt,进而调用 do_timer,最终触发 schedule() 函数进行进程切换与调度。换句话说,操作系统是靠硬件时钟的节拍来跑起来的!
5.4.3 死循环
操作系统本质上就是一个死循环!在 Linux 0.11 的 main.c 中:
c
void main(void) {
// ... 初始化代码 ...
for (;;) pause();
}
当没有其他任务需要运行时,CPU会一直执行 pause() 指令,直到被中断唤醒。
5.4.4 软中断(系统调用)
用户层程序无法直接调用内核功能。它通过汇编指令 int 0x80 或 syscall 触发软中断 ,陷入内核。
内核中有一张系统调用函数指针表 sys_call_table:
c
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, ... };
当用户调用系统函数(如 write),实际上会触发软中断,根据 eax 寄存器中的系统调用号(如 __NR_write),sys_call_table 会索引到对应函数执行。系统调用号就是数组下标!
5.4.5 缺页中断与异常
除零、野指针、缺页等都属于异常(也是软中断的一种)。内核在 trap_init 中设置了这些陷阱门:
c
void trap_init(void) {
set_trap_gate(0, ÷_error); // 除零异常
set_trap_gate(14, &page_fault); // 缺页异常
}
缺页异常发生时,内核会去分配物理内存并填充页表;除零/段错误发生时,内核会向当前进程发送 SIGFPE/SIGSEGV 信号来终止它。
5.5 如何理解内核态和用户态
说明:这些内容解释了为什么信号处理函数需要在用户态执行,以及系统调用切换的代价。
5.5.1 特权级别 (Ring 0 & Ring 3)
- Ring 0 (内核态):拥有最高权限,可以执行所有CPU指令,访问所有内存和外设。
- Ring 3 (用户态):权限最低,只能访问常规内存,无法直接读写硬件。
- Linux 仅使用 Ring 0 和 Ring 3。
5.5.2 虚拟地址空间划分 (32位为例)
- 用户空间 (0~3GB):每个进程独有,存储用户代码和数据。
- 内核空间 (3~4GB):所有进程共享,存储操作系统核心代码。这里的页表映射是所有进程共享的。
3G 用户态 / 1G 内核态 虚拟内存布局图:

5.5.3 用户态与内核态的切换流程
当用户程序需要 IO(读写文件、发送信号等)时,必须发生提权(从 Ring 3 到 Ring 0):
- 保存现场:用户态寄存器状态被保存。
- 切换栈 :从用户栈切换为内核栈。CPU 会从
TSS(任务状态段)中读取SS0和ESP0(内核栈基址和栈顶指针)来设置ss和esp寄存器。 - 执行内核代码:调用内核函数。
- 降权返回:内核函数执行完毕后,CPU 将特权级降回 Ring 3,恢复用户栈的现场,继续执行用户代码。
5.5.4 追踪 fopen 调用 (系统调用封装的体现)
在 glibc 源码追踪中:
c
_IO_new_fopen -> _fopen_internal -> _IO_file_open
-> __open (系统调用封装) -> INLINE_SYSCALL(open, 3, ...)
-> INTERNAL_SYSCALL_NCS( ... asm volatile ("syscall\n\t" ... ) )
标准C库将底层的 int 0x80 或 syscall 指令完全封装成了普通的函数调用,这就是为什么我们在写用户程序时看不到汇编指令的原因。
六、可重入函数与volatile
6.1 不可重入函数
当同一个函数被多个执行流(主程序和信号处理函数)同时调用,可能导致数据错乱,这种函数称为不可重入函数。
cpp
// 危险示例:在链表中插入节点时被信号打断
struct Node {
int data;
Node* next;
};
Node* head = nullptr;
void insert(int val) {
Node* new_node = new Node{val, nullptr};
// 第一步:new_node->next = head;
// 第二步:head = new_node;
// 如果在第一步和第二步之间收到信号,信号处理函数也调用insert
// 会导致链表断裂或内存泄漏
}
不可重入函数的特征:
- 使用了全局变量或静态变量
- 调用了
malloc/free(堆管理使用全局链表) - 调用了标准I/O函数(如
printf,内部有缓冲区) - 调用了其他不可重入函数
可重入函数只使用局部变量或参数,不修改共享数据。
✅ 信号处理函数中只能调用可重入函数 (异步信号安全函数),如
write、_exit,不能调用printf、malloc。完整的异步信号安全函数列表见man 7 signal-safety。
6.2 volatile关键字
当信号处理函数修改全局变量,而主循环检查该变量时,编译器可能优化导致变量被缓存到寄存器,内存中的新值无法被读取。
cpp
// 不加volatile可能出问题
int flag = 0; // 编译器可能优化,将其放到寄存器
void handler(int sig) {
flag = 1; // 修改内存中的flag
}
int main() {
signal(SIGINT, handler);
while (!flag); // 如果flag被缓存到寄存器,死循环
printf("quit\n");
return 0;
}
解决 :使用volatile关键字,强制每次从内存读取变量。
cpp
volatile int flag = 0; // 告诉编译器:不要优化,每次都从内存取值
6.2.1 volatile 的局限性
volatile 只保证内存可见性 ,不保证原子性 。对于需要原子读写的变量,不可过度依赖 volatile。
cpp
volatile int flag = 0; // 读写通常原子,OK
volatile long long counter = 0; // ❌ 64位变量在32位系统上读写可能不是原子的
最佳实践:
- 信号处理函数中,尽量使用
sig_atomic_t类型(C标准保证对该类型的读写是原子的,或尽可能原子) - 在 C++11 及以后,推荐使用
std::atomic<T>,兼顾原子性与可见性
cpp
#include <atomic>
std::atomic<int> flag{0}; // C++ 推荐做法
void handler(int sig) {
flag.store(1, std::memory_order_release);
}
int main() {
signal(SIGINT, handler);
while (!flag.load(std::memory_order_acquire));
return 0;
}
七、SIGCHLD信号与僵尸进程回收
7.1 问题背景
父进程fork子进程后,如果子进程先退出,会变成僵尸进程(Z状态),占用PCB资源。传统方案:
- 父进程
wait/waitpid阻塞等待(影响父进程工作效率) - 父进程轮询(非阻塞,但浪费CPU)
7.2 SIGCHLD机制
子进程在终止、暂停、恢复 时,内核会自动向父进程发送SIGCHLD(17)信号。父进程可以捕获该信号,在handler中异步回收子进程。
waitpid() 详解
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数:
pid:pid == -1:等待任意子进程pid > 0:等待进程ID为pid的子进程pid == 0:等待与调用进程同进程组的任意子进程pid < -1:等待进程组ID等于 |pid| 的任意子进程
status:输出子进程的退出状态(若为NULL则不保存)options:选项标志WNOHANG:如果没有子进程退出,立即返回0(非阻塞)WUNTRACED:也返回已停止(未继续)的子进程WCONTINUED:也返回已继续的子进程
返回值:
- 成功:返回退出的子进程PID
- 若设置了
WNOHANG且无子进程退出:返回0 - 失败:返回 -1,设置 errno
cpp
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
void sigchld_handler(int sig) {
// 重要:使用循环 + WNOHANG,一次性回收所有已退出的子进程
// 原因:多个子进程同时退出时,信号可能被合并(只触发一次handler)
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
std::cout << "回收子进程: " << pid << std::endl;
if (WIFEXITED(status)) {
std::cout << " 退出码: " << WEXITSTATUS(status) << std::endl;
} else if (WIFSIGNALED(status)) {
std::cout << " 被信号: " << WTERMSIG(status) << std::endl;
}
}
}
int main() {
signal(SIGCHLD, sigchld_handler);
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
// 子进程
std::cout << "子进程 " << getpid() << " 启动" << std::endl;
sleep(2);
exit(i + 1);
}
}
// 父进程继续做自己的事
while (true) {
std::cout << "父进程工作中... pid=" << getpid() << std::endl;
sleep(1);
}
return 0;
}
7.3 SA_NOCLDSTOP 与 SIGCONT 的联动
默认情况下,SIGCHLD 在子进程的终止、停止、继续 三种状态变化时都会产生。如果你只关心子进程退出,不关心暂停/恢复,应设置 SA_NOCLDSTOP。
三种情况对比:
| 子进程事件 | 默认SIGCHLD行为 | 设置 SA_NOCLDSTOP 后 |
|---|---|---|
| 子进程终止 (exit) | ✅ 发送SIGCHLD | ✅ 发送SIGCHLD |
| 子进程停止 (收到SIGSTOP/SIGTSTP) | ✅ 发送SIGCHLD | ❌ 不发送 |
| 子进程继续 (收到SIGCONT) | ✅ 发送SIGCHLD | ❌ 不发送 |
为什么要排除停止/继续?
cpp
// 没有 SA_NOCLDSTOP 的麻烦场景:
// 1. 父进程 fork 子进程
// 2. 用户按 Ctrl+Z → 子进程被 SIGTSTP 停止 → 父进程收到 SIGCHLD
// 3. handler 中 waitpid(WNOHANG) 无法回收(子进程没退出,只是暂停)
// 4. 用户执行 bg 让子进程继续 → 父进程又收到 SIGCHLD
// 结果:多次无意义的 handler 触发
// 正确做法:仅关心终止
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 只接收终止通知
sigaction(SIGCHLD, &sa, nullptr);
SIGCONT 的特殊性 :即使设置了 SA_NOCLDSTOP,当子进程因 SIGCONT 而继续时,父进程仍不会 收到 SIGCHLD。这是预期行为------SA_NOCLDSTOP 同时屏蔽了"停止"和"继续"两种通知。
7.4 Linux扩展:忽略SIGCHLD自动回收
在Linux上,可以将SIGCHLD设置为SIG_IGN,内核会自动回收子进程,不产生僵尸进程。
cpp
signal(SIGCHLD, SIG_IGN); // 设置后,子进程退出时内核自动回收
⚠️ 此方法并非所有Unix系统都支持,可移植性较差。跨平台代码请使用 waitpid 方案。
7.5 查看子进程退出状态
status 的低7位存储终止信号,第8位是core dump标志,高8位是退出码:
┌──────────────┬───┬─────────────┐
│ 退出码(8bit) │core│ 终止信号(7bit)│
└──────────────┴───┴─────────────┘
bit 15-8 bit7 bit 6-0
获取方式:
- 终止信号:
status & 0x7F - core dump标志:
(status >> 7) & 1 - 退出码:
(status >> 8) & 0xFF
也可使用宏:
WIFEXITED(status):子进程正常退出?若为真,WEXITSTATUS(status)返回退出码WIFSIGNALED(status):子进程被信号终止?若为真,WTERMSIG(status)返回信号编号WIFSTOPPED(status):子进程已停止?若为真,WSTOPSIG(status)返回停止信号WCOREDUMP(status):是否产生core dump
子进程退出状态位图/回收示意图:

八、常见易错点与最佳实践
8.1 易错点总结
| 易错点 | 说明 | 正确做法 |
|---|---|---|
| 信号处理函数中调用非可重入函数 | 如printf、malloc,可能导致死锁或数据错乱 | 只调用异步信号安全函数(write、_exit) |
| 忘记volatile导致死循环 | 编译器优化缓存全局变量 | 用volatile修饰,或用std::atomic |
| 多子进程退出时只回收一个 | SIGCHLD可能被合并,只触发一次handler | 使用while(waitpid(...WNOHANG)>0)循环回收 |
| 阻塞信号后忘记解除 | 导致特定信号永远无法递达 | 使用oldset备份,处理完后恢复 |
| 使用signal()注册信号 | 不同Unix行为不一致,且不安全 | 推荐使用sigaction() |
| 忽略SIGKILL/SIGSTOP | 这两个信号无法被忽略/捕获/阻塞 | 无解,系统保留特权 |
| alarm(0)误解 | 以为会立即触发闹钟 | alarm(0)用于取消当前闹钟,返回剩余秒数 |
| 信号处理函数破坏 errno | handler 中调用系统调用会覆盖全局 errno | 在 handler 入口保存 errno,出口恢复 |
| pause()的竞态条件 | 信号可能在解除阻塞和pause之间到达 | 使用sigsuspend()替代 |
| 未设置 SA_NOCLDSTOP | 子进程暂停/恢复也会触发SIGCHLD handler,造成无效调用 | 设置 SA_NOCLDSTOP,仅在子进程终止时通知 |
| 误用标准信号实现可靠通知 | 标准信号不排队,多次发送可能丢失 | 需要可靠通知时使用实时信号 + sigqueue |
errno 保存恢复模板:
cpp
void handler(int sig) {
int saved_errno = errno; // 入口保存
// ... 可能修改 errno 的操作(如 write、fork 等)
errno = saved_errno; // 出口恢复
}
8.2 最佳实践清单
✅ 信号处理函数原则:
- 尽可能简单,只设置标志位
- 调用
_exit()而不是exit()(exit不可重入) - 不要调用
printf、malloc等
✅ 使用sigaction代替signal,明确设置sa_mask和sa_flags
✅ 合理使用阻塞机制避免信号丢失和重入问题
✅ 回收子进程时循环调用waitpid + WNOHANG
✅ 在信号处理函数中保护 errno
✅ 使用sigsuspend替代pause以避免竞态条件
✅ 设置 SA_NOCLDSTOP 避免被 SIGCHLD 频繁中断
九、总结
9.1 Linux进程信号知识点汇总表
| 分类 | 子分类 | 具体内容 / 细节 |
|---|---|---|
| 概念 | 核心定义 | 异步通知机制(软中断) |
| 生命周期 | 产生 → 保存(pending) → 处理(delivery) | |
| 处理方式 | 1. 默认 (SIG_DFL) 2. 忽略 (SIG_IGN) 3. 自定义捕获 (Catch) | |
| 信号分类 | 标准信号 (1-31):不可靠,不排队 实时信号 (34-64):可靠,排队,可携带数据 | |
| 传递特性 | 可靠信号 :排队,不丢失 不可靠信号:不排队,相同信号合并 | |
| 信号产生 | 终端按键 | Ctrl+C (SIGINT) Ctrl+\ (SIGQUIT) Ctrl+Z (SIGSTOP) |
| 硬件异常 | 除零 SIGFPE 段错误 (野指针) SIGSEGV |
|
| 系统调用 | kill()、raise()、abort()、sigqueue() |
|
| 软件条件 | alarm() (SIGALRM) 管道断裂 (SIGPIPE) |
|
| 命令行 | kill -信号 PID |
|
| 信号保存 | 数据结构 | PCB中维护:pending位图(未决) + block位图(阻塞) |
| 操作函数 | sigemptyset、sigfillset、sigaddset、sigdelset、sigismember |
|
| 阻塞控制 | sigprocmask (修改阻塞集) |
|
| 获取未决集 | sigpending (获取pending集) |
|
| 信号捕捉 | 简易接口 | signal():简单但不可靠(行为因系统而异) |
| POSIX标准 | sigaction():推荐使用,支持 sa_mask、sa_flags、SA_SIGINFO |
|
| 数据传递 | sigqueue():发送带数据的信号(实时信号专用) |
|
| 防嵌套 | sa_mask:处理期间额外屏蔽信号,防止嵌套中断 |
|
| 原子挂起 | sigsuspend():原子操作,解决 pause() 的竞态条件 |
|
| 捕捉流程 | 用户态 → 内核态 → 检查pending → 返回用户态执行handler → 再次进入内核 | |
| 操作系统底层 | 硬件中断 | 外设触发,CPU查询中断向量表 (IDT) 处理 |
| 时钟中断 | 推动进程调度(如 Linux 0.11 中 timer_interrupt → do_timer → schedule) |
|
| 软中断 | int 0x80 / syscall:系统调用入口 (查 sys_call_table) |
|
| 异常 | 缺页、除零、段错误 (在 trap_init 中设置陷阱门) |
|
| 切换机制 | 内核态与用户态切换(特权级切换、栈切换、提权/降权) | |
| 进阶话题 | 函数安全性 | 可重入函数 vs 不可重入函数(信号处理函数只能调用异步信号安全函数) |
| 内存可见性 | volatile 的局限性与 sig_atomic_t / std::atomic |
|
| 子进程回收 | SIGCHLD与僵尸进程回收(waitpid详解、SA_NOCLDSTOP) |
|
| 工程实践 | SIGPIPE 处理(忽略、MSG_NOSIGNAL、捕获) |
|
| 易错与最佳实践 | 编码规范 | ✅ 信号处理函数中只调用异步信号安全函数 |
| 资源管理 | ✅ 子进程回收用 while 循环 + WNOHANG |
|
| 错误处理 | ✅ 在信号处理函数中保护 errno |
|
| 接口选择 | ✅ 优先使用 sigaction 和 sigsuspend |
|
| 配置优化 | ✅ 设置 SA_NOCLDSTOP 避免无效 SIGCHLD |
大综合架构图/虚拟内存图:

9.2 经典代码模板
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <atomic>
#include <cstring>
std::atomic<int> g_flag{0}; // 原子标志位
void sigint_handler(int sig) {
int saved_errno = errno;
g_flag.store(1);
errno = saved_errno;
}
void sigchld_handler(int sig) {
int saved_errno = errno;
while (waitpid(-1, nullptr, WNOHANG) > 0);
errno = saved_errno;
}
int main() {
// 设置 SIGINT 处理
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa, nullptr) == -1) {
perror("sigaction SIGINT");
return 1;
}
// 设置 SIGCHLD 处理(仅终止时通知)
struct sigaction sa_chld;
memset(&sa_chld, 0, sizeof(sa_chld));
sa_chld.sa_handler = sigchld_handler;
sigemptyset(&sa_chld.sa_mask);
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
if (sigaction(SIGCHLD, &sa_chld, nullptr) == -1) {
perror("sigaction SIGCHLD");
return 1;
}
// 创建子进程示例
for (int i = 0; i < 3; i++) {
if (fork() == 0) {
sleep(2);
_exit(0);
}
}
while (!g_flag.load()) {
std::cout << "Working... pid=" << getpid() << std::endl;
sleep(1);
}
std::cout << "Exiting gracefully." << std::endl;
return 0;
}
📖 进一步学习:
man 7 signal- 信号总览man 2 sigaction- sigaction详细文档man 7 signal-safety- 异步信号安全函数列表man 2 sigqueue- 带数据信号发送- Linux内核源码:
kernel/signal.c
希望通过本文,你能全面掌握Linux进程信号的原理与实践。如有疑问,欢迎评论区交流!