从函数使用到捕获函数规范,掌握 UNIX 信号处理的基础核心
一、核心认知:signal 函数的功能与定位
在 UNIX 信号处理中,signal 函数 是最基础、最常用的接口,定义在 <signal.h>
头文件中。其核心功能是"为指定信号设置处理动作"------通过该函数,进程可决定收到信号后的行为:执行系统默认动作、忽略信号,或调用自定义的信号捕获函数处理。
signal 函数是早期 UNIX 系统为简化信号处理设计的轻量级接口,虽存在兼容性和功能局限性,但因使用简单,至今仍是快速实现信号处理的首选工具(复杂场景需结合 sigaction
函数)。
二、signal 函数的完整解析:原型、参数与返回值
掌握 signal 函数的参数含义和返回值逻辑,是正确使用该函数的前提。其设计简洁但需注意细节(如参数取值范围、返回值判断)。
1. 函数原型
c
#include <signal.h>
// 功能:为信号 sig 设置处理动作,返回之前的处理动作
// 本质:修改内核中该信号的处理函数指针,实现信号行为的自定义
void (*signal(int sig, void (*f)(int)))(int);
// 简化理解:函数返回值为"参数是 int、返回值是 void"的函数指针
// 可通过 typedef 简化声明(推荐)
typedef void (*sig_handler_t)(int); // 定义信号处理函数类型
sig_handler_t signal(int sig, sig_handler_t f);
2. 核心参数解析
signal 函数有两个参数,分别控制"目标信号"和"处理动作",参数取值有严格规范:
参数 | 取值范围 | 功能说明 | 典型示例 |
---|---|---|---|
sig (信号编号) |
1~31(非可靠信号) | 系统预定义信号,如 SIGINT(2)、SIGTERM(15)、SIGUSR1(10),每个信号对应固定事件 | signal(SIGINT, ...) (处理 Ctrl+C 中断信号) |
sig (信号编号) |
34~64(可靠信号) | POSIX 标准扩展信号(SIGRTMIN~SIGRTMAX),支持信号排队,避免丢失 | signal(SIGRTMIN + 1, ...) (处理自定义可靠信号) |
sig (信号编号) |
无效值(如 0、65+) | 函数调用失败,返回 SIG_ERR,设置 errno 为 EINVAL | signal(0, ...) (无效信号,调用失败) |
f (处理动作) |
SIG_DFL |
Default:设置信号为系统默认处理动作(如 SIGINT 默认终止进程,SIGSEGV 默认终止并生成 core 文件) | signal(SIGINT, SIG_DFL) (恢复 Ctrl+C 默认终止行为) |
f (处理动作) |
SIG_IGN |
Ignore:设置信号为忽略动作,收到信号后内核直接丢弃,不影响进程执行 | signal(SIGTSTP, SIG_IGN) (忽略 Ctrl+Z 暂停信号) |
f (处理动作) |
自定义函数地址 | Catch:设置信号为捕获动作,收到信号时调用自定义函数(需符合 void (*)(int) 类型) |
signal(SIGUSR1, my_handler) (调用 my_handler 处理 SIGUSR1 信号) |
3. 返回值与错误处理
signal 函数的返回值是"之前的信号处理动作",需通过返回值判断调用是否成功,以及恢复原处理动作:
- 成功返回:返回调用前信号的处理动作(可能是 SIG_DFL、SIG_IGN 或之前注册的函数指针),可用于后续恢复原处理方式;
- 失败返回 :返回
SIG_ERR
(本质是 (void (*)(int))-1),同时设置 errno 标识错误原因,常见错误码:EINVAL
:信号编号无效(如 0、65+)或信号不可捕获(如 SIGKILL、SIGSTOP);EFAULT
:处理函数地址f
指向无效内存(如 NULL 或非法地址)。
实例:signal 函数返回值的使用(保存并恢复原处理动作)
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
// 自定义 SIGINT 处理函数
void temp_sigint_handler(int sig) {
printf("\n临时处理 SIGINT 信号,3 秒后恢复默认行为\n");
sleep(3);
// 恢复 SIGINT 默认处理动作
signal(SIGINT, SIG_DFL);
}
int main() {
sig_handler_t old_handler;
// 1. 保存 SIGINT 原处理动作,设置临时处理函数
old_handler = signal(SIGINT, temp_sigint_handler);
if (old_handler == SIG_ERR) {
perror("signal 设置临时处理函数失败");
return 1;
}
printf("已设置临时 SIGINT 处理函数,按下 Ctrl+C 测试(5 秒后自动恢复)\n");
sleep(5);
// 2. 手动恢复原处理动作(若 5 秒内未触发信号)
if (signal(SIGINT, old_handler) == SIG_ERR) {
perror("signal 恢复原处理函数失败");
return 1;
}
printf("已恢复 SIGINT 原处理动作,按下 Ctrl+C 会默认终止程序\n");
sleep(5);
return 0;
}
编译与运行
bash
gcc signal_return.c -o signal_return
./signal_return
输出示例
已设置临时 SIGINT 处理函数,按下 Ctrl+C 测试(5 秒后自动恢复)
^C
临时处理 SIGINT 信号,3 秒后恢复默认行为
已恢复 SIGINT 原处理动作,按下 Ctrl+C 会默认终止程序
^C
Terminated
关键价值:通过保存返回值,可在临时处理信号后恢复原动作(如临时忽略信号,处理完关键逻辑后恢复默认行为),确保程序行为的灵活性和正确性。
三、实战:signal 函数的三种核心使用方式
signal 函数的使用围绕"处理动作"展开,分为"默认处理""忽略信号""捕获信号"三种场景,每种场景对应不同的业务需求,以下通过实例详细演示。
方式 1:设置信号为默认处理(SIG_DFL)
适用场景:需恢复信号的系统默认行为(如之前修改过信号处理动作,后续需还原),或明确指定信号按默认逻辑处理。
实例:恢复 SIGINT 默认终止行为
代码格式化
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 临时忽略 SIGINT 的函数
void ignore_sigint() {
printf("临时忽略 SIGINT 信号(10 秒内 Ctrl+C 无效)\n");
signal(SIGINT, SIG_IGN);
sleep(10);
// 10 秒后恢复默认处理
signal(SIGINT, SIG_DFL);
printf("已恢复 SIGINT 默认行为,按下 Ctrl+C 终止程序\n");
}
int main() {
ignore_sigint();
// 恢复后等待信号
while (1) {
sleep(1);
}
return 0;
}
运行输出示例
临时忽略 SIGINT 信号(10 秒内 Ctrl+C 无效)
^C^C// 10 秒内按下 Ctrl+C 无反应
已恢复 SIGINT 默认行为,按下 Ctrl+C 终止程序
^C Terminated
方式 2:忽略信号(SIG_IGN)
适用场景:需屏蔽特定信号对进程的影响(如后台服务忽略 Ctrl+Z 暂停信号、忽略 SIGCHLD 避免僵死进程),确保进程不受干扰地执行。
实例:忽略 SIGTSTP(Ctrl+Z)和 SIGCHLD 信号
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
// 1. 忽略 SIGTSTP 信号(Ctrl+Z 无效)
if (signal(SIGTSTP, SIG_IGN) == SIG_ERR) {
perror("signal 忽略 SIGTSTP 失败");
return 1;
}
printf("已忽略 SIGTSTP 信号(Ctrl+Z 无效)\n");
// 2. 忽略 SIGCHLD 信号(部分系统自动回收子进程,避免僵死)
if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
perror("signal 忽略 SIGCHLD 失败");
return 1;
}
printf("已忽略 SIGCHLD 信号(子进程终止后自动回收)\n");
// 创建子进程,验证僵死预防
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败");
return 1;
}
if (pid == 0) {
printf("子进程 PID = %d,3 秒后终止\n", getpid());
sleep(3);
exit(EXIT_SUCCESS);
}
// 主进程不调用 wait,观察子进程是否僵死
printf("父进程 PID = %d,等待 10 秒(可通过 ps 查看子进程状态)\n", getpid());
sleep(10);
printf("父进程退出\n");
return 0;
}
运行输出:
已忽略 SIGTSTP 信号(Ctrl+Z 无效)
已忽略 SIGCHLD 信号(子进程终止后自动回收)
子进程 PID = 1235,3 秒后终止
父进程 PID = 1234,等待 10 秒(可通过 ps 查看子进程状态)
^Z // Ctrl+Z 无反应
父进程退出
验证 :子进程终止后,执行 ps aux | grep 1235 | grep -v grep
,无输出(子进程已被自动回收,未产生僵死进程)。
方式 3:捕获信号(自定义函数)
适用场景:需对信号进行自定义处理(如收到终止信号后清理资源、收到用户信号后执行特定任务),是 signal 函数最灵活的使用方式。
实例:捕获 SIGUSR1/SIGUSR2 信号并计数
代码示例
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
// 全局变量:统计信号接收次数(需用 volatile 确保不被编译器优化)
volatile int sigusr1_count = 0;
volatile int sigusr2_count = 0;
// 自定义信号捕获函数
void sig_handler(int sig) {
switch (sig) {
case SIGUSR1:
sigusr1_count++;
printf("捕获到 SIGUSR1 信号,累计次数:%d\n", sigusr1_count);
break;
case SIGUSR2:
sigusr2_count++;
printf("捕获到 SIGUSR2 信号,累计次数:%d\n", sigusr2_count);
break;
default:
printf("捕获到未知信号:%d\n", sig);
break;
}
}
int main() {
// 注册 SIGUSR1 和 SIGUSR2 捕获函数(共享同一个处理函数)
if (signal(SIGUSR1, sig_handler) == SIG_ERR || signal(SIGUSR2, sig_handler) == SIG_ERR) {
perror("signal 注册捕获函数失败");
return 1;
}
printf("程序启动,PID = %d\n", getpid());
printf("发送 SIGUSR1:kill -10 %d\n", getpid());
printf("发送 SIGUSR2:kill -12 %d\n", getpid());
printf("按下 Ctrl+C 终止程序\n");
// 主循环,持续运行等待信号
while (1) {
sleep(1);
}
return 0;
}
编译与运行步骤
-
编译程序
bashgcc signal_catch.c -o signal_catch
-
启动程序
bash./signal_catch
-
发送信号
在新终端执行以下命令(假设程序 PID 为 1234):
bashkill -10 $(pgrep signal_catch) # 发送 SIGUSR1 kill -12 $(pgrep signal_catch) # 发送 SIGUSR2 kill -10 $(pgrep signal_catch) # 再次发送 SIGUSR1
预期输出示例
程序启动,PID = 1234
发送 SIGUSR1:kill -10 1234
发送 SIGUSR2:kill -12 1234
按下 Ctrl+C 终止程序
捕获到 SIGUSR1 信号,累计次数:1
捕获到 SIGUSR2 信号,累计次数:1
捕获到 SIGUSR1 信号,累计次数:2
^C Terminated
三、信号捕获函数的编写规范与注意事项
自定义信号捕获函数是信号处理的核心,但因信号的异步特性,函数编写需遵循严格规范,否则易导致程序崩溃、数据错乱等问题。
1. 捕获函数的基础规范
捕获函数必须满足的语法规则:
- 函数签名固定 :必须符合
void (*handler)(int)
类型,即"参数为 int(信号编号)、返回值为 void",示例:
void my_handler(int sig) { ... }
; - 参数用途明确 :参数
sig
为当前收到的信号编号,可用于区分多个信号共享同一个处理函数(如前文示例中通过 switch 处理 SIGUSR1 和 SIGUSR2); - 避免返回值 :返回值为 void,不可返回任何数据,若需传递状态,需通过全局变量或静态变量(需加
volatile
修饰,避免编译器优化)。
2. 捕获函数的关键注意事项
-
严禁调用不可重入函数 :
信号捕获函数执行时可能中断主流程的任意操作,若调用"不可重入函数"(如
malloc
、free
、printf
、fopen
),会导致全局数据结构损坏(如malloc
维护的内存链表被中断后错乱)。可重入函数列表 (安全调用):
write
、close
、_exit
、sigaction
、memcpy
、memset
等(完整列表参考man 7 signal
);
不可重入函数列表 (禁止调用):malloc
、free
、printf
、fprintf
、strtok
、time
等。替代方案 :若需日志输出,用
write(STDOUT_FILENO, ...)
替代printf
;若需复杂逻辑,在捕获函数中仅设置"事件标记"(如volatile int flag = 1
),主流程轮询标记并执行逻辑。 -
全局变量需加 volatile 修饰 :
编译器会优化未被修改的全局变量(如缓存到寄存器),若捕获函数修改全局变量(如信号计数),主流程可能无法感知变化。需用
volatile
修饰变量,告知编译器"变量可能被异步修改,每次访问需从内存读取"。错误示例:
int sig_count = 0;
(无 volatile,主流程可能读取缓存值);正确示例:
volatile int sig_count = 0;
(有 volatile,确保数据同步)。 -
重新注册信号处理函数(兼容旧系统) :
部分旧 UNIX 系统(如 BSD)中,信号捕获函数执行一次后会自动恢复为默认动作(
SIG_DFL
),导致后续信号按默认方式处理(如首次捕获 SIGINT 后,再次按下 Ctrl+C 会终止进程)。兼容方案:在捕获函数开头重新注册自身,确保后续信号仍能被捕获,示例:
void sig_handler(int sig) { signal(sig, sig_handler); // 重新注册,兼容旧系统 printf("捕获到信号:%d\n", sig); }
-
避免耗时操作 :
捕获函数执行期间,进程会暂停原流程,若函数中存在
sleep
、循环等耗时操作,会导致主流程长时间阻塞。建议捕获函数仅执行"快速动作"(如设置标记、记录日志、清理资源),耗时逻辑移到主流程。
规范的捕获函数示例(安全处理信号)
代码示例
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// 全局事件标记(volatile 修饰)
volatile int exit_flag = 0;
volatile int rotate_flag = 0;
// 安全日志输出函数(用 write 替代 printf)
void safe_log(const char *msg) {
write(STDOUT_FILENO, msg, strlen(msg));
write(STDOUT_FILENO, "\n", 1);
}
// 规范的捕获函数
void sig_handler(int sig) {
// 1. 重新注册函数(兼容旧系统)
signal(sig, sig_handler);
// 2. 仅执行快速动作,设置标记
switch (sig) {
case SIGINT:
case SIGTERM:
safe_log("收到终止信号,设置退出标记");
exit_flag = 1;
break;
case SIGUSR1:
safe_log("收到日志轮转信号,设置轮转标记");
rotate_flag = 1;
break;
default:
char buf[64];
snprintf(buf, sizeof(buf), "收到未知信号:%d", sig);
safe_log(buf);
break;
}
}
// 主流程:轮询标记并执行复杂逻辑
void main_loop() {
while (1) {
// 检查退出标记
if (exit_flag) {
safe_log("主流程检测到退出标记,清理资源后退出");
// 执行资源清理(如关闭文件、释放内存)
sleep(2);
safe_log("资源清理完成,程序退出");
exit(EXIT_SUCCESS);
}
// 检查日志轮转标记
if (rotate_flag) {
safe_log("主流程检测到轮转标记,执行日志轮转");
// 执行日志轮转(如重命名旧日志)
sleep(1);
safe_log("日志轮转完成");
rotate_flag = 0; // 重置标记
}
// 主业务逻辑
safe_log("主业务逻辑执行中...");
sleep(3);
}
}
int main() {
// 注册信号捕获函数
if (signal(SIGINT, sig_handler) == SIG_ERR ||
signal(SIGTERM, sig_handler) == SIG_ERR ||
signal(SIGUSR1, sig_handler) == SIG_ERR) {
perror("signal 注册失败");
return 1;
}
safe_log("程序启动,PID = %d", getpid());
safe_log("发送终止信号:kill -2 %d 或 kill -15 %d", getpid(), getpid());
safe_log("发送轮转信号:kill -10 %d", getpid());
// 启动主流程
main_loop();
return 0;
}
运行示例
程序启动,PID = 1234
发送终止信号:kill -2 1234 或 kill -15 1234
发送轮转信号:kill -10 1234
主业务逻辑执行中...
主业务逻辑执行中...
收到日志轮转信号,设置轮转标记
主流程检测到轮转标记,执行日志轮转
日志轮转完成
主业务逻辑执行中...
收到终止信号,设置退出标记
主流程检测到退出标记,清理资源后退出
资源清理完成,程序退出
规范点 :捕获函数仅设置标记,主流程处理复杂逻辑;用 write
替代 printf
;全局变量加 volatile
;重新注册函数兼容旧系统,符合所有编写规范。
四、signal 函数的局限性与常见错误
signal 函数虽简单易用,但存在兼容性差、功能有限等局限性,且使用中易因参数错误、逻辑疏漏导致问题。
1. signal 函数的核心局限性
-
跨系统兼容性差 :
不同 UNIX 系统对 signal 函数的行为定义不一致:
- Linux 系统:捕获函数注册后持续有效,无需重新注册;
- BSD 系统:捕获函数执行一次后自动恢复为 SIG_DFL,需重新注册;
- Solaris 系统:部分信号(如 SIGALRM)的处理行为与其他系统差异较大。
这导致依赖 signal 函数的程序在不同系统中行为不一致,需额外适配。
-
不支持信号掩码与排队 :
signal 函数无法设置"信号掩码"(处理函数执行期间阻塞的其他信号),易导致信号嵌套触发(如处理函数执行时被其他信号中断);同时不支持可靠信号的排队机制,非可靠信号(1~31)短时间内多次发送会丢失。
-
无法获取信号附加信息 :
捕获函数仅能获取信号编号,无法获取信号的附加信息(如发送者 PID、信号携带的数据),无法满足进程间传递额外信息的需求(如分布式系统中的任务 ID 传递)。
-
不支持自动重启系统调用 :
当进程阻塞在可中断系统调用(如
read
、accept
)时,收到信号后系统调用会返回 -1,错误码为 EINTR,需手动重试;signal 函数无法配置"自动重启",需额外编写重试逻辑。
2. 使用 signal 函数的常见错误与解决方法
常见错误 | 问题现象 | 原因分析 | 解决方法 |
---|---|---|---|
信号编号错误 | signal 调用失败,返回 SIG_ERR,perror 提示"Invalid argument" | 1. 信号编号为 0 或超过系统最大信号编号(如 65+); 2. 信号不可捕获(如 SIGKILL=9、SIGSTOP=19),设置捕获函数时失败 | 1. 用 kill -l 查看系统支持的信号编号,确保参数有效; 2. 避免对 SIGKILL、SIGSTOP 调用 signal 函数(这两个信号不可捕获、不可忽略); 3. 示例:`if (sig < 1 |
捕获函数中调用不可重入函数 | 程序随机崩溃、日志错乱、内存泄漏,错误难以复现 | 捕获函数执行时中断主流程的不可重入函数(如 malloc),导致全局数据结构损坏(如内存链表断裂) | 1. 捕获函数中仅使用可重入函数(如 write、close); 2. 复杂逻辑(如日志、内存操作)移到主流程,捕获函数仅设置标记; 3. 用工具(如 valgrind)检测内存错误,定位不可重入函数调用 |
未检查返回值 | signal 调用失败未察觉,后续信号按默认方式处理(如预期捕获却终止进程) | 开发者默认 signal 调用成功,未判断返回值是否为 SIG_ERR,忽略了"信号编号无效""权限不足"等错误场景 | 1. 必须检查 signal 返回值,失败时打印错误信息并处理; 2. 示例: if (signal(SIGUSR1, handler) == SIG_ERR) { perror("signal 失败"); exit(1); } |
全局变量未加 volatile 修饰 | 捕获函数修改全局变量后,主流程读取的值未更新(如计数始终为 0) | 编译器优化未被修改的全局变量,将其缓存到寄存器,捕获函数异步修改内存中的变量后,主流程仍读取寄存器缓存值 | 1. 所有被捕获函数修改的全局/静态变量,均需加 volatile 修饰; 2. 示例:volatile int count = 0; (而非 int count = 0; ) |
系统调用未处理 EINTR 错误 | 进程阻塞在 read、accept 等系统调用时,收到信号后直接退出,未完成预期 I/O 操作 | 可中断系统调用被信号中断后返回 -1,错误码为 EINTR,未手动重试,导致 I/O 逻辑中断 | 1. 对可中断系统调用,检查错误码为 EINTR 时重试; 2. 示例: while ((n = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR); // 重试 3. 复杂场景改用 sigaction 函数,设置 SA_RESTART 标志自动重启系统调用 |
五、拓展:sigaction 函数------signal 的增强替代方案
为解决 signal 函数的局限性,POSIX 标准定义了 sigaction 函数,支持信号掩码、自动重启、可靠信号、附加信息获取等功能,是复杂场景下的首选接口。
1. sigaction 函数原型与核心参数
c
#include <signal.h>
// 功能:设置或获取信号处理动作,支持信号掩码、自动重启等增强功能
// 返回值:成功返回 0,失败返回 -1,设置 errno
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
// 信号处理动作结构体(核心参数)
struct sigaction {
void (*sa_handler)(int); // 基础捕获函数(同 signal 的 f 参数)
void (*sa_sigaction)(int, siginfo_t *, void *); // 增强捕获函数(获取附加信息)
sigset_t sa_mask; // 信号掩码:处理函数执行期间阻塞的信号
int sa_flags; // 功能标志(如自动重启、使用增强函数)
void (*sa_restorer)(void); // 已废弃,无需使用
};
// 增强捕获函数的 siginfo_t 结构体(存储信号附加信息)
typedef struct siginfo_t {
int si_signo; // 信号编号
pid_t si_pid; // 信号发送者 PID
uid_t si_uid; // 信号发送者 UID
void *si_addr; // 触发信号的内存地址(如 SIGSEGV 时的无效地址)
int si_value; // 信号携带的整数数据(自定义信号时传递)
// ... 其他字段(按需使用)
} siginfo_t;
2. sigaction 的核心优势与使用示例
优势 1:设置信号掩码,避免嵌套中断
通过 sa_mask
设置处理函数执行期间阻塞的信号,防止其他信号中断处理函数,示例:
代码示例
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sig_handler(int sig) {
printf("捕获到信号 %d,处理中(2 秒内阻塞其他信号)...\n", sig);
sleep(2); // 模拟处理耗时
printf("信号 %d 处理完成\n", sig);
}
int main() {
struct sigaction sa;
// 1. 初始化信号掩码:处理函数执行期间阻塞 SIGINT 和 SIGUSR1
sigemptyset(&sa.sa_mask); // 清空掩码
sigaddset(&sa.sa_mask, SIGINT); // 添加 SIGINT 到掩码
sigaddset(&sa.sa_mask, SIGUSR1); // 添加 SIGUSR1 到掩码
// 2. 设置处理函数和标志
sa.sa_handler = sig_handler;
sa.sa_flags = 0; // 无特殊标志
// 3. 注册信号
sigaction(SIGUSR1, &sa, NULL);
printf("程序启动,PID = %d\n", getpid());
printf("先发送 SIGUSR1,2 秒内发送 SIGINT 测试阻塞\n");
while (1) {
sleep(1);
}
return 0;
}
运行结果示例
程序启动,PID = 1234
先发送 SIGUSR1,2 秒内发送 SIGINT 测试阻塞
捕获到信号 10,处理中(2 秒内阻塞其他信号)...
^C// 2 秒内按下 Ctrl+C,被阻塞
信号 10 处理完成
Terminated// 处理完成后,SIGINT 生效,终止进程
优势 2:获取信号附加信息(如发送者 PID)
通过 sa_flags = SA_SIGINFO
使用增强捕获函数,获取信号发送者 PID、携带数据等信息,示例:
以下为规范格式后的代码内容,仅调整缩进和排版,未修改原内容:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
// 增强捕获函数(带附加信息)
void sig_info_handler(int sig, siginfo_t *info, void *context) {
printf("捕获到信号 %d\n", sig);
printf("发送者 PID:%d\n", info->si_pid); // 获取发送者 PID
printf("发送者 UID:%d\n", info->si_uid); // 获取发送者 UID
printf("信号携带数据:%d\n", info->si_value); // 获取携带数据
}
int main() {
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO; // 使用增强捕获函数
sa.sa_sigaction = sig_info_handler; // 设置增强函数
// 注册可靠信号(SIGRTMIN + 1)
sigaction(SIGRTMIN + 1, &sa, NULL);
printf("程序启动,PID = %d\n", getpid());
printf("发送信号并携带数据:kill -%d %d -o 123\n", SIGRTMIN + 1, getpid());
while (1) {
sleep(1);
}
return 0;
}
// 程序输出示例
程序启动,PID = 1234
发送信号并携带数据:kill -35 1234 -o 123
捕获到信号 35
发送者 PID:5678 // 发送者终端的 PID
发送者 UID:1000 // 发送者的 UID
信号携带数据:123 // 自定义携带数据
优势 3:自动重启系统调用
通过 sa_flags |= SA_RESTART
配置自动重启被中断的系统调用,避免手动重试,示例:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
void sig_handler(int sig) {
printf("捕获到信号 %d,处理完成\n", sig);
}
int main() {
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_handler = sig_handler;
sa.sa_flags = SA_RESTART; // 自动重启系统调用
sigaction(SIGUSR1, &sa, NULL);
// 打开文件,阻塞读取(测试系统调用重启)
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open 失败");
return 1;
}
char buf[128];
printf("阻塞读取文件(发送 kill -10 %d 测试重启)\n", getpid());
ssize_t n = read(fd, buf, sizeof(buf)); // 被信号中断后自动重启
if (n == -1) {
perror("read 失败");
return 1;
}
buf[n] = '\0';
printf("读取文件内容:%s\n", buf);
close(fd);
return 0;
}
阻塞读取文件(发送 kill -10 1234 测试重启) 捕获到信号 10,处理完成 读取文件内容:Hello from test.txt // read 被中断后自动重启,成功读取
UNIX 中 signal 函数的使用方法、信号捕获函数的编写规范,分析了 signal 函数的局限性与常见错误,并拓展了增强接口 sigaction 的使用。signal 函数是信号处理的基础,适合简单场景;复杂场景(如可靠信号、信号掩码、自动重启)需使用 sigaction 函数,确保程序的健壮性和跨平台兼容性。
编写信号捕获函数时,需严格遵循"禁用不可重入函数、全局变量加 volatile、避免耗时操作"等规范,避免异步特性导致的程序崩溃。同时,务必检查 signal/sigaction 的返回值,处理无效信号、不可捕获信号等错误场景,确保信号处理逻辑的正确性。