C语言可重入函数与信号安全完全指南

在编写信号处理函数和多线程程序时,理解可重入函数的概念至关重要。本文将详细介绍可重入函数的定义、为什么信号处理函数必须是异步信号安全的,以及如何编写安全的信号处理代码。

什么是可重入函数

可重入函数(Reentrant Function)是指可以被多个任务并发调用而不会产生数据竞争或状态错误的函数。无论有多少线程或信号处理程序同时调用它,都能正确工作。

可重入函数的特性

  • 不使用静态数据结构 - 不依赖全局或静态变量
  • 不调用不可重入函数 - 如 malloc、printf、strtok 等
  • 不修改自身代码 - 不改变函数本身的执行逻辑
  • 使用纯局部变量 - 所有数据来自调用者提供的参数

可重入 vs 不可重入示例

下面是经典的 strtok 不可重入问题:

non_reentrant.c - 不可重入的 strtok
#include 
#include 

void parse_tokens(char *str) {
    char *token = strtok(str, ",");  /* 内部使用静态变量! */
    while (token != NULL) {
        printf("Token: %s\n", token);
        token = strtok(NULL, ",");  /* 破坏之前的状态 */
    }
}

/* 如果在 parse_tokens 执行期间收到信号并调用 strtok,结果不可预测 */

使用可重入版本 strtok_r 修复:

reentrant.c - 使用可重入版本
#include 
#include 

void parse_tokens(char *str) {
    char *saveptr;  /* 调用者提供保存位置 */
    char *token = strtok_r(str, ",", &saveptr);
    while (token != NULL) {
        printf("Token: %s\n", token);
        token = strtok_r(NULL, ",", &saveptr);
    }
}

异步信号安全函数

信号处理函数必须调用异步信号安全(Async-Signal-Safe)的函数。这类函数可以在信号处理程序中安全调用,不会引起死锁或数据损坏。

POSIX 定义的信号安全函数

  • 文件操作:open、read、write、close
  • 字符串操作:memcpy、memset、strlen、strcmp
  • 时间操作:time
  • 原子操作:atomic 系列函数

以下函数在信号处理中不安全禁止使用:

  • printf、fprintf - 可能使用锁
  • malloc、free - 使用堆锁
  • strtok、localtime - 使用静态存储
  • exit - 可能执行 atexit 清理

安全的信号处理模式

使用 volatile 和 sig_atomic_t 进行安全的标志位通信:

safe_signal.c - 安全的信号处理
#include 
#include 
#include 

/* volatile: 防止编译器优化到寄存器 */
/* sig_atomic_t: 保证读写的原子性 */
static volatile sig_atomic_t g_flag = 0;

void signal_handler(int signum) {
    /* 简单赋值操作是信号安全的 */
    g_flag = 1;
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    while (!g_flag) {
        printf("Running... (Ctrl+C to stop)\n");
        sleep(1);
    }

    printf("Received signal, exiting...\n");
    return 0;
}

使用 self-pipe 技巧

当需要在信号处理程序中执行复杂操作时,使用 self-pipe 技巧:

self_pipe.c - 复杂操作的信号安全处理
#include 
#include 
#include 
#include 

int pipe_fd[2];

void signal_handler(int signum) {
    /* 只做原子操作:写入 pipe */
    char c = 1;
    write(pipe_fd[1], &c, 1);
}

int main(void) {
    pipe(pipe_fd);

    struct sigaction sa = {0};
    sa.sa_handler = signal_handler;
    sigaction(SIGTERM, &sa, NULL);

    /* 在主循环中处理信号(不安全操作放这里) */
    while (1) {
        char buf[64];
        int n = read(pipe_fd[0], buf, sizeof(buf));
        if (n > 0) {
            printf("Signal received, doing cleanup...\n");
            /* 这里可以安全调用任何函数 */
            break;
        }
    }
    return 0;
}

与多线程的交互

信号与多线程结合时需要特别注意。进程级信号会被传递到任意一个线程,需要用线程特定的信号掩码控制:

signal_threads.c - 线程信号处理
#include 
#include 
#include 

void *signal_thread(void *arg) {
    /* 这个线程专门处理信号 */
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    pthread_sigmask(SIG_BLOCK, &mask, NULL);

    int sig;
    sigwait(&mask, &sig);  /* 等待信号 */
    printf("Signal %d handled in thread\n", sig);
    return NULL;
}

int main(void) {
    pthread_t tid;
    pthread_create(&tid, NULL, signal_thread, NULL);
    pthread_join(tid, NULL);
    return 0;
}

安全函数速查表

  • strtok_r - 可重入的字符串分割
  • localtime_r - 可重入的时间转换
  • rand_r - 可重入的随机数
  • asprintf - 可重入的格式化字符串
  • getpwnam_r - 可重入的用户查询

总结

编写信号处理代码时,务必遵循以下原则:优先使用 volatile sig_atentomic_t 进行标志位通信;信号处理函数只调用异步信号安全的函数;复杂操作通过 pipe 或其他机制延迟到主循环处理;在多线程环境中使用 sigwait 集中处理信号。