UNIX下C语言编程与实践38-UNIX 信号操作:signal 函数与信号捕获函数的编写
从函数使用到捕获函数规范,掌握 UNIX 信号处理的基础核心
一、核心认知:signal 函数的功能与定位
在 UNIX 信号处理中,signal 函数是最基础、最常用的接口,定义在 <signal.h>
头文件中。其核心功能是“为指定信号设置处理动作”——通过该函数,进程可决定收到信号后的行为:执行系统默认动作、忽略信号,或调用自定义的信号捕获函数处理。
signal 函数是早期 UNIX 系统为简化信号处理设计的轻量级接口,虽存在兼容性和功能局限性,但因使用简单,至今仍是快速实现信号处理的首选工具(复杂场景需结合 sigaction
函数)。
二、signal 函数的完整解析:原型、参数与返回值
掌握 signal 函数的参数含义和返回值逻辑,是正确使用该函数的前提。其设计简洁但需注意细节(如参数取值范围、返回值判断)。
1. 函数原型
#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 中断信号) |
34~64(可靠信号) | POSIX 标准扩展信号(SIGRTMIN~SIGRTMAX),支持信号排队,避免丢失 | signal(SIGRTMIN + 1, ...) (处理自定义可靠信号) | |
无效值(如 0、65+) | 函数调用失败,返回 SIG_ERR,设置 errno 为 EINVAL | signal(0, ...) (无效信号,调用失败) | |
f (处理动作) | SIG_DFL | Default:设置信号为系统默认处理动作(如 SIGINT 默认终止进程,SIGSEGV 默认终止并生成 core 文件) | signal(SIGINT, SIG_DFL) (恢复 Ctrl+C 默认终止行为) |
SIG_IGN | Ignore:设置信号为忽略动作,收到信号后内核直接丢弃,不影响进程执行 | signal(SIGTSTP, SIG_IGN) (忽略 Ctrl+Z 暂停信号) | |
自定义函数地址 | 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 函数返回值的使用(保存并恢复原处理动作)
#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;
}
编译与运行
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 默认终止行为
代码格式化
#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 信号
#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 信号并计数
代码示例
#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;
}
编译与运行步骤
编译程序
gcc signal_catch.c -o signal_catch
启动程序
./signal_catch
发送信号
在新终端执行以下命令(假设程序 PID 为 1234):kill -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
、循环等耗时操作,会导致主流程长时间阻塞。建议捕获函数仅执行“快速动作”(如设置标记、记录日志、清理资源),耗时逻辑移到主流程。
规范的捕获函数示例(安全处理信号)
代码示例
#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 || sig > SIGRTMAX) { fprintf(stderr, "无效信号编号\n"); } |
捕获函数中调用不可重入函数 | 程序随机崩溃、日志错乱、内存泄漏,错误难以复现 | 捕获函数执行时中断主流程的不可重入函数(如 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 函数原型与核心参数
#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; // 信号发送者 PIDuid_t si_uid; // 信号发送者 UIDvoid *si_addr; // 触发信号的内存地址(如 SIGSEGV 时的无效地址)int si_value; // 信号携带的整数数据(自定义信号时传递)// ... 其他字段(按需使用)
} siginfo_t;
2. sigaction 的核心优势与使用示例
优势 1:设置信号掩码,避免嵌套中断
通过 sa_mask
设置处理函数执行期间阻塞的信号,防止其他信号中断处理函数,示例:
代码示例
#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 和 SIGUSR1sigemptyset(&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、携带数据等信息,示例:
以下为规范格式后的代码内容,仅调整缩进和排版,未修改原内容:
#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); // 获取发送者 PIDprintf("发送者 UID:%d\n", info->si_uid); // 获取发送者 UIDprintf("信号携带数据:%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
配置自动重启被中断的系统调用,避免手动重试,示例:
#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 的返回值,处理无效信号、不可捕获信号等错误场景,确保信号处理逻辑的正确性。