信号是Unix/Linux系统中进程间通信和异常处理的核心机制。C语言通过signal和sigaction接口提供信号处理能力,但信号处理充满了陷阱和边界情况。本文系统讲解信号机制、安全处理模式以及工程实践中的最佳方案。
信号基础
标准信号分类
/* 常见信号一览
*
* 终止信号(默认终止进程):
* SIGINT (2) Ctrl+C 产生的中断
* SIGTERM (15) 请求终止(可捕获)
* SIGKILL (9) 强制终止(不可捕获!)
* SIGHUP (1) 终端挂起
* SIGQUIT (3) 终端退出(产生core)
*
* 异常信号:
* SIGSEGV (11) 段错误(非法内存访问)
* SIGABRT (6) abort()调用
* SIGFPE (8) 浮点异常
* SIGBUS (7) 总线错误
* SIGILL (4) 非法指令
*
* 通知信号:
* SIGUSR1 (10) 用户自定义信号1
* SIGUSR2 (12) 用户自定义信号2
* SIGPIPE (13) 管道破裂
* SIGALRM (14) 定时器到期
* SIGCHLD (17) 子进程状态改变
*/
/* 查看所有信号 */
#include <signal.h>
#include <stdio.h>
void list_signals(void) {
for (int sig = 1; sig < 32; sig++) {
const char *name = strsignal(sig);
printf(" SIG%-10s (%2d): %s\n", sigabbrev_np(sig), sig, name);
}
}
signal() 基础用法
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
/* 简单信号处理函数 */
volatile sig_atomic_t got_interrupt = 0;
void handle_sigint(int signo) {
/* 注意:只应设置 volatile sig_atomic_t 变量 */
got_interrupt = 1;
}
int main(void) {
/* 注册信号处理函数 */
void (*old)(int) = signal(SIGINT, handle_sigint);
if (old == SIG_ERR) {
perror("signal");
return 1;
}
printf("PID=%d, 按 Ctrl+C 触发信号...\n", getpid());
while (!got_interrupt) {
printf("工作中...\n");
sleep(1);
}
printf("收到 SIGINT,优雅退出\n");
return 0;
}
sigaction 高级接口
sigaction 完整用法
#include <signal.h>
#include <stdio.h>
#include <string.h>
volatile sig_atomic_t g_running = 1;
volatile sig_atomic_t g_reload = 0;
/* SIGTERM/SIGINT: 优雅退出 */
void handle_shutdown(int signo) {
g_running = 0;
}
/* SIGHUP: 重新加载配置 */
void handle_reload(int signo) {
g_reload = 1;
}
void setup_signals(void) {
struct sigaction sa_shutdown, sa_reload;
/* 设置 SIGTERM 处理 */
memset(&sa_shutdown, 0, sizeof(sa_shutdown));
sa_shutdown.sa_handler = handle_shutdown;
sigemptyset(&sa_shutdown.sa_mask);
/* 在处理 SIGTERM 时屏蔽 SIGINT,避免竞争 */
sigaddset(&sa_shutdown.sa_mask, SIGINT);
sa_shutdown.sa_flags = 0; // 不使用 SA_RESTART,使系统调用中断
if (sigaction(SIGTERM, &sa_shutdown, NULL) < 0) {
perror("sigaction SIGTERM");
}
if (sigaction(SIGINT, &sa_shutdown, NULL) < 0) {
perror("sigaction SIGINT");
}
/* 设置 SIGHUP 处理 */
memset(&sa_reload, 0, sizeof(sa_reload));
sa_reload.sa_handler = handle_reload;
sigemptyset(&sa_reload.sa_mask);
sa_reload.sa_flags = SA_RESTART; // 自动重启被中断的系统调用
if (sigaction(SIGHUP, &sa_reload, NULL) < 0) {
perror("sigaction SIGHUP");
}
}
sa_flags 标志详解
/* sa_flags 常用标志:
*
* SA_RESTART - 被信号中断的系统调用自动重启
* SA_SIGINFO - 使用 sa_sigaction 三参数处理函数
* SA_NOCLDSTOP - 子进程停止时不产生 SIGCHLD
* SA_NOCLDWAIT - 子进程终止时不产生僵尸进程
* SA_NODEFER - 处理信号期间不自动屏蔽该信号
* SA_RESETHAND - 处理后恢复为默认行为(一次性)
*/
/* 使用 SA_SIGINFO 获取详细信息 */
void handle_siginfo(int signo, siginfo_t *info, void *context) {
printf("信号: %d\n", signo);
printf("发送进程 PID: %d\n", info->si_pid);
printf("发送进程 UID: %d\n", info->si_uid);
printf("信号值: %d\n", info->si_value.sival_int);
if (signo == SIGSEGV) {
printf("非法访问地址: %p\n", info->si_addr);
}
}
void setup_siginfo_handler(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handle_siginfo;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGSEGV, &sa, NULL);
}
信号集操作
屏蔽与等待信号
#include <signal.h>
#include <stdio.h>
/* 临时屏蔽信号 */
void critical_section_example(void) {
sigset_t block_set, old_set;
/* 初始化要屏蔽的信号集 */
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGTERM);
sigaddset(&block_set, SIGHUP);
/* 屏蔽信号,保存旧屏蔽字 */
if (sigprocmask(SIG_BLOCK, &block_set, &old_set) < 0) {
perror("sigprocmask");
return;
}
/* === 临界区开始 === */
printf("执行关键操作,不会被信号打断...\n");
/* ... 关键代码 ... */
/* === 临界区结束 === */
/* 恢复原来的屏蔽字 */
sigprocmask(SIG_SETMASK, &old_set, NULL);
}
/* 等待信号 */
void wait_for_signal(void) {
sigset_t wait_set;
sigemptyset(&wait_set);
sigaddset(&wait_set, SIGUSR1);
sigaddset(&wait_set, SIGUSR2);
/* 先屏蔽要等待的信号 */
sigprocmask(SIG_BLOCK, &wait_set, NULL);
printf("等待 SIGUSR1 或 SIGUSR2...\n");
/* sigwait 会原子性地取消屏蔽并等待 */
int received;
if (sigwait(&wait_set, &received) == 0) {
printf("收到信号: %d\n", received);
}
}
实时信号
实时信号与队列
/* 实时信号特点:
* 1. 信号可排队(标准信号可能丢失)
* 2. 可携带整数或指针数据
* 3. 保证先进先出顺序
* 4. 编号范围: SIGRTMIN ~ SIGRTMAX
*/
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t g_event_count = 0;
void handle_realtime(int signo, siginfo_t *info, void *ctx) {
printf("实时信号 %d, 值=%d, 来自PID=%d\n",
signo, info->si_value.sival_int, info->si_pid);
g_event_count++;
}
void setup_realtime(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handle_realtime;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO | SA_RESTART;
/* 使用 SIGRTMIN+0 避免与系统内部冲突 */
sigaction(SIGRTMIN + 1, &sa, NULL);
}
/* 发送实时信号 */
void send_realtime_signal(pid_t target, int value) {
union sigval sv;
sv.sival_int = value;
if (sigqueue(target, SIGRTMIN + 1, sv) < 0) {
perror("sigqueue");
}
}
安全信号处理
异步信号安全函数
/* POSIX 规定的异步信号安全函数(可在信号处理函数中调用):
*
* 安全: _exit, write, read, signal, sigprocmask,
* sigaction, kill, getpid, getppid, fork,
* exec, dup, dup2, pipe, close, stat,
* chmod, chdir, mkdir, rmdir, unlink,
* time, times, gettimeofday, clock_gettime
*
* 不安全: printf, malloc, free, exit, fopen,
* fclose, strftime, syslog, pthread_*
*
* 原则: 信号处理函数中只调用异步信号安全函数!
*/
/* 安全的日志写入 */
#include <unistd.h>
#include <string.h>
static int log_fd = -1;
void safe_log(const char *msg) {
if (log_fd >= 0) {
write(log_fd, msg, strlen(msg));
}
}
void handle_crash(int signo) {
/* 只使用安全函数 */
char buf[64];
int len = snprintf(buf, sizeof(buf), "Crash signal %d\n", signo);
write(STDERR_FILENO, buf, len);
_exit(128 + signo); // _exit 而非 exit
}
/* 自增原子标志 + 主循环检查(推荐模式) */
volatile sig_atomic_t g_terminate = 0;
void handle_term(int signo) {
/* 最小化信号处理函数 */
g_terminate = 1;
}
int main(void) {
struct sigaction sa;
sa.sa_handler = handle_term;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
while (!g_terminate) {
/* 主逻辑: 可以安全地调用任何函数 */
printf("运行中...\n");
sleep(1);
}
/* 清理代码在信号处理函数之外 */
printf("优雅关闭\n");
return 0;
}
信号与子进程
SIGCHLD 处理
#include <signal.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
/* 回收子进程,避免僵尸进程 */
void handle_sigchld(int signo) {
int status;
pid_t pid;
/* WNOHANG: 非阻塞,回收所有已退出的子进程 */
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
// 安全写入日志
char buf[64];
int len = snprintf(buf, sizeof(buf),
"子进程 %d 退出, 状态=%d\n", pid, WEXITSTATUS(status));
write(STDOUT_FILENO, buf, len);
}
}
}
void setup_sigchld(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_sigchld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
}
/* 或者直接忽略 SIGCHLD,子进程自动回收 */
void ignore_sigchld(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_IGN;
sa.sa_flags = SA_NOCLDWAIT;
sigaction(SIGCHLD, &sa, NULL);
}
定时器信号
alarm 与 interval timer
#include <signal.h>
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t g_timer_fired = 0;
void handle_alarm(int signo) {
g_timer_fired = 1;
}
/* 使用 setitimer 实现精确定时 */
void setup_timer(int interval_ms) {
struct sigaction sa;
sa.sa_handler = handle_alarm;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGALRM, &sa, NULL);
struct itimerval timer;
timer.it_value.tv_sec = interval_ms / 1000;
timer.it_value.tv_usec = (interval_ms % 1000) * 1000;
timer.it_interval.tv_sec = timer.it_value.tv_sec;
timer.it_interval.tv_usec = timer.it_value.tv_usec;
setitimer(ITIMER_REAL, &timer, NULL);
}
int main(void) {
setup_timer(500); // 每500ms触发
int count = 0;
while (count < 10) {
if (g_timer_fired) {
g_timer_fired = 0;
printf("定时器触发 #%d\n", ++count);
}
pause(); // 等待信号
}
return 0;
}
总结
- signal vs sigaction - 优先使用sigaction,功能更完善且行为可预测
- 异步信号安全 - 信号处理函数中只调用POSIX规定的安全函数
- 最小化处理 - 信号处理函数只设置标志,主循环检查并处理
- 信号屏蔽 - 使用sigprocmask保护临界区,sigwait同步等待
- 实时信号 - 可排队、可携带数据,适合事件通知场景
- SIGCHLD - 正确处理子进程退出,避免僵尸进程
信号处理是C语言系统编程的核心能力,理解其异步特性和安全约束是编写可靠程序的前提。