Linux信号之信号安全

信号安全编程 (Async-Signal-Safe)

概述

信号安全 (async-signal-safe) 是指函数可以在信号处理函数中安全调用的特性. 信号处理函数是异步执行的, 可能在程序执行的任何时刻被调用, 包括在非信号安全函数执行期间. 因此, 信号处理函数中只能调用信号安全的函数.

为什么需要信号安全

问题场景

信号处理函数是异步执行的, 可能在以下情况下被调用:

  1. 中断正在执行的函数: 信号可能在程序执行任何函数时到达
  2. 中断系统调用: 信号可能中断正在执行的系统调用
  3. 中断库函数 : 信号可能中断正在执行的库函数 (如 printf, malloc)

典型问题示例

问题 1: stdio 函数的不安全性
c 复制代码
#include <stdio.h>
#include <signal.h>

void handler(int sig) {
    printf("Signal received\n");  // ❌ 不安全!
}

int main() {
    signal(SIGINT, handler);
    
    // 主程序正在执行 printf, 更新内部缓冲区
    printf("Main program running...");
    
    // 如果此时信号到达, handler 中的 printf 会操作同一个缓冲区
    // 导致数据不一致, 可能崩溃或输出错误
    return 0;
}

问题分析:

  • printf 使用静态缓冲区
  • 主程序调用 printf 时, 缓冲区可能处于部分更新状态
  • 信号处理函数中的 printf 会操作同一个缓冲区
  • 导致数据竞争和不可预测的行为
问题 2: malloc/free 的不安全性
c 复制代码
#include <stdlib.h>
#include <signal.h>

void handler(int sig) {
    char *p = malloc(100);  // ❌ 不安全!
    // ... 使用 p
    free(p);  // ❌ 不安全!
}

int main() {
    signal(SIGINT, handler);
    
    // 主程序正在执行 malloc, 更新堆管理结构
    char *ptr = malloc(1000);
    
    // 如果此时信号到达, handler 中的 malloc/free 会操作同一个堆
    // 可能导致堆损坏, 程序崩溃
    return 0;
}

问题分析:

  • malloc/free 维护全局堆管理结构
  • 主程序调用 malloc 时, 堆结构可能处于不一致状态
  • 信号处理函数中的 malloc/free 会操作同一个堆
  • 导致堆损坏和程序崩溃

信号安全函数列表

POSIX.1 标准要求以下函数必须是信号安全的. 这些函数可以在信号处理函数中安全调用.

系统调用类

进程控制
  • _exit(), _Exit() - 立即终止进程
  • fork() - 创建子进程 (注意: 在某些情况下可能不安全, 见下方说明)
  • execve() - 执行程序
  • execl(), execle(), execv(), execvp(), execvpe(), fexecve() - exec 系列函数
文件操作
  • open(), openat() - 打开文件
  • close() - 关闭文件描述符
  • read(), write() - 读写文件描述符
  • lseek() - 移动文件指针
  • fcntl() - 文件控制
  • dup(), dup2() - 复制文件描述符
  • pipe() - 创建管道
  • chdir(), fchdir() - 改变工作目录
  • chmod(), fchmod(), chmodat() - 改变文件权限
  • chown(), fchown(), fchownat() - 改变文件所有者
  • link(), linkat() - 创建硬链接
  • unlink(), unlinkat() - 删除文件
  • rename(), renameat() - 重命名文件
  • symlink(), symlinkat() - 创建符号链接
  • readlink(), readlinkat() - 读取符号链接
  • stat(), fstat(), lstat(), fstatat() - 获取文件状态
  • mkdir(), mkdirat() - 创建目录
  • rmdir() - 删除目录
  • access(), faccessat() - 检查文件访问权限
  • truncate(), ftruncate() - 截断文件
  • fsync(), fdatasync() - 同步文件数据
  • futimens(), utimensat() - 更新时间戳
信号相关
  • kill() - 发送信号
  • sigaction() - 注册信号处理函数
  • sigaddset(), sigdelset(), sigemptyset(), sigfillset(), sigismember() - 信号集操作
  • sigpending() - 获取待处理信号
  • sigprocmask() - 设置信号掩码
  • sigsuspend() - 等待信号
  • sigqueue() - 发送实时信号
  • pthread_sigmask() - 线程信号掩码 (POSIX 线程)
定时器
  • alarm() - 设置定时器
  • setitimer() - 设置间隔定时器
  • getitimer() - 获取间隔定时器
  • timer_create(), timer_settime(), timer_gettime(), timer_delete() - POSIX 定时器
  • clock_gettime(), clock_settime(), clock_getres() - 时钟操作
  • nanosleep() - 纳秒级睡眠
网络操作
  • accept() - 接受连接
  • bind() - 绑定地址
  • connect() - 建立连接
  • listen() - 监听连接
  • send(), sendto(), sendmsg() - 发送数据
  • recv(), recvfrom(), recvmsg() - 接收数据
  • socket() - 创建套接字
  • getsockname(), getpeername() - 获取套接字地址
  • getsockopt(), setsockopt() - 套接字选项
  • shutdown() - 关闭套接字
异步 I/O
  • aio_error(), aio_return(), aio_suspend() - 异步 I/O 操作
其他系统调用
  • getpid(), getppid() - 获取进程 ID
  • getuid(), geteuid() - 获取用户 ID
  • getgid(), getegid() - 获取组 ID
  • getpgrp(), getpgid(), setpgid() - 进程组操作
  • setsid() - 创建新会话
  • umask() - 设置文件创建掩码
  • pause() - 等待信号

标准库函数类

字符串操作
  • strlen() - 字符串长度
  • strcpy(), strncpy() - 字符串复制
  • strcat(), strncat() - 字符串连接
  • strcmp(), strncmp() - 字符串比较
  • memcpy(), memmove() - 内存复制
  • memset() - 内存设置
  • memcmp() - 内存比较
数学函数
  • abs(), labs(), llabs() - 绝对值
  • div(), ldiv(), lldiv() - 除法运算
其他标准库函数
  • ffs(), ffsl(), ffsll() - 查找第一个设置位
  • abort() - 异常终止 (POSIX.1-2001 TC1)

信号安全函数的特点

  1. 可重入 (Reentrant): 函数不依赖全局状态或静态变量
  2. 原子性 (Atomic): 函数执行不能被信号中断
  3. 无锁 (Lock-free): 函数不使用可能被中断的锁机制

不安全的函数

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

stdio 函数 (全部不安全)

  • printf(), fprintf(), sprintf(), snprintf()
  • scanf(), fscanf(), sscanf()
  • fgets(), fputs(), fread(), fwrite()
  • fopen(), fclose(), fflush()
  • getc(), putc(), getchar(), putchar()
  • 所有其他 FILE* 相关函数

原因: 使用静态缓冲区, 不是可重入的

内存分配函数 (全部不安全)

  • malloc(), calloc(), realloc(), free()
  • alloca() - 栈上分配

原因: 维护全局堆管理结构, 不是可重入的

其他不安全的函数

  • pthread_*() - 大部分 POSIX 线程函数
  • longjmp(), siglongjmp() - 可能破坏栈状态
  • exit() - 执行清理函数, 可能不安全 (使用 _exit() 代替)
  • atexit() - 注册退出处理函数
  • dlopen(), dlsym() - 动态链接函数
  • syslog() - 系统日志函数 (某些实现可能不安全)

编写信号安全的代码

基本原则

  1. 最小化处理: 信号处理函数应该尽可能简单
  2. 只设置标志 : 使用 volatile sig_atomic_t 类型的标志变量
  3. 使用信号安全函数: 只调用 POSIX 规定的信号安全函数
  4. 避免全局状态 : 不访问或修改全局变量 (除了 volatile sig_atomic_t)
  5. 避免锁操作: 不使用可能被中断的锁

正确示例

示例 1: 使用标志变量
c 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

volatile sig_atomic_t flag = 0;

// 信号安全的处理函数
void signal_handler(int sig) {
    // ✅ 只设置标志位, 不做复杂操作
    flag = 1;
    
    // ✅ 可以使用 write (信号安全)
    const char *msg = "Signal received\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

int main() {
    struct sigaction sa;
    
    // 设置信号处理
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGUSR1, &sa, NULL);
    
    printf("Process PID: %d\n", getpid());
    printf("Send SIGUSR1 to trigger signal handler\n");
    
    // 主循环检查标志
    while (1) {
        if (flag) {
            printf("Main: Processing signal...\n");
            flag = 0;  // 重置标志
            // 执行实际处理工作
        }
        sleep(1);
    }
    
    return 0;
}
示例 2: 使用 write 代替 printf
c 复制代码
#include <signal.h>
#include <unistd.h>
#include <string.h>

void signal_handler(int sig) {
    // ✅ 使用 write 代替 printf
    const char *msg = "Signal received\n";
    write(STDOUT_FILENO, msg, strlen(msg));
    
    // ✅ 可以使用信号安全的系统调用
    _exit(1);  // 立即退出
}
示例 3: 使用管道进行通信
c 复制代码
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

static int pipefd[2];

void signal_handler(int sig) {
    char c = 'S';  // 信号标识
    
    // ✅ write 是信号安全的
    write(pipefd[1], &c, 1);
}

int main() {
    char c;
    
    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(1);
    }
    
    // 注册信号处理
    signal(SIGUSR1, signal_handler);
    
    printf("Process PID: %d\n", getpid());
    
    // 主循环从管道读取
    while (1) {
        if (read(pipefd[0], &c, 1) > 0) {
            printf("Main: Received signal notification\n");
            // 执行实际处理
        }
        sleep(1);
    }
    
    return 0;
}
示例 4: 使用 sigqueue 传递数据
c 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

// 使用 SA_SIGINFO 标志的信号处理函数
void signal_handler(int sig, siginfo_t *info, void *context) {
    // ✅ 从 siginfo_t 获取数据, 不需要调用不安全函数
    int value = info->si_value.sival_int;
    
    // ✅ 使用 write 输出
    char msg[100];
    int len = snprintf(msg, sizeof(msg), "Signal %d, value: %d\n", sig, value);
    write(STDOUT_FILENO, msg, len);
}

int main() {
    struct sigaction sa;
    union sigval value;
    
    // 设置信号处理 (使用 SA_SIGINFO)
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = signal_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);
    
    printf("Process PID: %d\n", getpid());
    
    // 发送带数据的信号
    value.sival_int = 123;
    sigqueue(getpid(), SIGUSR1, value);
    
    sleep(1);
    return 0;
}

错误示例

错误 1: 在信号处理函数中使用 printf
c 复制代码
void signal_handler(int sig) {
    printf("Signal received\n");  // ❌ 不安全!
    // 可能导致程序崩溃或输出错误
}
错误 2: 在信号处理函数中使用 malloc
c 复制代码
void signal_handler(int sig) {
    char *buf = malloc(100);  // ❌ 不安全!
    // ... 使用 buf
    free(buf);  // ❌ 不安全!
    // 可能导致堆损坏
}
错误 3: 在信号处理函数中访问非原子全局变量
c 复制代码
int counter = 0;  // ❌ 不是 volatile sig_atomic_t

void signal_handler(int sig) {
    counter++;  // ❌ 不安全! 可能丢失更新
}
错误 4: 在信号处理函数中使用锁
c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void signal_handler(int sig) {
    pthread_mutex_lock(&mutex);  // ❌ 可能死锁!
    // ...
    pthread_mutex_unlock(&mutex);
}

volatile sig_atomic_t 类型

为什么需要 volatile sig_atomic_t

在信号处理函数和主程序之间共享数据时, 必须使用 volatile sig_atomic_t 类型:

  1. volatile: 防止编译器优化, 确保每次读取都从内存获取最新值
  2. sig_atomic_t: 保证读写操作是原子的, 不会被信号中断

使用示例

c 复制代码
#include <signal.h>
#include <stdint.h>

volatile sig_atomic_t flag = 0;        // ✅ 正确
volatile sig_atomic_t counter = 0;     // ✅ 正确

int normal_var = 0;                    // ❌ 错误: 不是 volatile sig_atomic_t
volatile int normal_int = 0;           // ❌ 错误: 不是 sig_atomic_t (可能不是原子的)

sig_atomic_t 的限制

  • 大小限制 : sig_atomic_t 通常是一个整数类型, 大小可能因系统而异
  • 值范围: 通常可以安全存储 -127 到 127 之间的值 (或 0 到 255)
  • 只支持简单操作: 只支持简单的读写和赋值, 不支持复杂表达式
c 复制代码
volatile sig_atomic_t flag = 0;

void signal_handler(int sig) {
    flag = 1;              // ✅ 正确: 简单赋值
    flag++;                // ⚠️  可能不安全: 不是原子操作 (在某些系统上)
    flag = flag + 1;       // ⚠️  可能不安全: 不是原子操作
}

推荐做法: 只使用简单的赋值操作

c 复制代码
volatile sig_atomic_t flag = 0;

void signal_handler(int sig) {
    flag = 1;  // ✅ 只设置标志
}

int main() {
    while (1) {
        if (flag) {        // ✅ 检查标志
            // 执行实际处理
            flag = 0;      // ✅ 重置标志
        }
    }
}

最佳实践

1. 最小化信号处理函数

信号处理函数应该尽可能简单, 只做必要的工作:

c 复制代码
// ✅ 好的做法
volatile sig_atomic_t shutdown = 0;

void signal_handler(int sig) {
    shutdown = 1;  // 只设置标志
}

// ❌ 不好的做法
void signal_handler(int sig) {
    // 执行大量复杂操作
    process_data();
    update_database();
    send_notification();
    // ...
}

2. 使用自管道模式

对于需要复杂处理的场景, 使用管道将信号转换为 I/O 事件:

c 复制代码
#include <signal.h>
#include <unistd.h>

static int signal_fd[2];

void signal_handler(int sig) {
    char c = sig;  // 传递信号编号
    write(signal_fd[1], &c, 1);  // ✅ write 是信号安全的
}

int main() {
    // 创建管道
    pipe(signal_fd);
    
    // 注册信号处理
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    
    // 使用 select/poll/epoll 监听管道
    // 在主循环中处理信号
    while (1) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(signal_fd[0], &readfds);
        
        select(signal_fd[0] + 1, &readfds, NULL, NULL, NULL);
        
        if (FD_ISSET(signal_fd[0], &readfds)) {
            char sig;
            read(signal_fd[0], &sig, 1);
            // 在主循环中安全地处理信号
            handle_signal(sig);
        }
    }
}

3. 使用 signalfd (Linux 特有)

Linux 提供了 signalfd, 可以将信号转换为文件描述符事件:

c 复制代码
#include <sys/signalfd.h>
#include <signal.h>

int main() {
    sigset_t mask;
    int sfd;
    
    // 设置要监听的信号
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    
    // 阻塞这些信号
    sigprocmask(SIG_BLOCK, &mask, NULL);
    
    // 创建 signalfd
    sfd = signalfd(-1, &mask, 0);
    
    // 使用 select/poll/epoll 监听 sfd
    // 读取信号信息并处理
    struct signalfd_siginfo info;
    read(sfd, &info, sizeof(info));
    
    // 在主循环中安全地处理信号
    handle_signal(info.ssi_signo);
    
    return 0;
}

4. 避免在信号处理函数中使用全局变量

c 复制代码
// ❌ 不好的做法
int global_counter = 0;

void signal_handler(int sig) {
    global_counter++;  // 不安全
}

// ✅ 好的做法
volatile sig_atomic_t flag = 0;

void signal_handler(int sig) {
    flag = 1;  // 安全
}

int main() {
    static int counter = 0;  // 在主程序中维护状态
    
    while (1) {
        if (flag) {
            counter++;  // 在主程序中安全地更新
            flag = 0;
        }
    }
}

5. 处理重入问题

即使使用信号安全函数, 也要注意重入问题:

c 复制代码
// ⚠️  潜在问题: 如果信号在处理过程中再次到达
volatile sig_atomic_t in_handler = 0;

void signal_handler(int sig) {
    if (in_handler) {
        return;  // 防止重入
    }
    in_handler = 1;
    
    // 处理信号
    // ...
    
    in_handler = 0;
}

更好的方法是使用信号掩码:

c 复制代码
void signal_handler(int sig) {
    sigset_t mask;
    
    // 阻塞相同信号, 防止重入
    sigemptyset(&mask);
    sigaddset(&mask, sig);
    sigprocmask(SIG_BLOCK, &mask, NULL);
    
    // 处理信号
    // ...
    
    // 解除阻塞
    sigprocmask(SIG_UNBLOCK, &mask, NULL);
}

或者使用 sigactionsa_mask:

c 复制代码
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGUSR1);  // 在处理 SIGUSR1 时阻塞它
sa.sa_handler = signal_handler;
sigaction(SIGUSR1, &sa, NULL);

多线程环境中的信号安全

在多线程程序中, 信号安全更加复杂:

  1. 信号发送: 信号会发送到任意一个线程
  2. 信号处理: 每个线程有独立的信号掩码
  3. 全局状态: 需要额外的同步机制

多线程最佳实践

c 复制代码
#include <pthread.h>
#include <signal.h>

// 使用互斥锁保护共享数据
pthread_mutex_t data_mutex = PTHREAD_MUTEX_INITIALIZER;

static int signal_pipe[2];

void signal_handler(int sig) {
    // 在多线程环境中, 信号处理函数仍然需要是信号安全的
    // 但可以使用线程安全的机制与主线程通信
    
    // ✅ 使用 write 写入管道 (信号安全)
    char c = sig;
    write(signal_pipe[1], &c, 1);
}

void *signal_thread(void *arg) {
    sigset_t set;
    int sig;
    
    // 在这个线程中处理信号
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGTERM);
    
    while (1) {
        sigwait(&set, &sig);  // 等待信号
        // 安全地处理信号 (不在信号处理函数中)
        // handle_signal(sig);
    }
    return NULL;
}

int main() {
    pthread_t thread;
    
    // 创建管道用于信号通信
    pipe(signal_pipe);
    
    // 在主线程中阻塞所有信号
    sigset_t all_signals;
    sigfillset(&all_signals);
    pthread_sigmask(SIG_BLOCK, &all_signals, NULL);
    
    // 创建专门处理信号的线程
    pthread_create(&thread, NULL, signal_thread, NULL);
    
    // 主线程继续工作
    // ...
    
    pthread_join(thread, NULL);
    return 0;
}

常见错误和调试

错误 1: 使用 printf 导致死锁

c 复制代码
// ❌ 错误示例
void signal_handler(int sig) {
    printf("Signal received\n");  // 可能导致死锁或崩溃
}

症状: 程序可能挂起或崩溃

原因 : printf 使用锁保护内部缓冲区, 如果主程序正在执行 printf 时信号到达, 可能导致死锁

解决 : 使用 write() 代替 printf()

错误 2: 使用 malloc 导致堆损坏

c 复制代码
// ❌ 错误示例
void signal_handler(int sig) {
    char *buf = malloc(100);
    // ...
    free(buf);
}

症状: 程序崩溃, 堆损坏

原因 : malloc/free 维护全局堆结构, 不是可重入的

解决: 预先分配缓冲区, 或使用栈上变量

错误 3: 非原子变量更新丢失

c 复制代码
// ❌ 错误示例
int counter = 0;

void signal_handler(int sig) {
    counter++;  // 更新可能丢失
}

症状: 计数器值不正确

原因 : counter++ 不是原子操作, 可能被信号中断

解决 : 使用 volatile sig_atomic_t 类型

错误 4: 在信号处理函数中调用 exit

c 复制代码
// ❌ 错误示例
void signal_handler(int sig) {
    exit(1);  // 可能不安全
}

原因 : exit() 会调用 atexit() 注册的函数, 这些函数可能不是信号安全的

解决 : 使用 _exit() 立即终止

测试信号安全性

测试方法

  1. 压力测试: 在信号处理函数中快速发送大量信号
  2. 竞态条件测试: 在主程序执行各种操作时发送信号
  3. 重入测试: 在信号处理函数执行期间再次发送相同信号

测试示例

c 复制代码
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

volatile sig_atomic_t count = 0;

void signal_handler(int sig) {
    count++;  // 测试原子性
    write(STDOUT_FILENO, ".", 1);  // 测试 write 安全性
}

int main() {
    struct sigaction sa;
    
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGUSR1, &sa, NULL);
    
    printf("PID: %d\n", getpid());
    printf("Sending 1000 signals...\n");
    
    // 快速发送大量信号
    for (int i = 0; i < 1000; i++) {
        kill(getpid(), SIGUSR1);
    }
    
    sleep(1);  // 等待所有信号处理完成
    
    printf("\nCount: %d\n", count);
    
    return 0;
}

总结

关键要点

  1. 信号处理函数必须只调用信号安全函数
  2. 使用 volatile sig_atomic_t 在信号处理函数和主程序之间通信
  3. 最小化信号处理函数, 只做必要的工作
  4. 使用管道或 signalfd 将信号转换为 I/O 事件
  5. 避免在信号处理函数中使用全局状态

信号安全函数检查清单

在信号处理函数中使用函数前, 检查:

  • 函数是否在 POSIX 信号安全函数列表中?
  • 函数是否可重入?
  • 函数是否使用全局状态?
  • 函数是否使用锁?
  • 函数是否可能被信号中断?

推荐资源

  • man 7 signal-safety - POSIX 信号安全函数列表
  • man 2 sigaction - 信号处理函数注册
  • POSIX.1-2008 标准文档
  • Linux 内核源码: kernel/signal.c

参考

  • POSIX.1-2008: Signal Safety
  • Linux Programmer's Manual: signal-safety(7)
  • "Advanced Programming in the UNIX Environment" by W. Richard Stevens
  • "The Linux Programming Interface" by Michael Kerrisk
相关推荐
智驱力人工智能2 小时前
从合规到习惯 海上作业未穿救生衣AI识别系统的工程实践与体系价值 未穿救生衣检测 AI救生衣状态识别 边缘计算救生衣监测设备
人工智能·深度学习·opencv·算法·目标检测·边缘计算
猎板PCB黄浩2 小时前
高多层线路板工厂专业选型指南:全流程评估体系与猎板适配场景解析
大数据·人工智能·算法·pcb
霖大侠2 小时前
Squeeze-and-Excitation Networks
人工智能·算法·机器学习·transformer
Trouvaille ~2 小时前
【Linux】库制作与原理(三):动态链接与加载机制
linux·c语言·汇编·got·动静态库·动态链接·plt
APIshop2 小时前
高性能采集方案:淘宝商品 API 的并发调用与数据实时处理
linux·网络·算法
松涛和鸣3 小时前
DAY38 TCP Network Programming
linux·网络·数据库·网络协议·tcp/ip·算法
ss2733 小时前
ThreadPoolExecutor七大核心参数:从源码看线程池的设计
java·数据库·算法
川213 小时前
ZooKeeper配置+失误
linux·分布式·zookeeper
qq_433554543 小时前
C++ 状压DP(01矩阵约束问题)
c++·算法·矩阵