信号处理
00:00
signal.h与信号处理
概述
信号是操作系统向进程发送的异步通知,用于报告异常事件或请求中断。C语言通过 <signal.h> 头文件提供信号处理机制,允许程序捕获并响应信号,而不是被操作系统直接终止。信号处理在守护进程、服务端程序和需要优雅退出的应用中非常重要。
基础概念
什么是信号
信号是一种软件中断,由操作系统内核或其它进程发送给目标进程。进程收到信号后,可以执行以下三种操作之一:
- 执行默认操作(通常是终止进程)
- 忽略信号
- 捕获信号并执行自定义处理函数
标准信号
| 信号 | 值 | 说明 |
|---|---|---|
SIGINT | 2 | 用户按下 Ctrl+C 产生的中断信号 |
SIGTERM | 15 | 请求终止进程(可捕获,用于优雅退出) |
SIGKILL | 9 | 强制终止进程(不可捕获、不可忽略) |
SIGSEGV | 11 | 段错误,非法内存访问 |
SIGABRT | 6 | 由 abort() 产生的异常终止 |
SIGFPE | 8 | 算术异常(如除零、浮点溢出) |
SIGPIPE | 13 | 管道破裂(向已关闭的管道写入) |
SIGUSR1 | 10 | 用户自定义信号1 |
SIGUSR2 | 12 | 用户自定义信号2 |
SIGCHLD | 17 | 子进程状态改变(停止或退出) |
SIGALRM | 14 | 由 alarm() 设置的定时器到期 |
signal 函数原型
#include <signal.h>
// 注册信号处理函数
// sig: 要捕获的信号编号
// func: 处理函数指针,或 SIG_DFL(默认处理)、SIG_IGN(忽略)
// 返回值: 之前的信号处理函数指针,或 SIG_ERR(失败)
void (*signal(int sig, void (*func)(int)))(int);
快速上手
最简单的信号处理
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
// 信号处理函数
void handler(int sig) {
printf("收到信号 %d,程序即将退出\n", sig);
exit(0);
}
int main(void) {
// 注册 SIGINT 的处理函数(Ctrl+C)
signal(SIGINT, handler);
printf("程序运行中,按 Ctrl+C 触发信号...\n");
// 无限循环等待信号
while (1) {
// 在实际程序中这里会做有意义的工作
}
return 0;
}
详细用法
signal 函数的返回值
signal 函数返回之前的信号处理函数指针,可以用来保存和恢复:
#include <stdio.h>
#include <signal.h>
void my_handler(int sig) {
printf("处理信号: %d\n", sig);
}
int main(void) {
// 保存原来的处理方式
void (*old_handler)(int) = signal(SIGINT, my_handler);
if (old_handler == SIG_ERR) {
perror("signal 注册失败");
return 1;
}
printf("信号处理已注册\n");
printf("按 Ctrl+C 触发信号,按回车恢复默认处理\n");
getchar();
// 恢复默认处理
signal(SIGINT, old_handler);
printf("已恢复默认信号处理\n");
return 0;
}
忽略信号
#include <stdio.h>
#include <signal.h>
int main(void) {
// 忽略 SIGINT(Ctrl+C 将无法终止程序)
signal(SIGINT, SIG_IGN);
printf("SIGINT 已被忽略,Ctrl+C 无效\n");
printf("5秒后程序自动退出\n");
// 简单等待
for (volatile int i = 0; i < 500000000; i++);
return 0;
}
使用 raise 发送信号
#include <stdio.h>
#include <signal.h>
void handler(int sig) {
printf("捕获到信号: %d\n", sig);
}
int main(void) {
signal(SIGUSR1, handler);
printf("向自己发送 SIGUSR1 信号\n");
raise(SIGUSR1); // 向当前进程发送信号
printf("信号处理完成\n");
return 0;
}
使用 sigaction(POSIX 推荐)
sigaction 比 signal 更强大、更可移植,是 POSIX 标准推荐的信号注册方式:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("\n收到 SIGINT (%d),按 Ctrl+\\ 退出\n", sig);
}
int main(void) {
struct sigaction sa;
// 初始化 sigaction 结构
sa.sa_handler = sigint_handler; // 处理函数
sigemptyset(&sa.sa_mask); // 清空信号屏蔽集
sa.sa_flags = 0; // 默认标志
// 注册信号处理
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction 失败");
return 1;
}
printf("程序运行中,按 Ctrl+C 触发信号处理\n");
while (1) {
pause(); // 等待信号
}
return 0;
}
sigaction 的高级标志
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void handler(int sig) {
write(STDOUT_FILENO, "信号已处理\n", 13);
}
int main(void) {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
// SA_RESTART: 被信号中断的系统调用自动重启
// SA_RESETHAND: 信号处理后被重置为默认
// SA_NOCLDSTOP: 子进程停止时不发送 SIGCHLD
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
printf("按 Ctrl+C 触发信号(SA_RESTART 模式)\n");
pause();
return 0;
}
常见场景
场景一:优雅退出
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
// 全局标志,指示程序是否应该退出
static volatile sig_atomic_t should_exit = 0;
void exit_handler(int sig) {
// 只设置标志,不在信号处理函数中做复杂操作
should_exit = 1;
}
int main(void) {
// 注册终止信号的处理函数
signal(SIGINT, exit_handler);
signal(SIGTERM, exit_handler);
printf("程序启动,按 Ctrl+C 优雅退出\n");
while (!should_exit) {
printf("正在工作...\n");
sleep(1);
}
// 退出前的清理工作
printf("\n正在清理资源...\n");
printf("清理完成,程序退出\n");
return 0;
}
场景二:定时器功能
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t timer_expired = 0;
void alarm_handler(int sig) {
timer_expired = 1;
}
int main(void) {
signal(SIGALRM, alarm_handler);
printf("设置5秒定时器\n");
alarm(5); // 5秒后发送 SIGALRM
printf("等待定时器到期...\n");
while (!timer_expired) {
pause(); // 等待信号
}
printf("定时器到期!\n");
return 0;
}
场景三:段错误诊断
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <execinfo.h>
#include <unistd.h>
void segfault_handler(int sig) {
void *array[10];
int size;
fprintf(stderr, "错误: 收到信号 %d (段错误)\n", sig);
// 获取调用栈
size = backtrace(array, 10);
backtrace_symbols_fd(array, size, STDERR_FILENO);
exit(1);
}
int main(void) {
// 注册段错误处理函数,用于诊断
signal(SIGSEGV, segfault_handler);
printf("即将触发段错误...\n");
// 故意制造段错误
int *ptr = NULL;
*ptr = 42; // 访问空指针
return 0;
}
注意事项
信号处理函数中的限制
信号处理函数中只能安全地调用”异步信号安全”(async-signal-safe)的函数。以下函数是安全的:
write、read、_exitsignal、sigprocmaskgetpid、getppid
以下函数是不安全的,不应在信号处理函数中调用:
printf、fprintf、sprintfmalloc、freeexit(应使用_exit)
// 危险:在信号处理函数中调用非安全函数
void bad_handler(int sig) {
printf("收到信号 %d\n", sig); // 不安全!printf 可能正在被主线程使用
free(some_ptr); // 不安全!
}
// 安全:只使用异步信号安全函数和 volatile 标志
static volatile sig_atomic_t got_signal = 0;
void good_handler(int sig) {
got_signal = sig; // 只设置标志
}
// 在主循环中检查标志并做复杂操作
sig_atomic_t 类型
在信号处理函数和主程序之间共享的变量必须使用 volatile sig_atomic_t 类型,保证读写的原子性:
#include <signal.h>
#include <stdio.h>
// 正确:使用 volatile sig_atomic_t
static volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1; // 原子操作,安全
}
int main(void) {
signal(SIGINT, handler);
while (!flag) {
// 主循环
}
printf("检测到信号,退出\n");
return 0;
}
SIGKILL 和 SIGSTOP 不可捕获
// 以下代码会失败,SIGKILL 和 SIGSTOP 不能被捕获或忽略
signal(SIGKILL, handler); // 返回 SIG_ERR
signal(SIGSTOP, SIG_IGN); // 返回 SIG_ERR
信号可能丢失
标准信号(非实时信号)是排队的,如果同一信号在处理函数执行期间多次到达,可能只记录一次。如果需要可靠传递,应使用 POSIX 实时信号(SIGRTMIN 到 SIGRTMAX)。
进阶用法
使用信号屏蔽
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
int main(void) {
sigset_t block_set, old_set;
// 初始化信号集,添加要屏蔽的信号
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGTERM);
// 屏蔽信号
sigprocmask(SIG_BLOCK, &block_set, &old_set);
printf("SIGINT 和 SIGTERM 已屏蔽,5秒内按 Ctrl+C 无效\n");
sleep(5);
// 解除屏蔽
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf("信号已解除屏蔽,现在可以响应 Ctrl+C\n");
pause(); // 等待信号
return 0;
}
使用 sigwait 同步等待信号
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
int main(void) {
sigset_t wait_set;
int sig;
// 屏蔽 SIGINT 和 SIGTERM
sigemptyset(&wait_set);
sigaddset(&wait_set, SIGINT);
sigaddset(&wait_set, SIGTERM);
pthread_sigmask(SIG_BLOCK, &wait_set, NULL);
printf("主线程等待信号中...\n");
// 同步等待信号(不会触发信号处理函数)
sigwait(&wait_set, &sig);
printf("收到信号: %d\n", sig);
return 0;
}
自定义信号通信
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
static volatile sig_atomic_t data_ready = 0;
void sigusr1_handler(int sig) {
data_ready = 1;
}
int main(void) {
signal(SIGUSR1, sigusr1_handler);
pid_t pid = fork();
if (pid == 0) {
// 子进程:3秒后通知父进程
sleep(3);
printf("子进程: 通知父进程数据就绪\n");
kill(getppid(), SIGUSR1);
return 0;
} else {
// 父进程:等待子进程通知
printf("父进程: 等待子进程通知...\n");
while (!data_ready) {
pause();
}
printf("父进程: 收到通知,数据已就绪\n");
}
return 0;
}