C 库函数 – sigaction()(手把手讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观
前言:信号处理的基石
在 C 语言编程中,信号(Signal)是操作系统与程序之间的重要通信机制。它们如同程序运行时的“紧急电话”,用于通知程序发生特定事件,例如用户按下 Ctrl+C 终止程序,或内存访问越界等异常情况。而 sigaction()
函数正是处理这类信号的核心工具。相较于传统的 signal()
函数,sigaction()
提供了更精细的控制和更可靠的信号处理能力,因此成为现代 C 程序员必须掌握的技能之一。本文将从信号的基础概念出发,逐步解析 sigaction()
的用法与原理,帮助读者在实际开发中灵活应用这一功能。
信号的基础概念:理解程序的“紧急电话”
什么是信号?
信号是操作系统向进程发送的异步事件通知。例如:
SIGINT
:用户按下 Ctrl+C 发送的中断信号。SIGSEGV
:程序访问非法内存地址时触发。SIGTERM
:系统要求程序终止的请求。
这些信号如同电话铃声,当程序接收到信号后,需要通过特定的“接电话逻辑”(即信号处理函数)来响应。如果不处理,系统会按默认方式处置,例如直接终止程序。
信号处理的挑战
直接使用 signal()
函数可能存在以下问题:
- 不可重入性:某些函数在信号处理期间不可调用,可能导致程序崩溃。
- 行为不确定性:在部分系统中,
signal()
可能会重置信号处理函数。 - 信号屏蔽:无法灵活控制信号的递达顺序。
sigaction()
函数通过结构体参数提供了对信号行为的全面控制,解决了上述问题。
sigaction() 函数详解:结构与参数解析
函数原型与结构体
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
该函数通过 struct sigaction
结构体定义信号行为,其关键字段如下:
字段名 | 作用说明 |
---|---|
sa_handler | 指向信号处理函数的指针,或预定义的 SIG_IGN (忽略信号)、SIG_DFL (默认行为) |
sa_sigaction | 当 SA_SIGINFO 标志启用时,指向扩展信号处理函数的指针 |
sa_mask | 在信号处理期间临时阻塞的信号集合 |
sa_flags | 控制信号行为的标志位集合 |
核心参数详解
-
sa_handler
与sa_sigaction
sa_handler
用于传统信号处理,仅接收信号编号参数。sa_sigaction
需配合SA_SIGINFO
标志,可接收额外的siginfo_t
结构体,包含信号来源等信息。
比喻:
将sa_handler
比作接电话时只记录来电号码,而sa_sigaction
则能获取来电者的详细信息(如姓名、来电时间)。 -
sa_mask
:信号屏蔽机制
当处理某个信号时,程序可能暂时阻塞其他信号的递达。例如,若处理SIGINT
时屏蔽SIGTERM
,可避免两者同时执行导致的竞态条件。 -
sa_flags
标志位
常用标志包括:SA_RESTART
:允许系统调用自动恢复,而非返回错误。SA_NODEFER
:在处理信号期间不自动屏蔽该信号。SA_SIGINFO
:启用sa_sigaction
的扩展功能。
使用示例:从简单到复杂场景
示例1:捕获 Ctrl+C 信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void interrupt_handler(int sig) {
printf("Received signal %d, program exiting...\n", sig);
exit(EXIT_SUCCESS);
}
int main() {
struct sigaction act;
act.sa_handler = interrupt_handler;
sigemptyset(&act.sa_mask); // 不屏蔽其他信号
act.sa_flags = 0;
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
while(1) {
printf("Running... Press Ctrl+C to exit.\n");
sleep(1);
}
return 0;
}
运行效果:
程序循环输出信息,按下 Ctrl+C 后触发 interrupt_handler
,程序优雅退出。
示例2:获取信号来源信息
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void detailed_handler(int sig, siginfo_t *info, void *context) {
printf("Signal %d received from PID %d, UID %d\n",
sig, info->si_pid, info->si_uid);
}
int main() {
struct sigaction act;
act.sa_sigaction = detailed_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO; // 启用扩展处理
if (sigaction(SIGUSR1, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
pause(); // 等待信号
return 0;
}
使用场景:
通过另一个终端发送信号:
kill -SIGUSR1 <PID>
程序将输出发送信号的进程和用户信息。
常见问题与注意事项
问题1:信号处理函数中能调用哪些函数?
在信号处理函数中,只能调用异步信号安全的函数(如 write()
、signal()
等)。若需执行复杂操作,可设置标志位后退出处理函数,再在主程序中响应。
问题2:如何避免信号丢失?
通过合理设置 sa_mask
和 sa_flags
,例如:
sigaddset(&act.sa_mask, SIGTERM); // 阻塞 SIGTERM 信号
act.sa_flags |= SA_RESTART; // 允许系统调用自动恢复
问题3:sigaction()
与 signal()
的区别?
- 功能范围:
sigaction()
支持更复杂的配置(如sa_mask
和sa_sigaction
)。 - 行为一致性:
sigaction()
在不同系统中的表现更可预测。 - 重置问题:
signal()
在某些系统中可能重置处理函数,导致信号无法再次触发。
进阶技巧:信号排队与实时信号
信号排队机制
当多个相同信号连续到达时,系统默认仅保留一个。若启用 SA_NODEFER
标志:
act.sa_flags |= SA_NODEFER; // 允许同一信号多次触发
此时,每个信号都会被独立处理,适用于需要记录所有触发次数的场景。
实时信号的应用
实时信号(如 SIGRTMIN
到 SIGRTMAX
)允许携带附加数据。通过 sigqueue()
函数发送带数据的信号:
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int main() {
int data = 42;
if (sigqueue(getpid(), SIGRTMIN, (union sigval){.sival_int = data}) == -1) {
perror("sigqueue");
}
// 处理函数通过 info->si_value 获取数据
return 0;
}
结论:掌握信号处理的核心工具
sigaction()
函数如同程序的“信号管理中枢”,通过灵活配置 struct sigaction
结构体,开发者能够精确控制信号的响应逻辑、屏蔽策略和行为模式。从基础的中断处理到复杂的多信号协作场景,这一函数提供了坚实的底层支持。建议读者通过实际编写示例代码加深理解,例如尝试实现一个能同时响应多个信号并记录日志的程序。掌握 sigaction()
不仅能提升代码的健壮性,更是迈向系统级编程的重要一步。