在 UNIX/Linux 操作系统中,信号(Signal)是一种进程间的通信机制,用于通知进程特定的事件发生。信号是异步的,允许进程在无需轮询的情况下处理来自系统或其他进程的事件。今儿个,咱们来唠一唠信号的概念、处理方法、以及在 C++ 中的实现。


一、什么是信号?

信号是一种用于通知进程发生特定事件的软中断机制。信号可以由操作系统、硬件设备、用户(例如通过 Ctrl+C)或其他进程发出。信号允许进程异步地响应事件,例如异常情况、外部中断等。

信号的主要特性包括:

  • 异步性:信号可以在任意时刻被发送,接收进程并不需要主动查询。
  • 软中断:信号不会强行中断进程,而是通知它处理特定的事件。
  • 系统资源管理:信号机制帮助操作系统管理进程生命周期、资源、权限和状态。

二、信号的处理方式

当一个进程接收到信号时,操作系统为其提供了三种处理方式:

2.1 默认处理

大多数信号都有默认的处理方式。例如,收到 SIGTERM 信号时,默认行为是终止进程;收到 SIGSEGV 信号时,进程会被终止并生成核心转储文件。程序可以选择不修改这些默认行为。

2.2 自定义处理

进程可以通过定义信号处理函数(signal handler)来覆盖信号的默认处理方式。当进程接收到指定信号时,操作系统将调用用户定义的处理函数来执行自定义逻辑。

2.3 忽略信号

某些信号允许进程选择忽略它们。例如,程序可以忽略 SIGPIPE 信号,避免在向关闭的套接字写数据时导致进程终止。


三、使用 signal() 设置信号处理函数

在 UNIX/Linux 系统中,可以使用 signal() 函数来设置信号的处理方式。该函数允许开发者为特定信号定义自定义处理函数,或者选择恢复默认处理或忽略信号。

3.1 signal() 函数的原型

1
sighandler_t signal(int signum, sighandler_t handler);
  • signum:信号的编号,例如 SIGINT(中断信号)。
  • handler:信号处理函数,可以是自定义函数,SIG_IGN(忽略信号)或 SIG_DFL(恢复默认行为)。

四、信号处理函数的编写

信号处理函数是一个符合特定原型的函数,它在信号到达时被操作系统调用。该函数通常只接收一个参数,即信号的编号。

信号处理函数的原型

1
void signalHandler(int signum);

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <csignal>
#include <unistd.h>

// 自定义信号处理函数
void signalHandler(int signum) {
    std::cout << "Received signal: " << signum << std::endl;
    // 处理信号的逻辑
}

int main() {
    // 设置 SIGINT 信号的处理函数
    signal(SIGINT, signalHandler);

    std::cout << "Press Ctrl+C to send SIGINT signal..." << std::endl;

    // 无限循环等待信号
    while (true) {
        sleep(1);
    }

    return 0;
}

在上面的代码中,当用户按下 Ctrl+C 时,SIGINT 信号被发送给程序,操作系统调用 signalHandler 函数来处理这个信号,输出收到信号的编号。


五、常见信号及其默认行为

以下是一些常见的信号及其默认行为:

信号编号 信号名称 描述 默认行为
SIGINT 中断信号 用户通过键盘(Ctrl+C)触发 终止进程
SIGTERM 终止信号 请求正常终止进程 终止进程
SIGKILL 强制终止 无法被捕捉或忽略,强制终止进程 立即终止进程
SIGSEGV 段错误 无效的内存访问,如访问非法地址 终止进程,并生成核心转储
SIGPIPE 管道破裂 写入一个读端已经关闭的管道 终止进程
SIGALRM 定时器信号 定时器到期 终止进程

六、信号处理中的注意事项

6.1 信号处理函数的简洁性

由于信号处理函数是异步调用的,在信号处理函数中,必须保证代码尽可能简短,避免复杂操作。例如,标准库中的很多函数(如 mallocprintf 等)在信号处理函数中是不安全的,因为它们可能会在多个信号到达时产生未定义行为。

建议使用

  • write() 代替 printf()
  • 使用全局标志位记录信号状态,并在主循环中处理复杂逻辑。

6.2 线程安全与信号重入问题

信号处理函数是异步的,可能在执行过程中被新的信号打断(信号的重入问题)。为了避免这种情况,信号处理函数应避免使用非线程安全的函数,并考虑使用信号屏蔽机制来阻止其他信号的干扰。

屏蔽信号: 可以使用 sigprocmask() 来屏蔽在信号处理期间不希望处理的信号,确保信号处理函数的执行是安全的。

6.3 特殊信号的处理

某些信号如 SIGKILLSIGSTOP 是无法被捕捉或忽略的。SIGKILL 是一种强制终止信号,通常用于无法通过其他信号终止的进程;SIGSTOP 则用于暂停进程,无法被进程自身阻止或处理。


七、高级信号处理:使用 sigaction()

尽管 signal() 函数足以应对大多数简单的信号处理场景,但在实际开发中,使用 sigaction() 提供更强大的控制功能。sigaction() 允许开发者指定更复杂的信号处理行为,如信号阻塞、自动恢复等。

sigaction() 函数的原型

1
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:信号编号。
  • act:新的信号处理行为。
  • oldact:保存旧的信号处理行为。

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <csignal>
#include <unistd.h>

void signalHandler(int signum) {
    std::cout << "Interrupt signal (" << signum << ") received." << std::endl;
}

int main() {
    struct sigaction action;
    action.sa_handler = signalHandler;  // 设置自定义处理函数
    sigemptyset(&action.sa_mask);       // 清空阻塞的信号集
    action.sa_flags = 0;

    // 使用 sigaction 设置 SIGINT 的处理行为
    sigaction(SIGINT, &action, NULL);

    while (true) {
        std::cout << "Waiting for signal..." << std::endl;
        sleep(1);
    }

    return 0;
}

在这个例子中,sigaction() 允许开发者对信号处理的控制更加细致,例如阻塞某些信号并恢复处理前的状态。


八、总结

信号机制是 UNIX/Linux 操作系统中重要的进程间通信手段,通过它可以异步地处理系统级事件和进程间通信。C++ 提供了 signal()sigaction() 等函数,帮助开发者方便地管理信号处理。理解信号的工作原理、捕捉和屏蔽信号的正确方式,有助于编写更健壮、更高效的系统程序。