MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 信号处理函数

2021-03-064.5k 阅读

C++ 中的信号处理基础

在 C++ 编程中,信号处理是一个重要的概念,它允许程序对特定的系统事件做出响应。信号是由操作系统发送给进程的异步通知,用于指示发生了某些特定的事件,例如程序收到了终止请求、发生了除零错误等。C++ 通过 <csignal> 头文件提供了处理信号的能力。

信号的概念

信号可以看作是一种软件中断。当特定事件发生时,操作系统会向相应的进程发送信号。每个信号都有一个唯一的编号,在 <csignal> 头文件中定义了各种常见信号的宏。例如:

  • SIGINT:通常由用户通过按下 Ctrl+C 组合键生成,用于请求终止进程。
  • SIGTERM:这是一个通用的终止信号,由系统或其他进程发送,要求进程正常终止。
  • SIGSEGV:当进程访问无效的内存地址,例如空指针解引用或越界访问数组时,会收到这个信号。

信号处理函数的注册

在 C++ 中,我们使用 signal 函数来注册信号处理函数。signal 函数的原型如下:

#include <csignal>
void (*signal(int sig, void (*func)(int)))(int);

这个函数接受两个参数:

  • sig:要处理的信号编号。
  • func:指向信号处理函数的指针。信号处理函数必须具有 void func(int) 的形式,其中 int 参数是接收到的信号编号。

signal 函数返回一个指向先前注册的信号处理函数的指针,如果出错则返回 SIG_ERR

简单的信号处理示例

下面是一个简单的示例,展示如何注册一个信号处理函数来处理 SIGINT 信号:

#include <iostream>
#include <csignal>

// 信号处理函数
void signalHandler(int signum) {
    std::cout << "Received signal: " << signum << std::endl;
    std::cout << "Cleaning up and exiting..." << std::endl;
    // 执行清理操作,例如关闭文件、释放资源等
    // 这里简单演示,直接退出程序
    exit(signum);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, signalHandler);

    std::cout << "Program is running. Press Ctrl+C to terminate." << std::endl;
    // 程序主循环,防止程序立即退出
    while (true) {
        // 这里可以放置程序的主要逻辑
    }

    return 0;
}

在这个示例中:

  1. 定义了一个 signalHandler 函数,它是 SIGINT 信号的处理函数。当接收到 SIGINT 信号时,它会打印出接收到的信号编号,并提示进行清理操作,然后调用 exit 函数终止程序。
  2. main 函数中,使用 signal 函数将 signalHandler 注册为 SIGINT 信号的处理函数。
  3. 程序进入一个无限循环,模拟程序的正常运行状态。当用户按下 Ctrl+C 时,操作系统会向该进程发送 SIGINT 信号,signalHandler 函数将被调用。

信号处理的特性与注意事项

异步性

信号是异步发生的,这意味着信号处理函数可能在程序执行的任何时刻被调用,甚至可能在程序执行关键代码段时。因此,在信号处理函数中需要特别小心,避免访问共享资源,因为这可能导致竞态条件。例如,假设在信号处理函数和主程序中都访问同一个全局变量,并且没有适当的同步机制,就可能出现数据不一致的问题。

可重入性

信号处理函数必须是可重入的。可重入函数是指可以被中断,并且在中断后再次调用时能正常工作的函数。一般来说,标准库中的许多函数不是可重入的,例如 printf 函数。在信号处理函数中调用不可重入函数可能导致未定义行为。像 write 这样的系统调用通常是可重入的,可以在信号处理函数中安全使用。

信号屏蔽

在处理一个信号时,操作系统通常会自动屏蔽该信号,以防止在处理过程中再次收到相同的信号。这是为了避免递归调用信号处理函数。但是,在某些情况下,可能需要手动屏蔽或解除屏蔽信号。例如,在进行一些关键操作时,可能不希望被某些信号打断,可以先屏蔽这些信号,操作完成后再解除屏蔽。

高级信号处理技巧

使用 sigaction 函数

虽然 signal 函数简单易用,但它在一些复杂场景下功能有限。sigaction 函数提供了更强大和灵活的信号处理方式。sigaction 函数的原型如下:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum 是要处理的信号编号。act 是一个指向 struct sigaction 的指针,用于指定新的信号处理行为。oldact 是一个可选参数,如果不为 NULL,则会将先前的信号处理设置存储在这个指针指向的结构体中。

struct sigaction 的定义如下:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  • sa_handler:与 signal 函数中的处理函数指针类似,用于指定常规的信号处理函数。
  • sa_sigaction:用于指定更复杂的信号处理函数,它可以获取更多关于信号的信息,例如信号的来源等。
  • sa_mask:在调用信号处理函数时要屏蔽的信号集。
  • sa_flags:用于指定信号处理的各种标志,例如 SA_RESTART 标志可以使某些系统调用在被信号中断后自动重新启动。

下面是一个使用 sigaction 函数的示例,处理 SIGINT 信号:

#include <iostream>
#include <signal.h>

// 信号处理函数
void signalHandler(int signum) {
    std::cout << "Received signal: " << signum << std::endl;
    std::cout << "Cleaning up and exiting..." << std::endl;
    exit(signum);
}

int main() {
    struct sigaction act;
    act.sa_handler = signalHandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (sigaction(SIGINT, &act, NULL) == -1) {
        std::cerr << "Failed to set signal handler" << std::endl;
        return 1;
    }

    std::cout << "Program is running. Press Ctrl+C to terminate." << std::endl;
    while (true) {
        // 程序主逻辑
    }

    return 0;
}

在这个示例中:

  1. 定义了 signalHandler 函数作为 SIGINT 信号的处理函数。
  2. 创建了一个 struct sigaction 结构体 act,设置 sa_handlersignalHandler,清空 sa_mask 并将 sa_flags 设置为 0。
  3. 使用 sigaction 函数将 act 结构体的设置应用到 SIGINT 信号上。如果设置失败,打印错误信息并退出程序。

信号集操作

在使用 sigaction 或其他更高级的信号处理功能时,经常需要操作信号集。信号集是一组信号的集合,可以用于屏蔽或解除屏蔽信号。<signal.h> 头文件提供了一些用于操作信号集的函数:

  • sigemptyset(sigset_t *set):清空信号集 set,使其不包含任何信号。
  • sigfillset(sigset_t *set):将信号集 set 填充为包含所有信号。
  • sigaddset(sigset_t *set, int signum):将信号 signum 添加到信号集 set 中。
  • sigdelset(sigset_t *set, int signum):从信号集 set 中删除信号 signum
  • sigismember(const sigset_t *set, int signum):检查信号 signum 是否在信号集 set 中。

例如,下面的代码演示了如何屏蔽 SIGINT 信号一段时间,然后再解除屏蔽:

#include <iostream>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    // 屏蔽 SIGINT 信号
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        std::cerr << "Failed to block signal" << std::endl;
        return 1;
    }

    std::cout << "SIGINT signal is blocked. Press Ctrl+C to test." << std::endl;
    sleep(5);

    // 解除屏蔽 SIGINT 信号
    if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
        std::cerr << "Failed to unblock signal" << std::endl;
        return 1;
    }

    std::cout << "SIGINT signal is unblocked. Press Ctrl+C to terminate." << std::endl;
    while (true) {
        // 程序主逻辑
    }

    return 0;
}

在这个示例中:

  1. 创建了一个信号集 set,并将 SIGINT 信号添加到其中。
  2. 使用 sigprocmask 函数屏蔽 SIGINT 信号,SIG_BLOCK 表示将 set 中的信号添加到进程的信号屏蔽字中。
  3. 程序暂停 5 秒钟,期间按下 Ctrl+C 不会终止程序,因为 SIGINT 信号被屏蔽。
  4. 5 秒后,使用 sigprocmask 函数解除屏蔽 SIGINT 信号,SIG_UNBLOCK 表示从进程的信号屏蔽字中移除 set 中的信号。

信号处理在多线程程序中的应用

在多线程程序中,信号处理变得更加复杂。默认情况下,信号会被发送到进程,而不是特定的线程。但是,通过一些机制,可以将信号处理与特定线程关联起来。

线程特定的信号处理

在 POSIX 线程库(Pthreads)中,可以使用 pthread_sigmask 函数来设置线程的信号屏蔽字。这个函数与 sigprocmask 类似,但作用于特定的线程。例如:

#include <iostream>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>

void* threadFunction(void* arg) {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    // 线程屏蔽 SIGINT 信号
    if (pthread_sigmask(SIG_BLOCK, &set, NULL) != 0) {
        std::cerr << "Failed to block signal in thread" << std::endl;
        return nullptr;
    }

    std::cout << "Thread: SIGINT signal is blocked. Press Ctrl+C to test." << std::endl;
    sleep(5);

    // 线程解除屏蔽 SIGINT 信号
    if (pthread_sigmask(SIG_UNBLOCK, &set, NULL) != 0) {
        std::cerr << "Failed to unblock signal in thread" << std::endl;
        return nullptr;
    }

    std::cout << "Thread: SIGINT signal is unblocked. Press Ctrl+C to terminate." << std::endl;
    while (true) {
        // 线程主逻辑
    }

    return nullptr;
}

int main() {
    pthread_t thread;
    if (pthread_create(&thread, NULL, threadFunction, NULL) != 0) {
        std::cerr << "Failed to create thread" << std::endl;
        return 1;
    }

    std::cout << "Main thread: Press Ctrl+C to test signal handling." << std::endl;
    while (true) {
        // 主线程主逻辑
    }

    pthread_join(thread, NULL);
    return 0;
}

在这个示例中:

  1. 定义了一个线程函数 threadFunction,在该函数中,首先屏蔽 SIGINT 信号,暂停 5 秒后再解除屏蔽。
  2. main 函数中创建了一个新线程,并进入主线程的无限循环。

信号传递到特定线程

在多线程程序中,有时希望将信号传递到特定的线程进行处理。可以使用 pthread_kill 函数向特定线程发送信号。例如:

#include <iostream>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>

void signalHandler(int signum) {
    std::cout << "Thread received signal: " << signum << std::endl;
    std::cout << "Thread cleaning up and exiting..." << std::endl;
    pthread_exit(nullptr);
}

void* threadFunction(void* arg) {
    struct sigaction act;
    act.sa_handler = signalHandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (sigaction(SIGUSR1, &act, NULL) == -1) {
        std::cerr << "Failed to set signal handler in thread" << std::endl;
        return nullptr;
    }

    std::cout << "Thread is waiting for SIGUSR1 signal." << std::endl;
    while (true) {
        // 线程主逻辑
    }

    return nullptr;
}

int main() {
    pthread_t thread;
    if (pthread_create(&thread, NULL, threadFunction, NULL) != 0) {
        std::cerr << "Failed to create thread" << std::endl;
        return 1;
    }

    sleep(2);
    // 主线程向子线程发送 SIGUSR1 信号
    if (pthread_kill(thread, SIGUSR1) != 0) {
        std::cerr << "Failed to send signal to thread" << std::endl;
        return 1;
    }

    std::cout << "Main thread sent SIGUSR1 signal to the thread." << std::endl;
    pthread_join(thread, NULL);
    return 0;
}

在这个示例中:

  1. 定义了 signalHandler 函数作为 SIGUSR1 信号的处理函数,在线程接收到 SIGUSR1 信号时,打印信息并退出线程。
  2. threadFunction 中注册了 SIGUSR1 信号的处理函数。
  3. main 函数中创建线程,暂停 2 秒后,使用 pthread_kill 函数向子线程发送 SIGUSR1 信号。

异常与信号处理的关系

在 C++ 中,异常机制用于处理程序中的错误和异常情况,而信号处理用于处理系统级的异步事件。虽然它们的用途不同,但在某些情况下可能需要同时考虑。

异常安全的信号处理

由于信号处理函数是异步调用的,并且可能在程序执行的任何时刻打断正常执行流程,因此在信号处理函数中抛出异常是非常危险的。这可能导致程序进入未定义行为,因为异常机制依赖于正常的函数调用栈和栈展开机制,而信号处理函数的调用方式与正常函数调用不同。

例如,假设在信号处理函数中抛出异常,而此时程序正在执行一个复杂的对象构造或析构过程,异常的抛出可能会破坏对象的完整性,导致内存泄漏或其他未定义行为。

从信号处理函数调用 C++ 异常处理代码

虽然不能在信号处理函数中直接抛出异常,但可以通过一些间接的方式在信号处理函数中触发异常处理。一种常见的方法是使用一个全局标志变量。在信号处理函数中设置这个标志,然后在程序的正常执行流程中检查这个标志,并根据标志的值抛出异常。

#include <iostream>
#include <csignal>
#include <exception>

bool signalReceived = false;

// 信号处理函数
void signalHandler(int signum) {
    signalReceived = true;
}

void someFunction() {
    // 模拟一些可能触发信号的操作
    // 例如,这里简单循环
    for (int i = 0; i < 10; ++i) {
        if (signalReceived) {
            throw std::runtime_error("Signal received during operation");
        }
        // 实际操作
        std::cout << "Working on iteration " << i << std::endl;
    }
}

int main() {
    signal(SIGINT, signalHandler);

    try {
        someFunction();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

在这个示例中:

  1. 定义了一个全局变量 signalReceived 用于表示是否接收到信号。
  2. signalHandler 函数在接收到信号时设置 signalReceivedtrue
  3. someFunction 函数在执行过程中检查 signalReceived,如果为 true 则抛出异常。
  4. main 函数中注册信号处理函数,并在 try - catch 块中调用 someFunction,捕获并处理可能抛出的异常。

实际应用场景

服务器程序中的信号处理

在服务器程序中,信号处理非常重要。例如,当服务器接收到 SIGTERM 信号时,需要优雅地关闭连接、保存状态并退出。可以注册一个信号处理函数,在函数中遍历所有的客户端连接,关闭它们,并将服务器的状态保存到文件中。

#include <iostream>
#include <csignal>
#include <vector>
#include <unistd.h>

// 模拟客户端连接
std::vector<int> clientConnections;

// 信号处理函数
void signalHandler(int signum) {
    std::cout << "Received SIGTERM signal. Shutting down server." << std::endl;
    // 关闭所有客户端连接
    for (int conn : clientConnections) {
        // 实际中这里需要调用关闭连接的函数,例如 close(conn)
        std::cout << "Closing client connection " << conn << std::endl;
    }
    // 保存服务器状态到文件
    std::cout << "Saving server state to file." << std::endl;
    exit(signum);
}

int main() {
    signal(SIGTERM, signalHandler);

    // 模拟服务器运行,添加一些客户端连接
    for (int i = 0; i < 5; ++i) {
        clientConnections.push_back(i);
        std::cout << "New client connection " << i << " added." << std::endl;
        sleep(1);
    }

    std::cout << "Server is running. Send SIGTERM to shut it down." << std::endl;
    while (true) {
        // 服务器主循环
    }

    return 0;
}

在这个示例中:

  1. 定义了一个 clientConnections 向量来模拟服务器的客户端连接。
  2. signalHandler 函数在接收到 SIGTERM 信号时,遍历并关闭所有客户端连接,然后保存服务器状态并退出。
  3. main 函数中注册信号处理函数,并模拟添加客户端连接和服务器的运行。

实时系统中的信号处理

在实时系统中,信号处理可以用于处理硬件中断等实时事件。例如,在一个基于微控制器的实时系统中,当外部设备产生中断信号时,可以通过信号处理函数来读取设备数据、更新系统状态等。虽然 C++ 在一些嵌入式实时系统中的应用可能受到资源限制,但在一些资源相对丰富的实时系统中,信号处理可以提供一种灵活的方式来处理异步事件。

假设我们有一个简单的实时系统,模拟一个温度传感器,当温度超过一定阈值时,产生一个信号,信号处理函数记录温度并采取相应的措施。

#include <iostream>
#include <csignal>
#include <unistd.h>

const int temperatureThreshold = 30;
int currentTemperature = 20;

// 信号处理函数
void signalHandler(int signum) {
    std::cout << "Temperature exceeded threshold: " << currentTemperature << " degrees Celsius." << std::endl;
    // 采取措施,例如启动冷却设备
    std::cout << "Starting cooling device." << std::endl;
}

int main() {
    signal(SIGUSR1, signalHandler);

    std::cout << "Real - time system is running. Monitoring temperature." << std::endl;
    while (true) {
        // 模拟温度变化
        currentTemperature++;
        std::cout << "Current temperature: " << currentTemperature << " degrees Celsius." << std::endl;
        if (currentTemperature > temperatureThreshold) {
            // 模拟温度超过阈值,发送 SIGUSR1 信号
            raise(SIGUSR1);
        }
        sleep(1);
    }

    return 0;
}

在这个示例中:

  1. 定义了温度阈值 temperatureThreshold 和当前温度 currentTemperature
  2. signalHandler 函数在接收到 SIGUSR1 信号时,打印温度信息并采取启动冷却设备的措施。
  3. main 函数中注册信号处理函数,并模拟温度变化,当温度超过阈值时发送 SIGUSR1 信号。

通过以上内容,我们详细介绍了 C++ 中的信号处理函数,包括基础概念、简单示例、高级技巧、在多线程程序中的应用、与异常的关系以及实际应用场景。希望这些内容能帮助你在 C++ 编程中更好地处理信号相关的问题。