前置知识: C

信号处理

4 minIntermediate2026/6/14

signal.h与信号处理

概述

信号是操作系统向进程发送的异步通知,用于报告异常事件或请求中断。C语言通过 <signal.h> 头文件提供信号处理机制,允许程序捕获并响应信号,而不是被操作系统直接终止。信号处理在守护进程、服务端程序和需要优雅退出的应用中非常重要。

基础概念

什么是信号

信号是一种软件中断,由操作系统内核或其它进程发送给目标进程。进程收到信号后,可以执行以下三种操作之一:

  1. 执行默认操作(通常是终止进程)
  2. 忽略信号
  3. 捕获信号并执行自定义处理函数

标准信号

信号说明
SIGINT2用户按下 Ctrl+C 产生的中断信号
SIGTERM15请求终止进程(可捕获,用于优雅退出)
SIGKILL9强制终止进程(不可捕获、不可忽略)
SIGSEGV11段错误,非法内存访问
SIGABRT6abort() 产生的异常终止
SIGFPE8算术异常(如除零、浮点溢出)
SIGPIPE13管道破裂(向已关闭的管道写入)
SIGUSR110用户自定义信号1
SIGUSR212用户自定义信号2
SIGCHLD17子进程状态改变(停止或退出)
SIGALRM14alarm() 设置的定时器到期

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 推荐)

sigactionsignal 更强大、更可移植,是 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)的函数。以下函数是安全的:

  • writeread_exit
  • signalsigprocmask
  • getpidgetppid

以下函数是不安全的,不应在信号处理函数中调用:

  • printffprintfsprintf
  • mallocfree
  • exit(应使用 _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 实时信号(SIGRTMINSIGRTMAX)。

进阶用法

使用信号屏蔽

#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;
}