C++信号处理函数中的非异步安全函数调用问题

C++信号处理函数中的非异步安全函数调用问题

一、问题概述

信号处理函数(signal handler)在执行时可能中断程序在任何位置,因此在其中调用非异步安全函数(non-async-safe functions)会导致未定义行为,常见问题包括:

  1. 死锁:中断了持有锁的代码
  2. 数据竞争:破坏了数据结构的完整性
  3. 内存管理问题:中断了malloc/free的内部状态
  4. 标准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

七、总结

  1. 信号处理函数中只能调用异步安全函数
  2. 简单操作:设置标志位,使用volatile sig_atomic_t
  3. 复杂处理:通过管道、signalfd或专门的信号处理线程
  4. 避免:锁、内存分配、标准I/O、复杂数据结构
  5. 紧急情况:使用_exit()立即退出,避免进一步破坏
  6. 平台差异:POSIX和Windows有不同的信号/异常处理机制

正确的信号处理需要谨慎设计,确保在异步中断的上下文中不破坏程序状态。在需要复杂处理的场景中,推荐使用自管道、signalfd或专门的信号处理线程等技术。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值