Linux 信号机制深度解析:从基础概念到实战应用

引言

在 Linux 系统中,信号(Signal)作为一种异步通信机制,扮演着进程间交互与系统事件通知的关键角色。它如同软件层面的 "中断",能够打断进程的正常执行流,触发预设的处理逻辑。本文将从信号的基本概念出发,结合代码示例与实战场景,深入解析信号的发送、处理、屏蔽等核心机制,帮助读者掌握 Linux 信号的全流程管理。

一、信号基础:软件中断的本质

1. 信号的定义与作用

信号 是 Linux 系统中用于异步通知进程的事件机制,本质是一个整数(信号编号)。它可以由硬件事件(如键盘中断)、系统内核(如内存访问错误)或用户进程(如kill命令)产生,用于:

  • 通知进程异步事件(如文件就绪、定时器超时)。

  • 强制进程终止或暂停(如SIGKILLSIGSTOP)。

  • 实现进程间通信(轻量级 IPC)。

2. 信号的命名与分类

  • 编号与名称 :通过kill -l命令可查看 62 个信号,前 31 个为非实时信号 (不可靠,可能丢失),后 31 个为实时信号(可靠,按顺序排队处理)。

  • 常用信号

    信号编号 名称 描述
    2 SIGINT Ctrl+C 终止进程(可捕获、忽略)
    9 SIGKILL 强制终止进程(不可捕获、忽略)
    14 SIGALRM 闹钟超时(常用于定时任务)
    17 SIGCHLD 子进程状态改变(如终止、暂停,用于父进程回收僵尸)
    15 SIGTERM 优雅终止进程(默认处理,可捕获)

3. 信号的生命周期

  1. 产生 :通过硬件、内核或kill/raise函数触发。

  2. 未决(Pending):信号已产生但未被处理,存储于进程的未决信号集。

  3. 递送(Delivery):信号被递送给进程,触发处理逻辑(默认、忽略或捕获)。

二、信号处理:捕获、忽略与默认

1. 信号处理方式

  • 默认处理 :系统预设行为(如SIGINT默认终止进程)。

  • 忽略处理 :进程对信号不做响应(SIGKILLSIGSTOP不可忽略)。

  • 捕获处理:进程通过自定义函数处理信号(需注册信号处理函数)。

2. 信号处理函数注册

复制代码
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 参数

    • signum:信号编号。

    • handler:处理方式(SIG_IGN忽略,SIG_DFL默认,或自定义函数指针)。

  • 返回值 :成功返回旧处理方式,失败返回SIG_ERR

示例:捕获SIGINT信号
复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
​
void sigint_handler(int signum) {
    printf("Caught SIGINT (signal %d)\n", signum);
}
​
int main() {
    signal(SIGINT, sigint_handler); // 注册捕获函数
    printf("Press Ctrl+C to trigger SIGINT...\n");
    while (1) sleep(1);
    return 0;
}

三、僵尸进程与信号驱动回收

1. 僵尸进程的成因

子进程终止后未被父进程回收,残留进程描述符(状态为Z),占用系统资源。

2. SIGCHLD信号与异步回收

  • 子进程状态改变时,内核向父进程发送SIGCHLD信号。

  • 父进程通过捕获该信号,在处理函数中调用waitpid回收僵尸。

示例:信号驱动回收僵尸进程
复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
​
void sigchld_handler(int signum) {
    while (waitpid(-1, NULL, WNOHANG) > 0) { // 非阻塞回收所有僵尸
        printf("Zombie reaped\n");
    }
}
​
int main() {
    signal(SIGCHLD, sigchld_handler); // 注册信号处理函数
    
    for (int i = 0; i < 3; i++) {
        if (fork() == 0) { // 子进程
            _exit(0); // 立即终止,成为僵尸
        }
    }
    
    while (1) sleep(1); // 父进程循环等待
    return 0;
}

四、信号发送与进程控制

1. 命令行发送信号:kill

复制代码
kill [-信号编号或名称] PID        # 向指定PID进程发送信号
kill -9 PID                      # 强制终止进程(SIGKILL)
kill -SIGTERM PID                 # 优雅终止进程(默认信号)

2. 系统调用发送信号

kill():向指定进程发送信号
复制代码
#include <signal.h>
int kill(pid_t pid, int signum);
  • pid > 0:发送给指定 PID 进程。

  • pid = -1:发送给所有有权限的进程(慎用)。

raise():向自身发送信号
复制代码
#include <signal.h>
int raise(int signum); // 等价于 kill(getpid(), signum)
示例:父子进程信号交互
复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
​
int main() {
    pid_t pid = fork();
    if (pid == 0) { // 子进程
        sleep(2);
        printf("Child sending SIGUSR1 to parent (PID %d)\n", getppid());
        kill(getppid(), SIGUSR1); // 向父进程发送自定义信号
    } else { // 父进程
        signal(SIGUSR1, [](int s) { printf("Parent received SIGUSR1\n"); });
        printf("Parent waiting for signal...\n");
        pause(); // 阻塞等待信号
    }
    return 0;
}

五、信号与进程状态控制

1. 暂停与唤醒进程

pause():无限阻塞直到信号到达
复制代码
#include <unistd.h>
int pause(); // 返回-1,errno设为EINTR(被信号中断)
kill -STOP/CONT:暂停与恢复进程
复制代码
kill -STOP PID  # 暂停进程(状态变为T)
kill -CONT PID  # 恢复进程(状态变为S/R)

2. 定时器与闹钟:alarmsleep

alarm(seconds):设置定时器,到期发送SIGALRM
复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds); // 返回旧闹钟剩余时间
sleep(seconds):睡眠并响应信号
复制代码
#include <unistd.h>
unsigned int sleep(unsigned int seconds); // 提前唤醒返回剩余秒数

六、信号集与屏蔽机制

1. 信号集操作

信号集(sigset_t)用于批量管理信号,常见操作:

复制代码
#include <signal.h>
sigset_t set;
sigemptyset(&set);       // 清空信号集
sigaddset(&set, SIGINT); // 添加信号
sigdelset(&set, SIGQUIT);// 删除信号
int is_member = sigismember(&set, SIGINT); // 检查信号是否存在

2. 信号屏蔽:sigprocmask

复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how

    • SIG_BLOCK:添加信号到掩码(阻塞未决)。

    • SIG_UNBLOCK:从掩码中移除信号(允许递送)。

    • SIG_SETMASK:设置新掩码。

示例:屏蔽信号确保临界区安全
复制代码
#include <stdio.h>
#include <signal.h>
​
void critical_section() {
    sigset_t mask, old_mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT); // 屏蔽SIGINT
    
    sigprocmask(SIG_BLOCK, &mask, &old_mask); // 应用屏蔽
    
    printf("Entering critical section (masked SIGINT)\n");
    sleep(5); // 模拟临界操作
    
    sigprocmask(SIG_SETMASK, &old_mask, NULL); // 恢复旧掩码
    printf("Exited critical section\n");
}
​
int main() {
    critical_section();
    return 0;
}

七、信号的继承与恢复

1. fork后的信号处理继承

子进程继承父进程的信号处理方式:

  • 父进程捕获的信号,子进程同样捕获。

  • 父进程忽略的信号,子进程同样忽略。

2. exec后的信号处理重置

新进程(exec创建)的信号处理恢复为默认,除了被忽略的信号(继续忽略)。

八、实战场景:信号的典型应用

1. 定时任务:闹钟与信号结合

复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
​
void alarm_handler(int signum) {
    printf("Alarm triggered!\n");
    alarm(2); // 重置闹钟
}
​
int main() {
    signal(SIGALRM, alarm_handler);
    alarm(2); // 设置2秒闹钟
    
    while (1) {
        printf("Working...\n");
        sleep(1);
    }
    return 0;
}

2. 优雅退出:捕获SIGTERM

复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
​
volatile int running = 1;
​
void sigterm_handler(int signum) {
    printf("Received SIGTERM, shutting down...\n");
    running = 0;
}
​
int main() {
    signal(SIGTERM, sigterm_handler);
    
    while (running) {
        printf("Running...\n");
        sleep(1);
    }
    printf("Exited gracefully\n");
    return 0;
}

九、总结:信号机制的核心脉络

模块 关键知识点
基础概念 信号编号、分类(实时 / 非实时)、生命周期(产生 - 未决 - 递送)。
处理方式 默认、忽略、捕获(signal函数),信号处理函数的异步执行。
僵尸回收 SIGCHLD信号驱动,waitpid非阻塞回收。
发送与控制 kill命令、kill()/raise()函数,进程暂停(STOP)与恢复(CONT)。
高级特性 信号集(sigset_t)、屏蔽机制(sigprocmask)、定时与临界区保护。

信号机制是 Linux 系统的重要组成部分,合理运用信号能显著提升程序的健壮性与交互性。在实际开发中,需注意信号的异步特性、不可靠信号的潜在丢失问题,以及多信号处理时的竞态条件,确保系统稳定运行。