在 Linux 中,信号(signal)是用于进程间通信的一种机制。信号可以通知一个进程发生了某种事件或者需要进行某些操作。信号通常是由操作系统内核发送给进程,也可以由其他进程发送给目标进程。

1. 信号的基本概念

  • 信号类型:信号是一种异步通知机制,通常由内核或其他进程触发。
  • 信号传递:信号会传递给目标进程,目标进程在接收到信号后会执行默认的处理行为,或者根据程序的设定执行自定义处理。
  • 信号的目标:信号可以由内核发送,也可以由一个进程发送给另一个进程。信号会传递给特定的进程或进程组。

2. 信号的常见用途

  • 中断:用户按下 Ctrl+C 会发送 SIGINT 信号,通知进程中断。
  • 终止进程:可以发送 SIGTERMSIGKILL 来结束进程。
  • 控制进程行为:比如使用 SIGSTOP 停止进程,使用 SIGCONT 恢复进程执行。
  • 进程间通信:可以使用信号传递简单的信息或请求,通知进程某些事件的发生。

3. 信号的常见类型

以下是 Linux 中的一些常见信号:

p12

信号 信号编号 说明
SIGINT 2 中断信号(通常由 Ctrl+C 产生)
SIGTERM 15 终止信号(请求进程终止,优雅退出)
SIGKILL 9 强制终止信号(无法被捕获或忽略)
SIGSTOP 19 停止信号(停止进程,无法被捕获)
SIGCONT 18 恢复进程运行信号(使停止的进程继续)
SIGSEGV 11 段错误信号(程序访问非法内存时触发)
SIGALRM 14 定时器到期信号
SIGCHLD 17 子进程状态变化信号
SIGUSR1 10 用户定义的信号1(可供用户自定义用途)
SIGUSR2 12 用户定义的信号2(可供用户自定义用途)

4. 信号的处理

进程在接收到信号时,可能会采取以下几种行为:

  1. 默认行为:操作系统为每个信号提供一个默认的处理方式。例如,SIGTERM 的默认行为是终止进程,SIGSEGV 的默认行为是终止进程并生成核心转储文件。

  2. 忽略信号:进程可以选择忽略某些信号。例如,使用 SIGCHLD 时,进程可能选择忽略它,避免子进程终止时通知父进程。

  3. 自定义处理:进程可以安装一个信号处理函数,处理特定信号。在信号到达时,程序会跳转到该函数进行处理。

5. 信号处理的常用函数

  • signal():设置信号的处理方式,可以是默认行为、忽略或自定义处理函数。

      signal(SIGINT, signal_handler);
    

    这里,signal_handler 是自定义的信号处理函数,当进程收到 SIGINT 信号时会执行该函数。

  • sigaction():更强大的信号处理设置,比 signal() 更为灵活。可以控制信号的屏蔽、恢复等行为。

      struct sigaction sa;
      sa.sa_handler = signal_handler;
      sa.sa_flags = 0;
      sigemptyset(&sa.sa_mask);
      sigaction(SIGINT, &sa, NULL);
    

    使用 sigaction() 可以更精细地控制信号处理行为,例如,指定信号处理时是否屏蔽其他信号。

  • raise():向当前进程发送信号。

      raise(SIGINT);  // 发送 SIGINT 信号给当前进程
    
  • kill():向指定的进程发送信号。

      kill(pid, SIGTERM);  // 向进程 pid 发送 SIGTERM 信号
    
  • sigprocmask():用于设置或检查信号屏蔽字,进程可以控制哪些信号可以中断其执行。

      sigset_t set;
      sigemptyset(&set);
      sigaddset(&set, SIGINT);
      sigprocmask(SIG_BLOCK, &set, NULL);
    

6. 信号的屏蔽与阻塞

信号可以在进程中被屏蔽,阻止它们立即执行。使用 sigprocmask() 函数,进程可以选择屏蔽某些信号,直到它们显式解除屏蔽。

例如,进程可以屏蔽 SIGINT 信号,这样按下 Ctrl+C 时不会立即终止程序。

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);  // 屏蔽 SIGINT 信号

7. 信号的发送

信号可以由操作系统、内核或用户程序发送。用户程序可以使用 kill()raise() 等函数来发送信号:

kill(pid, SIGKILL);  // 向进程 pid 发送 SIGKILL 信号
  • kill() 可以向任意进程发送信号。
  • raise() 只能向当前进程发送信号。

8. 信号的处理示例

以下是一个使用 signal() 函数处理 SIGINT 信号的简单示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handle_signal(int sig) {
    printf("Caught signal %d\n", sig);
}

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

    while (1) {
        printf("Running... Press Ctrl+C to send SIGINT\n");
        sleep(1);  // 模拟长时间运行
    }

    return 0;
}

在此程序中,按下 Ctrl+C 会触发 SIGINT 信号,进程捕获到信号后会执行 handle_signal 函数。

9. 常见信号的默认行为

信号 默认行为
SIGINT 终止进程(Ctrl+C)
SIGTERM 终止进程(默认的终止信号)
SIGKILL 立即终止进程(无法被捕获或忽略)
SIGSTOP 停止进程(不能被捕获或忽略)
SIGCONT 恢复停止的进程

10. 信号的应用场景

  • 进程间通信:信号常常用于进程间的简单通信,虽然它比消息队列和管道等方式更简单,但也具有一定的限制。

  • 进程控制:信号用于控制进程的行为,比如终止进程、暂停进程等。SIGKILL 是强制终止进程的常用信号,SIGSTOP 用于停止进程,SIGCONT 用于恢复进程。

  • 事件通知:信号常用于通知程序发生某些事件,如定时器到期、外部中断等。

总结

Linux 信号是进程间通信和控制的重要机制,可以用于通知进程发生特定事件或请求进程执行某些操作。理解信号的概念和如何处理它们,对于编写高效、可靠的系统级程序非常重要。