C语言信号处理完全指南

系统编程信号处理

信号是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语言系统编程的核心能力,理解其异步特性和安全约束是编写可靠程序的前提。