
在 Linux 信号的 “产生→保存→捕捉→处理” 全生命周期中,信号捕捉是最复杂、最核心,也是面试最高频的环节。它不仅涉及信号机制本身,还深度关联操作系统运行原理、内核态与用户态切换、函数调用栈管理等底层知识。 你是否曾有过这些困惑?:为什么
signal函数有时会 “失效”?sigaction到底强在哪里?信号处理函数明明是用户写的,为什么能在 “内核调度” 下执行?进程从 “正常执行” 到 “处理信号”,背后经历了怎样的态切换? 本文将以信号捕捉的完整流程为线索,无缝串联 “内核态 / 用户态” 核心概念、操作系统运行逻辑,再通过sigaction的全方位实战,带你从 “应用层使用” 穿透到 “内核层实现”。下面就让我们正式开始吧!
在深入流程之前,我们先明确 “信号捕捉” 的精准定义,以及它与 “默认处理”、“忽略处理” 的本质区别 —— 这是理解后续内容的前提。
信号的三种处理方式中,自定义捕捉(Catch) 是指:进程通过系统调用注册用户自定义处理函数,当信号递达时,操作系统不再执行默认动作,而是中断当前进程的正常执行流程,转而执行用户定义的处理函数,执行完毕后再恢复原流程。
用生活场景类比:你正在写代码(进程正常执行),突然收到 “快递上门” 的短信(信号递达)。你放下代码,去门口签收快递(执行信号处理函数),签收完成后回到电脑前,继续从刚才停下的地方写代码(恢复原流程)—— 这个 “中断→执行→恢复” 的过程,就是信号捕捉的核心逻辑。
处理方式 | 核心特征 | 执行主体 | 典型场景 |
|---|---|---|---|
默认处理(SIG_DFL) | 内核预设逻辑 | 内核 | Ctrl+C终止进程、段错误崩溃 |
忽略处理(SIG_IGN) | 递达后无操作 | 内核 | 忽略子进程终止信号SIGCHLD |
自定义捕捉(Catch) | 执行用户代码 | 内核调度 + 用户态执行 | 优雅退出、资源释放、热更新 |
关键结论:信号捕捉是 “内核态调度” 与 “用户态执行” 的结合体—— 调度权在操作系统内核,而执行的代码是用户编写的。这也是它比其他两种处理方式复杂的根本原因。
用户写的处理函数位于用户空间,而信号的检测、调度发生在内核空间。操作系统为了保证安全,严格划分了 “内核态” 和 “用户态”:
信号捕捉的过程,本质上就是进程在 “用户态” 与 “内核态” 之间反复切换的过程。要理解捕捉流程,必须先掌握 “操作系统如何运行”“内核态与用户态的区别”—— 这也是本文穿插核心话题的原因。
很多开发者只关注 “代码怎么写”,却忽略了 “代码怎么被操作系统执行”。信号捕捉的触发、调度、恢复,全部依赖操作系统的核心运行机制 ——进程调度与中断处理。
操作系统(OS)是硬件和应用程序之间的 “大管家”,核心职责有三个:
信号,本质上是一种软件中断—— 它和硬件中断一样,会打断进程的正常执行流程,触发预设的处理逻辑。
CPU 的执行速度极快,看似 “同时运行” 的多个进程,实际上是 OS 通过时间片轮转机制,让 CPU 轮流为每个进程服务:
信号捕捉的关键关联:信号的检测时机,就藏在 “时钟中断”“系统调用中断” 的处理过程中 ——OS 每次切换进程或处理中断时,都会检查当前进程的 PCB,看是否有 “未决且未阻塞” 的信号需要处理。
中断是 OS 感知外部事件、实现异步处理的核心机制,分为硬件中断和软件中断:
信号属于软件中断,其处理流程与硬件中断高度相似:
事件触发(如Ctrl+C)→ OS检测中断 → 保存当前进程上下文 → 执行中断处理逻辑(信号调度)→ 恢复进程上下文至此,我们可以得出一个核心结论:信号捕捉是操作系统 “中断处理机制” 在应用层的体现。没有中断,就没有信号的异步触发;没有进程调度,就没有信号处理函数的执行与恢复。
如果说 “中断” 是信号捕捉的 “触发器”,那么 “内核态与用户态的切换” 就是信号捕捉的 “核心通道”。所有信号捕捉的流程,都围绕这两种状态的切换展开。
进程的运行状态分为用户态(User Mode) 和内核态(Kernel Mode),二者的核心区别体现在权限和可访问的内存空间:
对比维度 | 用户态 | 内核态 |
|---|---|---|
权限等级 | 低权限(Ring 3) | 最高权限(Ring 0) |
内存访问 | 仅能访问用户空间(进程私有内存) | 可访问内核空间+ 所有用户空间 |
执行代码 | 用户编写的应用程序代码、库函数 | 操作系统内核代码(系统调用、中断处理) |
触发方式 | 进程启动后默认进入 | 执行系统调用、触发中断 / 异常时进入 |
Linux 系统的虚拟内存被划分为两部分:
关键规则:
进程不会无缘无故在用户态和内核态之间切换,只有满足特定条件时才会触发:
read、write、sleep、sigprocmask(用户态主动请求进入内核态);中断 / 系统调用处理完成后,OS 恢复进程的用户态执行上下文,CPU 切换回用户态,继续执行进程的正常代码。
信号捕捉的完整流程,包含两次 “用户态→内核态” 和两次 “内核态→用户态”,堪称 “态切换的教科书案例”。后续我们拆解捕捉流程时,会反复回到这个知识点。
终于来到本文的核心 —— 信号捕捉的全流程。我们以用户按下 Ctrl+C 触发 SIGINT 信号,进程执行自定义处理函数为例,将流程拆解为5 个关键阶段,结合 “态切换” 和 “上下文保存”,一步一步还原底层细节。

sigaction(或signal)注册SIGINT信号的自定义处理函数sig_int_handler;SIGINT信号,当前正在用户态执行正常业务代码(如for循环)。Ctrl+C,键盘产生硬件中断;SIGINT(2 号)信号;SIGINT信号标记为 “未决”;sleep系统调用),进程从用户态切换到内核态。OS 在内核态中完成以下操作:
sleep系统调用执行完毕;blocked(阻塞集)和pending(未决集);SIGINT未阻塞且未决,且进程为其注册了自定义处理函数,判定为 “需要执行信号捕捉”。这是信号捕捉最关键的一步 ——OS 需要 “篡改” 进程的执行流程,让其先执行处理函数,再恢复原流程。具体操作:
sig_int_handler的入口地址;sigreturn系统调用”(用于恢复原流程)。sig_int_handler函数;sig_int_handler执行完毕,触发 sigreturn系统调用(由编译器自动插入)。sigreturn系统调用触发,进程从用户态切换到内核态;SIGINT的未决标记清除;为了方便记忆,我们将上述流程简化为 “两进两出” 的态切换模型:

4.4 关键细节:为什么常规信号不支持排队?
结合捕捉流程,我们可以解释 “常规信号递达前多次产生,仅执行一次处理函数” 的原因:
这也是实时信号(34 号及以上)引入 “链表排队” 机制的核心原因。
了解了信号捕捉的底层流程,我们回到应用层 —— 如何注册自定义处理函数?Linux 提供了两个接口:signal和sigaction。
很多初学者习惯用signal,但在实际开发中,sigaction是唯一推荐的选择。为什么?我们先对比二者的差异,再通过实战拆解sigaction的核心功能。
对比维度 | signal(ANSI C 标准) | sigaction(POSIX 标准) |
|---|---|---|
可移植性 | 差(不同 Linux 发行版实现不同) | 强(POSIX 标准,全平台一致) |
功能完整性 | 弱(仅能注册处理函数) | 强(支持信号屏蔽、参数传递、行为控制) |
安全性 | 低(存在 “竞态条件”,可能丢失信号) | 高(支持原子操作,可避免竞态) |
信号屏蔽 | 不支持(无法控制捕捉期间的信号) | 支持(自动屏蔽当前信号,可自定义屏蔽集) |
核心结论:signal是sigaction的 “简化版”,底层其实是调用sigaction实现的,但阉割了大部分核心功能。生产环境中,禁止使用 signal,必须使用 sigaction。
sigaction函数用于查询或设置信号的处理动作,是 POSIX 标准定义的信号捕捉核心接口。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);SIGINT、SIGQUIT),不支持SIGKILL和SIGSTOP;struct sigaction结构体的指针,设置新的信号处理动作;若为NULL,则仅查询当前处理动作;struct sigaction结构体的指针,保存原来的信号处理动作;若为NULL,则不保存。errno(如EINVAL表示信号编号无效)。 struct sigaction是sigaction函数的核心,包含了信号处理的所有配置项,定义如下:
struct sigaction {
// 信号处理函数指针(核心)
void (*sa_handler)(int);
// 替代的处理函数指针(支持传递信号附加信息,本文暂不展开)
void (*sa_sigaction)(int, siginfo_t *, void *);
// 信号屏蔽集:捕捉该信号期间,自动屏蔽的信号集合
sigset_t sa_mask;
// 行为控制标志(位掩码,控制信号处理的细节)
int sa_flags;
// 预留字段(未使用)
void (*sa_restorer)(void);
}; 我们重点讲解4 个核心成员,这是掌握sigaction的关键:
与signal函数的处理函数完全一致,格式为void (*sa_handler)(int),接收一个参数(信号编号)。
有三种特殊取值:
SIG_DFL:执行默认动作;SIG_IGN:忽略信号; 这是sigaction最强大的功能之一:当进程执行该信号的处理函数时,OS 会自动将sa_mask中的信号加入进程的阻塞集;处理函数执行完毕后,OS 会自动恢复原来的阻塞集。
核心作用:避免信号的 “嵌套触发” 和 “竞态条件”。例如:
SIGINT信号,此时又收到一个SIGINT信号,若不屏蔽,会嵌套执行处理函数,导致栈溢出;sa_mask添加SIGINT,可保证 “同一信号的处理函数不会嵌套执行”。 sa_flags是位掩码,通过组合不同的标志,控制信号处理的行为。常用标志如下:
标志值 | 功能描述 |
|---|---|
SA_RESTART | 被信号中断的慢系统调用(如read、accept)会自动重启,而非返回错误 |
SA_NODEFER | 捕捉信号期间,不自动屏蔽当前信号(允许嵌套执行,慎用) |
SA_RESETHAND | 执行完处理函数后,自动将信号的处理动作恢复为默认动作(SIG_DFL) |
SA_SIGINFO | 使用sa_sigaction作为处理函数(支持传递信号附加信息) |
当sa_flags设置为SA_SIGINFO时,OS 会调用sa_sigaction而非sa_handler,该函数支持接收信号的附加信息(如信号产生的原因、发送进程的 PID 等),格式为:
void (*sa_sigaction)(int signum, siginfo_t *info, void *context); 理论终究要落地为代码。我们通过10 个递进式实战案例,从基础的信号捕捉,到高级的信号屏蔽、系统调用重启、原子操作,全方位掌握sigaction的使用。
需求:注册SIGINT信号的自定义处理函数,实现Ctrl+C不终止进程,而是打印提示信息。
// sigaction_basic.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 自定义信号处理函数
void sig_int_handler(int signum)
{
cout << "\n[捕捉成功] 收到SIGINT信号(编号:" << signum << "),进程不终止!" << endl;
}
int main()
{
cout << "进程PID:" << getpid() << ",等待SIGINT信号(按下Ctrl+C测试)..." << endl;
struct sigaction act;
// 1. 初始化结构体(必须!避免脏数据)
// 方式1:手动置0
// memset(&act, 0, sizeof(act));
// 方式2:使用sigemptyset初始化sa_mask
sigemptyset(&act.sa_mask);
// 2. 设置处理函数
act.sa_handler = sig_int_handler;
// 3. 设置行为标志:默认(0)
act.sa_flags = 0;
// 4. 注册信号处理动作
int ret = sigaction(SIGINT, &act, NULL);
if (ret == -1)
{
perror("sigaction failed");
return 1;
}
// 死循环,保持进程运行
while (true)
{
sleep(1);
cout << "进程正常运行中..." << endl;
}
return 0;
}编译运行:
g++ sigaction_basic.cpp -o sigaction_basic
./sigaction_basic 测试结果:按下Ctrl+C,进程不终止,打印自定义提示信息,证明sigaction成功注册处理函数。
需求:处理SIGINT信号期间,自动屏蔽SIGQUIT(Ctrl+\)信号,避免处理函数执行时被打断。
// sigaction_samask.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 自定义处理函数(故意设置5秒延时,模拟耗时操作)
void sig_int_handler(int signum)
{
cout << "\n[开始处理] SIGINT信号,耗时5秒..." << endl;
// 模拟耗时处理
for (int i = 0; i < 5; i++)
{
sleep(1);
cout << "处理中:第" << i+1 << "秒" << endl;
}
cout << "[处理完成] SIGINT信号" << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
cout << "测试步骤:1. 按下Ctrl+C触发SIGINT;2. 处理期间按下Ctrl+\\触发SIGQUIT" << endl;
struct sigaction act;
sigemptyset(&act.sa_mask);
// 核心:向sa_mask中添加SIGQUIT,处理SIGINT期间自动屏蔽SIGQUIT
sigaddset(&act.sa_mask, SIGQUIT);
// 额外:屏蔽自身,避免嵌套触发(sa_flags=0时,OS会自动屏蔽当前信号,此处为显式演示)
sigaddset(&act.sa_mask, SIGINT);
act.sa_handler = sig_int_handler;
act.sa_flags = 0;
// 注册SIGINT和SIGQUIT(SIGQUIT使用默认处理)
sigaction(SIGINT, &act, NULL);
// 注册SIGQUIT的默认处理(确保触发时终止进程)
struct sigaction quit_act;
sigemptyset(&quit_act.sa_mask);
quit_act.sa_handler = SIG_DFL;
quit_act.sa_flags = 0;
sigaction(SIGQUIT, &quit_act, NULL);
while (true)
{
sleep(1);
cout << "进程正常运行中..." << endl;
}
return 0;
}编译运行:
g++ sigaction_samask.cpp -o sigaction_samask
./sigaction_samask测试结果:
Ctrl+C,进入SIGINT处理函数,开始 5 秒倒计时;Ctrl+\,无任何反应(SIGQUIT被屏蔽);SIGINT处理完成,SIGQUIT立即递达,进程终止(执行默认动作)。 核心结论:sa_mask实现了 “捕捉期间的临时屏蔽”,是解决信号嵌套、竞态的关键。
背景:当进程执行慢系统调用(如read、accept、recv)时,若收到信号,系统调用会被中断,返回-1并设置errno=EINTR。这会导致程序逻辑出错。
需求:使用SA_RESTART标志,让被SIGINT中断的read系统调用自动重启。
// sigaction_sarestart.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <cstring>
using namespace std;
void sig_int_handler(int signum)
{
cout << "\n捕捉到SIGINT信号,read系统调用将自动重启!" << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
cout << "测试:输入字符前按下Ctrl+C,观察read是否重启" << endl;
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = sig_int_handler;
// 核心:设置SA_RESTART,让慢系统调用自动重启
act.sa_flags = SA_RESTART;
// 对比测试:注释上面一行,使用act.sa_flags = 0,观察效果
sigaction(SIGINT, &act, NULL);
char buf[1024];
cout << "请输入字符串:";
// 慢系统调用:从标准输入读取数据,若不输入,会一直阻塞
ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf)-1);
if (ret == -1)
{
if (errno == EINTR)
{
cerr << "read被信号中断,未重启!" << endl;
}
else
{
perror("read failed");
}
return 1;
}
buf[ret] = '\0';
cout << "你输入的内容:" << buf << endl;
return 0;
}编译运行:
g++ sigaction_sarestart.cpp -o sigaction_sarestart
./sigaction_sarestart测试步骤与结果:
Ctrl+C,打印捕捉信息,随后程序继续等待输入,输入字符后正常读取 —— 证明read自动重启;Ctrl+C后,read返回-1,程序打印 “read 被信号中断,未重启”—— 证明慢系统调用被中断。 需求:SIGINT信号的处理函数仅执行一次,第二次按下Ctrl+C时,进程执行默认动作(终止)。
// sigaction_saresethand.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sig_int_handler(int signum)
{
cout << "\n捕捉到SIGINT信号(仅执行一次)!" << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
cout << "测试:第一次按Ctrl+C执行处理函数,第二次按Ctrl+C终止进程" << endl;
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = sig_int_handler;
// 核心:设置SA_RESETHAND,执行后恢复默认动作
act.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &act, NULL);
while (true)
{
sleep(1);
cout << "进程正常运行中..." << endl;
}
return 0;
} 测试结果:第一次按下Ctrl+C,打印提示信息;第二次按下Ctrl+C,进程立即终止。
需求:使用sigaction的oldact参数,实现 “原子化” 替换信号处理动作,并在程序退出时恢复原动作(避免修改系统全局状态)。
// sigaction_atomic.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
using namespace std;
// 原信号处理动作(用于保存)
struct sigaction old_int_act, old_quit_act;
void sig_handler(int signum)
{
cout << "\n捕捉到信号:" << signum << "(" << (signum == SIGINT ? "SIGINT" : "SIGQUIT") << ")" << endl;
}
// 退出时恢复原信号动作
void cleanup()
{
sigaction(SIGINT, &old_int_act, NULL);
sigaction(SIGQUIT, &old_quit_act, NULL);
cout << "已恢复原信号处理动作,程序退出!" << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
atexit(cleanup); // 注册退出清理函数
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = sig_handler;
act.sa_flags = 0;
// 核心:原子化设置,并保存原动作
sigaction(SIGINT, &act, &old_int_act);
sigaction(SIGQUIT, &act, &old_quit_act);
cout << "已设置自定义信号处理动作,按下Ctrl+C或Ctrl+\\测试(30秒后自动退出)" << endl;
sleep(30);
return 0;
} 测试结果:程序运行期间,Ctrl+C和Ctrl+\执行自定义处理;30 秒后程序退出,自动恢复原信号动作(Ctrl+C恢复终止功能)。
需求:尝试用sigaction注册SIGKILL(9 号)信号的处理函数,验证其不可捕捉、不可忽略的特性。
// sigaction_kill.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sig_kill_handler(int signum)
{
cout << "捕捉到SIGKILL信号(这行代码永远不会执行)!" << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = sig_kill_handler;
act.sa_flags = 0;
// 尝试注册SIGKILL信号
int ret = sigaction(SIGKILL, &act, NULL);
if (ret == -1)
{
perror("sigaction SIGKILL failed");
cout << "结论:SIGKILL信号不可捕捉、不可忽略!" << endl;
}
cout << "请在另一个终端执行:kill -9 " << getpid() << " 测试" << endl;
sleep(20);
return 0;
} 测试结果:sigaction调用失败,打印sigaction SIGKILL failed: Invalid argument;执行kill -9 进程PID,进程立即终止。
需求:先阻塞SIGINT信号,10 秒后解除阻塞,让未决的SIGINT信号触发捕捉。
// sigaction_mask_comb.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sig_int_handler(int signum)
{
cout << "\n===== 信号递达 =====" << endl;
cout << "捕捉到SIGINT信号,执行处理函数" << endl;
cout << "====================" << endl;
}
// 打印未决信号集
void print_pending()
{
sigset_t pending;
sigpending(&pending);
cout << "[未决信号集]:";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&pending, i))
{
cout << i << " ";
}
}
cout << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
// 1. 注册SIGINT处理函数
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = sig_int_handler;
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
// 2. 阻塞SIGINT信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, &old_set);
cout << "已阻塞SIGINT信号,10秒内按下Ctrl+C测试..." << endl;
// 3. 每隔2秒打印一次未决信号集
for (int i = 0; i < 5; i++)
{
print_pending();
sleep(2);
}
// 4. 解除阻塞
sigprocmask(SIG_SETMASK, &old_set, NULL);
cout << "已解除阻塞,未决信号立即递达!" << endl;
while (true)
{
sleep(1);
cout << "进程正常运行中..." << endl;
}
return 0;
} 测试结果:阻塞期间按下Ctrl+C,未决信号集显示2;解除阻塞后,立即执行处理函数。
背景:信号处理函数可能在任意时刻被调用,若处理函数中调用了不可重入函数(如malloc、printf、strcpy),当主程序也在执行该函数时,会导致数据错乱、栈溢出。
需求:演示不可重入函数的危害,以及如何避免。
// sigaction_reentrant.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;
// 全局数组,模拟共享资源
char g_buf[1024];
// 不可重入函数:使用了全局变量,且包含多步操作
void unsafe_func(const char *str)
{
// 第一步:清空缓冲区
memset(g_buf, 0, sizeof(g_buf));
// 模拟耗时操作(增加信号中断的概率)
usleep(100000);
// 第二步:拷贝数据(若此时被信号中断,会导致数据不完整)
strcpy(g_buf, str);
cout << "unsafe_func执行完成,g_buf:" << g_buf << endl;
}
// 信号处理函数:调用了不可重入函数unsafe_func
void sig_int_handler(int signum)
{
cout << "\n信号处理函数:调用unsafe_func(信号)" << endl;
unsafe_func("SIGNAL_DATA");
}
int main()
{
cout << "进程PID:" << getpid() << endl;
cout << "测试:程序运行后,快速按下Ctrl+C,触发信号中断" << endl;
// 注册信号处理函数
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = sig_int_handler;
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
// 主程序循环调用unsafe_func
while (true)
{
cout << "主程序:调用unsafe_func(主程序)" << endl;
unsafe_func("MAIN_DATA");
sleep(1);
}
return 0;
} 测试结果:快速按下Ctrl+C时,会出现g_buf数据错乱(如显示SIGNAL_DATA或空字符串),证明不可重入函数的危害。
解决方案:
memcpy、strcmp、write);sa_mask屏蔽相关信号,避免并发访问。需求:重构案例 8,将信号处理逻辑移到主程序,信号处理函数仅设置全局标志,避免不可重入函数的调用。
// sigaction_reentrant_safe.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;
// 全局标志:标记是否收到SIGINT信号
volatile sig_atomic_t g_sigint_received = 0;
// 全局数组
char g_buf[1024];
// 安全的处理函数(在主程序中执行)
void safe_handle_sigint()
{
cout << "\n主程序处理SIGINT信号:" << endl;
unsafe_func("SIGNAL_DATA"); // 此时可安全调用,因为主程序单线程执行
g_sigint_received = 0; // 重置标志
}
// 不可重入函数(仅在主程序中调用)
void unsafe_func(const char *str)
{
memset(g_buf, 0, sizeof(g_buf));
usleep(100000);
strcpy(g_buf, str);
cout << "unsafe_func执行完成,g_buf:" << g_buf << endl;
}
// 信号处理函数:仅设置全局标志(原子操作)
void sig_int_handler(int signum)
{
g_sigint_received = 1;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = sig_int_handler;
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
while (true)
{
// 检查信号标志,若收到则处理
if (g_sigint_received)
{
safe_handle_sigint();
continue;
}
// 主程序业务逻辑
cout << "主程序:调用unsafe_func(主程序)" << endl;
unsafe_func("MAIN_DATA");
usleep(50000);
}
return 0;
} 测试结果:无论何时按下Ctrl+C,数据都不会错乱,因为信号处理函数仅执行原子操作,真正的业务逻辑在主程序中串行执行。
需求:实现一个后台服务程序,通过捕捉SIGINT和SIGTERM信号,完成 “资源释放→日志保存→优雅退出” 的流程。
// sigaction_graceful_exit.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cstdio>
using namespace std;
// 全局标志:标记是否需要退出
volatile sig_atomic_t g_quit = 0;
// 模拟资源句柄
FILE *g_log_file = NULL;
int g_socket_fd = 100; // 模拟套接字
// 信号处理函数:设置退出标志
void sig_quit_handler(int signum)
{
cout << "\n收到退出信号:" << (signum == SIGINT ? "SIGINT" : "SIGTERM") << endl;
g_quit = 1;
}
// 优雅退出函数:释放资源、保存日志
void graceful_exit()
{
cout << "\n开始优雅退出,释放资源..." << endl;
// 1. 关闭日志文件
if (g_log_file)
{
fprintf(g_log_file, "程序优雅退出,时间:%ld\n", time(NULL));
fclose(g_log_file);
cout << "日志文件已关闭" << endl;
}
// 2. 关闭套接字
cout << "套接字FD " << g_socket_fd << " 已关闭" << endl;
// 3. 打印退出信息
cout << "程序已优雅退出!" << endl;
}
int main()
{
cout << "===== 后台服务程序启动 =====" << endl;
cout << "进程PID:" << getpid() << endl;
cout << "发送 SIGINT(Ctrl+C)或 SIGTERM(kill 进程PID)可触发优雅退出" << endl;
// 初始化资源
g_log_file = fopen("service.log", "a");
if (!g_log_file)
{
perror("fopen failed");
return 1;
}
fprintf(g_log_file, "程序启动,时间:%ld\n", time(NULL));
cout << "日志文件已打开" << endl;
// 注册退出清理函数
atexit(graceful_exit);
// 注册SIGINT和SIGTERM信号
struct sigaction act;
sigemptyset(&act.sa_mask);
// 屏蔽SIGTERM,避免退出时被打断
sigaddset(&act.sa_mask, SIGTERM);
act.sa_handler = sig_quit_handler;
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
sigaction(SIGTERM, &act, NULL);
// 服务主循环
while (!g_quit)
{
// 模拟业务处理
cout << "服务正在运行,处理请求中..." << endl;
sleep(2);
}
cout << "主循环退出,准备执行清理操作..." << endl;
return 0;
}编译运行:
g++ sigaction_graceful_exit.cpp -o sigaction_graceful_exit
./sigaction_graceful_exit测试结果:
Ctrl+C或执行kill 进程PID,程序设置g_quit标志;graceful_exit函数,释放日志文件、套接字资源;service.log中记录启动和退出时间。sa_handler设置处理函数,sa_mask控制临时屏蔽,sa_flags控制行为,oldact保存原动作;signal,必须使用sigaction;信号处理函数尽量简洁,仅执行原子操作;通过全局标志将复杂逻辑移到主程序。信号捕捉是 Linux 开发中 “看似简单,实则深奥” 的知识点 —— 它不仅要求我们掌握应用层的接口使用,更需要理解操作系统的底层运行原理。 本文从 “前置认知→底层基石→核心流程→接口实战→面试总结” 五个维度,层层拆解了信号捕捉的全部核心内容。所有案例均在 Ubuntu 20.04 环境下验证通过,建议大家亲手编译运行,通过修改代码参数(如
sa_flags、sa_mask),深入理解每个配置项的作用。 信号机制的学习并未结束,后续我们还会探讨 “实时信号的排队机制”“信号与线程的交互”“sigqueue 的使用” 等进阶内容。关注我,持续解锁 Linux 内核与 C/C++ 开发的核心干货!