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
相关推荐
宴之敖者、1 天前
Linux——\r,\n和缓冲区
linux·运维·服务器
LuDvei1 天前
LINUX错误提示函数
linux·运维·服务器
未来可期LJ1 天前
【Linux 系统】进程间的通信方式
linux·服务器
Abona1 天前
C语言嵌入式全栈Demo
linux·c语言·面试
Lenyiin1 天前
Linux 基础IO
java·linux·服务器
The Chosen One9851 天前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器
Kira Skyler1 天前
eBPF debugfs中的追踪点format实现原理
linux
吴维炜1 天前
「Python算法」计费引擎系统SKILL.md
python·算法·agent·skill.md·vb coding
2501_927773071 天前
uboot挂载
linux·运维·服务器
wdfk_prog1 天前
[Linux]学习笔记系列 -- [drivers][dma]dmapool
linux·笔记·学习