信号安全编程 (Async-Signal-Safe)
概述
信号安全 (async-signal-safe) 是指函数可以在信号处理函数中安全调用的特性. 信号处理函数是异步执行的, 可能在程序执行的任何时刻被调用, 包括在非信号安全函数执行期间. 因此, 信号处理函数中只能调用信号安全的函数.
为什么需要信号安全
问题场景
信号处理函数是异步执行的, 可能在以下情况下被调用:
- 中断正在执行的函数: 信号可能在程序执行任何函数时到达
- 中断系统调用: 信号可能中断正在执行的系统调用
- 中断库函数 : 信号可能中断正在执行的库函数 (如
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()- 获取进程 IDgetuid(),geteuid()- 获取用户 IDgetgid(),getegid()- 获取组 IDgetpgrp(),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)
信号安全函数的特点
- 可重入 (Reentrant): 函数不依赖全局状态或静态变量
- 原子性 (Atomic): 函数执行不能被信号中断
- 无锁 (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()- 系统日志函数 (某些实现可能不安全)
编写信号安全的代码
基本原则
- 最小化处理: 信号处理函数应该尽可能简单
- 只设置标志 : 使用
volatile sig_atomic_t类型的标志变量 - 使用信号安全函数: 只调用 POSIX 规定的信号安全函数
- 避免全局状态 : 不访问或修改全局变量 (除了
volatile sig_atomic_t) - 避免锁操作: 不使用可能被中断的锁
正确示例
示例 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 类型:
- volatile: 防止编译器优化, 确保每次读取都从内存获取最新值
- 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);
}
或者使用 sigaction 的 sa_mask:
c
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGUSR1); // 在处理 SIGUSR1 时阻塞它
sa.sa_handler = signal_handler;
sigaction(SIGUSR1, &sa, NULL);
多线程环境中的信号安全
在多线程程序中, 信号安全更加复杂:
- 信号发送: 信号会发送到任意一个线程
- 信号处理: 每个线程有独立的信号掩码
- 全局状态: 需要额外的同步机制
多线程最佳实践
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() 立即终止
测试信号安全性
测试方法
- 压力测试: 在信号处理函数中快速发送大量信号
- 竞态条件测试: 在主程序执行各种操作时发送信号
- 重入测试: 在信号处理函数执行期间再次发送相同信号
测试示例
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;
}
总结
关键要点
- 信号处理函数必须只调用信号安全函数
- 使用
volatile sig_atomic_t在信号处理函数和主程序之间通信 - 最小化信号处理函数, 只做必要的工作
- 使用管道或 signalfd 将信号转换为 I/O 事件
- 避免在信号处理函数中使用全局状态
信号安全函数检查清单
在信号处理函数中使用函数前, 检查:
- 函数是否在 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