C++信号处理函数中的非异步安全函数调用问题
一、问题概述
信号处理函数(signal handler)在执行时可能中断程序在任何位置,因此在其中调用非异步安全函数(non-async-safe functions)会导致未定义行为,常见问题包括:
- 死锁:中断了持有锁的代码
- 数据竞争:破坏了数据结构的完整性
- 内存管理问题:中断了
malloc/free的内部状态 - 标准I/O问题:破坏了
stdio缓冲区的状态
二、异步安全函数 vs 非异步安全函数
2.1 异步安全函数(Async-Signal-Safe Functions)
可以在信号处理函数中安全调用的函数,POSIX标准规定了一组异步安全函数。
// 常见的异步安全函数
write() // 低层次I/O
read() // 低层次I/O
signal() // 但要注意递归问题
sigaction() // 设置信号处理
_exit() // 立即退出
_Exit() // 立即退出
abort() // 终止程序
kill() // 发送信号
getpid() // 获取进程ID
getuid() // 获取用户ID
// C++标准库:基本没有异步安全函数!
2.2 非异步安全函数示例(禁止在信号处理中调用)
// ❌ 禁止在信号处理函数中调用
printf() // 使用缓冲I/O
fprintf() // 使用缓冲I/O
cout << // C++流操作
malloc() // 内存分配
free() // 内存释放
new // C++内存分配
delete // C++内存释放
strtok() // 非可重入
rand() // 使用静态状态
pthread_mutex_lock() // 可能造成死锁
std::vector::push_back() // 可能破坏内部状态
三、常见问题场景
3.1 死锁场景
#include <csignal>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex g_mutex;
int g_counter = 0;
// ❌ 错误示例:信号处理函数中使用锁
void signal_handler(int signal) {
std::lock_guard<std::mutex> lock(g_mutex); // 危险!可能造成死锁
std::cout << "Signal " << signal << " received" << std::endl;
g_counter++;
}
int main() {
std::signal(SIGINT, signal_handler);
{
std::lock_guard<std::mutex> lock(g_mutex);
// 如果信号在此处触发,信号处理函数会尝试获取已持有的锁
std::this_thread::sleep_for(std::chrono::seconds(5));
}
return 0;
}
3.2 内存管理问题
#include <csignal>
#include <cstdlib>
#include <cstring>
char* g_buffer = nullptr;
// ❌ 错误示例:在信号处理函数中调用malloc/free
void signal_handler(int signal) {
// 如果信号中断了malloc/free,再次调用会导致堆损坏
if (g_buffer) {
free(g_buffer); // 危险!可能破坏堆的内部状态
g_buffer = nullptr;
}
g_buffer = (char*)malloc(100); // 危险!
strcpy(g_buffer, "Signal received");
}
int main() {
g_buffer = (char*)malloc(50);
std::signal(SIGUSR1, signal_handler);
// 如果信号在malloc内部触发,会导致问题
for (int i = 0; i < 1000; ++i) {
char* temp = (char*)malloc(100);
// 使用temp...
free(temp);
}
return 0;
}
四、解决方案
4.1 方法一:设置标志位(volatile sig_atomic_t)
#include <csignal>
#include <atomic>
#include <unistd.h>
#include <iostream>
// ✅ 正确方法:使用volatile sig_atomic_t作为标志
volatile sig_atomic_t g_signal_received = 0;
// 信号处理函数:只做最少的工作
void signal_handler(int signal) {
// 只能设置标志位或调用异步安全函数
const char msg[] = "Signal received\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1); // 异步安全的write
g_signal_received = signal; // 这是安全的
}
void process_signal() {
// 在主循环或专门的处理线程中检查标志
if (g_signal_received != 0) {
int sig = g_signal_received;
g_signal_received = 0;
// 这里可以安全地调用非异步安全函数
std::cout << "Processing signal: " << sig << std::endl;
// 其他复杂处理...
}
}
int main() {
std::signal(SIGINT, signal_handler);
while (true) {
process_signal();
// 正常业务逻辑...
sleep(1);
}
return 0;
}
4.2 方法二:使用自管道技巧(Self-Pipe Trick)
#include <csignal>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <iostream>
#include <system_error>
class SignalHandler {
private:
static int pipe_fd[2];
// ✅ 信号处理函数:只向管道写入
static void signal_handler(int signal) {
char sig = static_cast<char>(signal);
// write是异步安全的
ssize_t written = write(pipe_fd[1], &sig, 1);
(void)written; // 忽略返回值,因为信号处理中不能做复杂处理
}
public:
static void initialize() {
// 创建管道
if (pipe(pipe_fd) == -1) {
throw std::system_error(errno, std::generic_category());
}
// 设置写端为非阻塞(防止信号处理中阻塞)
int flags = fcntl(pipe_fd[1], F_GETFL);
fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK);
// 设置信号处理
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
std::signal(SIGUSR1, signal_handler);
}
static bool check_signal() {
struct pollfd pfd;
pfd.fd = pipe_fd[0];
pfd.events = POLLIN;
// 非阻塞检查
int result = poll(&pfd, 1, 0);
if (result > 0 && (pfd.revents & POLLIN)) {
char sig;
ssize_t bytes = read(pipe_fd[0], &sig, 1);
if (bytes == 1) {
handle_signal(static_cast<int>(sig));
return true;
}
}
return false;
}
private:
static void handle_signal(int signal) {
// 在主线程上下文中安全处理信号
std::cout << "Handling signal: " << signal << std::endl;
// 可以安全调用任何函数
// ...
}
};
int SignalHandler::pipe_fd[2] = {-1, -1};
int main() {
SignalHandler::initialize();
while (true) {
// 检查并处理信号
SignalHandler::check_signal();
// 正常业务逻辑
// ...
sleep(1);
}
return 0;
}
4.3 方法三:使用sigwait(多线程程序)
#include <csignal>
#include <pthread.h>
#include <iostream>
#include <cstring>
class SignalThread {
private:
pthread_t signal_thread;
sigset_t signal_set;
static void* thread_func(void* arg) {
SignalThread* self = static_cast<SignalThread*>(arg);
while (true) {
int sig = 0;
// 同步等待信号(不是异步处理)
int result = sigwait(&self->signal_set, &sig);
if (result == 0) {
// 在专门线程中安全处理信号
self->handle_signal(sig);
}
}
return nullptr;
}
void handle_signal(int signal) {
// 可以安全调用任何函数
std::cout << "Signal thread handling signal: "
<< strsignal(signal) << std::endl;
switch (signal) {
case SIGINT:
std::cout << "Initiating graceful shutdown..." << std::endl;
// 执行清理操作...
break;
case SIGUSR1:
std::cout << "Reloading configuration..." << std::endl;
// 重新加载配置...
break;
}
}
public:
SignalThread() {
// 初始化信号集
sigemptyset(&signal_set);
sigaddset(&signal_set, SIGINT);
sigaddset(&signal_set, SIGTERM);
sigaddset(&signal_set, SIGUSR1);
sigaddset(&signal_set, SIGUSR2);
// 在主线程中阻塞这些信号
pthread_sigmask(SIG_BLOCK, &signal_set, nullptr);
// 创建专门处理信号的线程
pthread_create(&signal_thread, nullptr, thread_func, this);
}
~SignalThread() {
pthread_cancel(signal_thread);
pthread_join(signal_thread, nullptr);
}
};
int main() {
SignalThread signal_thread;
// 主线程不会被信号中断
std::cout << "Main thread running. Send signals to process "
<< getpid() << std::endl;
while (true) {
// 正常业务逻辑,不会被信号打断
std::cout << "Working..." << std::endl;
sleep(2);
}
return 0;
}
4.4 方法四:使用signalfd(Linux特有)
#ifdef __linux__
#include <sys/signalfd.h>
#include <csignal>
#include <unistd.h>
#include <iostream>
#include <system_error>
#include <poll.h>
class SignalFdHandler {
private:
int signal_fd;
sigset_t mask;
public:
SignalFdHandler() {
// 设置要处理的信号
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGUSR1);
// 阻塞这些信号,防止默认处理
if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) {
throw std::system_error(errno, std::generic_category());
}
// 创建signalfd
signal_fd = signalfd(-1, &mask, SFD_NONBLOCK);
if (signal_fd == -1) {
throw std::system_error(errno, std::generic_category());
}
}
~SignalFdHandler() {
close(signal_fd);
}
bool check_signal() {
struct pollfd pfd;
pfd.fd = signal_fd;
pfd.events = POLLIN;
if (poll(&pfd, 1, 0) > 0) {
if (pfd.revents & POLLIN) {
struct signalfd_siginfo fdsi;
ssize_t bytes = read(signal_fd, &fdsi, sizeof(fdsi));
if (bytes == sizeof(fdsi)) {
handle_signal(fdsi.ssi_signo);
return true;
}
}
}
return false;
}
void handle_signal(int signal) {
// 在主线程上下文中安全处理
std::cout << "SignalFd handling signal: "
<< strsignal(signal) << std::endl;
switch (signal) {
case SIGINT:
case SIGTERM:
std::cout << "Shutting down..." << std::endl;
// 执行清理操作
break;
case SIGUSR1:
std::cout << "User signal 1 received" << std::endl;
break;
}
}
int get_fd() const { return signal_fd; }
};
int main() {
SignalFdHandler signal_handler;
while (true) {
// 检查信号(可以与其他I/O一起处理)
signal_handler.check_signal();
// 正常业务逻辑
std::cout << "Working..." << std::endl;
sleep(1);
}
return 0;
}
#endif // __linux__
五、最佳实践和注意事项
5.1 安全模式总结
// 在信号处理函数中安全的模式:
// 1. 设置volatile sig_atomic_t标志
// 2. 调用异步安全函数(如write)
// 3. 使用自增计数器(原子操作)
// 4. 通过管道/文件描述符通信
// 绝对禁止:
// 1. 调用malloc/free/new/delete
// 2. 使用标准I/O(printf/cout)
// 3. 获取锁(mutex/semaphore)
// 4. 调用非可重入函数
// 5. 访问复杂数据结构
5.2 可重入函数设计
// 设计信号安全的函数
class SignalSafeLogger {
private:
static constexpr int BUFFER_SIZE = 1024;
char buffer[BUFFER_SIZE];
int pos;
// 异步安全的字符串复制
void safe_strcpy(char* dest, const char* src) {
while (*src && pos < BUFFER_SIZE - 1) {
dest[pos++] = *src++;
}
dest[pos] = '\0';
}
public:
SignalSafeLogger() : pos(0) {
buffer[0] = '\0';
}
// 可以在信号处理函数中调用
void log(const char* message) {
const char prefix[] = "LOG: ";
for (int i = 0; i < sizeof(prefix) - 1 && pos < BUFFER_SIZE - 1; ++i) {
buffer[pos++] = prefix[i];
}
safe_strcpy(buffer, message);
// 添加换行
if (pos < BUFFER_SIZE - 1) {
buffer[pos++] = '\n';
buffer[pos] = '\0';
}
}
// 在主线程中输出
void flush() {
if (pos > 0) {
write(STDERR_FILENO, buffer, pos); // 异步安全
pos = 0;
buffer[0] = '\0';
}
}
};
5.3 错误处理模式
#include <csignal>
#include <cstdlib>
#include <unistd.h>
// 紧急情况下的安全错误处理
void emergency_shutdown(int signal) {
// 1. 记录信号(使用异步安全方式)
const char msg[] = "Emergency shutdown due to signal X\n";
char emergency_msg[sizeof(msg)];
// 手动构造消息
char* p = emergency_msg;
const char* base = "Emergency shutdown due to signal ";
while (*base) *p++ = *base++;
// 转换信号号为字符串
int sig = signal;
if (sig >= 10) *p++ = '0' + sig / 10;
*p++ = '0' + sig % 10;
*p++ = '\n';
*p = '\0';
// 2. 写入日志(异步安全)
write(STDERR_FILENO, emergency_msg, p - emergency_msg);
// 3. 执行最低限度的清理
// 如果有文件描述符需要立即关闭,在这里处理
// 但不要调用close(),因为它可能不是异步安全的
// 而是直接使用_exit()
// 4. 立即退出
_exit(EXIT_FAILURE); // 异步安全的退出
}
// 正常的信号处理
volatile sig_atomic_t shutdown_requested = 0;
void graceful_signal_handler(int signal) {
const char msg[] = "Shutdown requested\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
shutdown_requested = 1;
}
int main() {
// 设置紧急信号处理
std::signal(SIGSEGV, emergency_shutdown);
std::signal(SIGILL, emergency_shutdown);
std::signal(SIGFPE, emergency_shutdown);
std::signal(SIGABRT, emergency_shutdown);
// 设置正常关闭的信号处理
std::signal(SIGINT, graceful_signal_handler);
std::signal(SIGTERM, graceful_signal_handler);
while (!shutdown_requested) {
// 正常业务逻辑
}
// 执行正常的清理
std::cout << "Performing graceful shutdown..." << std::endl;
// ... 清理代码 ...
return 0;
}
六、跨平台考虑
#ifdef _WIN32
// Windows信号处理(结构化异常处理)
#include <windows.h>
#include <iostream>
LONG WINAPI exception_handler(PEXCEPTION_POINTERS pExceptionInfo) {
std::cerr << "Exception caught: 0x"
<< std::hex << pExceptionInfo->ExceptionRecord->ExceptionCode
<< std::endl;
// Windows中可以在异常处理中做更多事情
// 但仍然要小心
return EXCEPTION_EXECUTE_HANDLER;
}
void setup_windows_signal_handling() {
SetUnhandledExceptionFilter(exception_handler);
}
#else
// POSIX系统
#include <csignal>
void setup_posix_signal_handling() {
// 使用sigaction替代signal,提供更细粒度的控制
struct sigaction sa;
sa.sa_handler = graceful_signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 系统调用被中断后自动重启
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
// 对致命信号使用默认处理
signal(SIGSEGV, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGFPE, SIG_DFL);
}
#endif
七、总结
- 信号处理函数中只能调用异步安全函数
- 简单操作:设置标志位,使用
volatile sig_atomic_t - 复杂处理:通过管道、signalfd或专门的信号处理线程
- 避免:锁、内存分配、标准I/O、复杂数据结构
- 紧急情况:使用
_exit()立即退出,避免进一步破坏 - 平台差异:POSIX和Windows有不同的信号/异常处理机制
正确的信号处理需要谨慎设计,确保在异步中断的上下文中不破坏程序状态。在需要复杂处理的场景中,推荐使用自管道、signalfd或专门的信号处理线程等技术。
2690

被折叠的 条评论
为什么被折叠?



