Linux常用信号(进程间通信)详解

在Linux系统中,信号(Signal) 是一种进程间通信(IPC)机制,用于通知进程发生了某种事件(如用户中断、错误异常、定时器到期等)。信号本质上是一种“软件中断”,它允许内核或其他进程异步地通知目标进程执行预定义的操作(如终止、暂停、忽略等)。

与管道(Pipe)、消息队列等需要显式数据传输的IPC方式不同,信号更轻量,适用于简单的事件通知(而非大量数据传递)。例如:用户按下Ctrl+C终止程序(触发SIGINT)、进程访问非法内存(触发SIGSEGV)、后台进程被挂起后恢复(SIGCONT)等场景,都依赖信号机制实现。

本文将从信号的基本概念出发,详细介绍Linux常用信号、信号处理机制、核心系统调用,并通过实例演示信号在进程通信中的应用,最后总结最佳实践。

目录#

  1. 信号基本概念
    • 信号的生命周期
    • 信号的分类
  2. Linux常用信号详解
    • 标准信号(1-31)
    • 常用信号表及默认行为
  3. 信号处理机制
    • 默认行为
    • 忽略信号
    • 自定义信号处理函数
  4. 核心信号系统调用
    • kill():发送信号
    • sigaction():注册信号处理函数
    • sigprocmask():阻塞/解除阻塞信号
    • pause():等待信号
    • alarm():定时器信号
  5. 实践案例
    • 案例1:捕获Ctrl+CSIGINT)信号
    • 案例2:父子进程通过SIGUSR1通信
    • 案例3:信号阻塞保护临界区
  6. 信号使用最佳实践
  7. 参考资料

1. 信号基本概念#

1.1 信号的生命周期#

一个信号从产生到被处理,经历以下阶段:

  1. 产生(Generation):信号由内核、其他进程或进程自身触发。

    • 内核触发:如硬件错误(SIGSEGV)、定时器到期(SIGALRM)。
    • 进程触发:通过kill()系统调用向其他进程发送信号。
    • 自身触发:通过raise()向自身发送信号,或abort()触发SIGABRT
  2. 传递(Delivery):内核将信号发送到目标进程。若进程当前被阻塞(如等待I/O),信号会暂存,待进程恢复运行后传递。

  3. 未决(Pending):若目标进程暂时无法处理信号(如信号被阻塞),信号会处于“未决”状态,直到阻塞解除。

  4. 处理(Handling):进程根据预设规则处理信号(默认行为、忽略、自定义处理函数)。

1.2 信号的分类#

Linux信号分为两类:

  • 标准信号(Standard Signals):编号1-31,继承自早期Unix,不支持排队(同一信号多次产生可能被合并),携带信息有限。
  • 实时信号(Real-Time Signals):编号34-64(SIGRTMINSIGRTMAX),支持排队(多次产生不会丢失),可携带更多信息(如信号发送者PID、附加数据)。

本文重点讨论标准信号(最常用)。

2. Linux常用信号详解#

2.1 标准信号(1-31)#

标准信号的编号和名称在<signal.h>中定义,部分信号有固定含义(如SIGINT),部分可由用户自定义(如SIGUSR1/SIGUSR2)。以下是最常用的标准信号:

2.2 常用信号表及默认行为#

信号编号信号名称默认行为描述及触发场景
1SIGHUP终止终端断开连接(如SSH会话关闭),常用于通知守护进程重读配置文件(需自定义处理)。
2SIGINT终止用户按下Ctrl+C,请求进程中断。
3SIGQUIT终止并生成core dump用户按下Ctrl+\,比SIGINT更强制,常用于调试(生成内存转储文件)。
4SIGILL终止并生成core dump进程执行非法指令(如代码损坏、硬件故障)。
6SIGABRT终止并生成core dump进程调用abort()函数(如assert断言失败)。
8SIGFPE终止并生成core dump浮点运算错误(如除零、溢出)。
9SIGKILL终止强制终止进程,无法被捕获或忽略(“必杀信号”)。
11SIGSEGV终止并生成core dump进程访问非法内存地址(如空指针解引用、数组越界)。
13SIGPIPE终止向已关闭的管道(Pipe)写入数据(如`echo "a"
14SIGALRM终止alarm()函数设置的定时器到期。
15SIGTERM终止kill命令默认发送的信号(kill pid等价于kill -15 pid),可被捕获。
17SIGCHLD忽略子进程终止或暂停时,内核发送给父进程的通知信号(默认忽略,需自定义处理以避免僵尸进程)。
19SIGSTOP暂停进程强制暂停进程,无法被捕获或忽略
20SIGTSTP暂停进程用户按下Ctrl+Z,可被捕获(如自定义暂停逻辑)。
28SIGWINCH忽略终端窗口大小改变时触发(如top命令根据窗口大小调整输出)。
10/12SIGUSR1/SIGUSR2终止用户自定义信号,无默认语义,需通过代码实现逻辑(如进程间自定义事件通知)。

注意

  • SIGKILL(9)和SIGSTOP(19)是唯一无法被捕获、忽略或阻塞的信号,用于强制终止/暂停进程。
  • core dump:默认行为为“终止并生成core dump”时,进程会在当前目录生成core.pid文件(需开启系统core dump限制,ulimit -c unlimited),用于调试。

3. 信号处理机制#

进程对信号的处理有三种方式:

3.1 默认行为(Default Action)#

内核预定义的处理逻辑,如“终止”“暂停”等(见2.2节表格)。

3.2 忽略信号(Ignore)#

进程显式忽略信号(通过代码设置),内核会丢弃该信号,进程无任何反应。
例外SIGKILLSIGSTOP无法被忽略。

3.3 自定义信号处理函数(Catch)#

进程注册自定义函数(“信号处理器”),当信号触发时,内核中断当前执行流程,跳转到处理器函数执行,完成后返回原流程。

关键函数:signal()sigaction()#

注册自定义处理器的常用接口有两个:

  1. signal()(简单但功能有限):
    原型:void (*signal(int signum, void (*handler)(int)))(int);
    作用:为信号signum注册处理器handlerSIG_DFL表示默认,SIG_IGN表示忽略)。
    缺点:行为在不同系统中可能不一致(如是否自动重启被中断的系统调用),不推荐用于生产环境。

  2. sigaction()(推荐,功能更强大):
    原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    作用:为信号signum设置处理动作act,并可通过oldact获取旧动作。
    优势:支持设置标志(如SA_RESTART自动重启系统调用)、获取信号发送者信息、原子化设置/获取动作等。

struct sigaction结构体定义:

struct sigaction {
    void     (*sa_handler)(int);          // 简单处理器(无附加信息)
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理器(带信号信息)
    sigset_t   sa_mask;                   // 处理期间阻塞的信号集
    int        sa_flags;                  // 标志(如SA_SIGINFO、SA_RESTART)
    void     (*sa_restorer)(void);        // 已废弃,无需关注
};
  • sa_handlersa_sigaction二选一:使用SA_SIGINFO标志时,内核会调用sa_sigaction并传入信号详细信息(如发送者PID、信号值等)。
  • sa_mask:处理信号期间,内核会自动阻塞sa_mask中的信号,避免嵌套处理(默认阻塞当前信号本身,除非设置SA_NODEFER标志)。

4. 核心信号系统调用#

4.1 kill():发送信号#

向指定进程/进程组发送信号。
原型:int kill(pid_t pid, int sig);
参数:

  • pid:目标进程ID(>0:指定进程;0:当前进程组;-1:所有进程;< -1:进程组ID为-pid)。
  • sig:信号编号(0为“空信号”,用于检测进程是否存在)。
    返回值:成功返回0,失败返回-1(如目标进程不存在)。

示例

// 向进程1234发送SIGTERM(终止信号)
kill(1234, SIGTERM);
// 向当前进程组所有进程发送SIGKILL
kill(0, SIGKILL);

4.2 sigaction():注册信号处理器#

如3.3节所述,sigaction()是设置信号处理逻辑的推荐接口。
示例:注册SIGINT处理器,打印提示后退出:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
 
void sigint_handler(int signum) {
    printf("捕获到SIGINT(%d),程序即将退出...\n", signum);
    exit(0); // 退出进程
}
 
int main() {
    struct sigaction act;
    act.sa_handler = sigint_handler; // 设置处理器函数
    sigemptyset(&act.sa_mask);       // 处理期间不阻塞其他信号
    act.sa_flags = 0;                // 默认标志
 
    // 为SIGINT注册处理器
    if (sigaction(SIGINT, &act, NULL) == -1) {
        perror("sigaction failed");
        return 1;
    }
 
    while (1); // 无限循环,等待信号
    return 0;
}

4.3 sigprocmask():阻塞/解除阻塞信号#

控制进程的“信号掩码”(阻塞信号集),暂时阻止指定信号被传递。
原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:

  • how:操作类型(SIG_BLOCK:添加到掩码;SIG_UNBLOCK:从掩码移除;SIG_SETMASK:替换掩码)。
  • set:待操作的信号集(NULL表示不修改)。
  • oldset:输出旧信号集(NULL表示不获取)。

示例:阻塞SIGINT,执行临界区后解除阻塞:

sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // 要阻塞的信号:SIGINT
 
// 阻塞SIGINT,并保存旧掩码
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1) {
    perror("sigprocmask failed");
    return 1;
}
 
// 临界区:执行期间不会被SIGINT中断
printf("执行临界区...\n");
sleep(5); // 模拟耗时操作
 
// 恢复旧掩码(解除SIGINT阻塞)
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
    perror("sigprocmask failed");
    return 1;
}

4.4 pause():等待信号#

使进程暂停运行,直到收到一个未被阻塞的信号(若信号被忽略,进程继续暂停;若信号终止进程,pause()不返回;若信号被捕获,处理器执行后pause()返回-1并设置errno=EINTR)。
原型:int pause(void);

示例:等待任意信号唤醒:

printf("等待信号...\n");
pause(); // 阻塞直到收到信号
printf("被信号唤醒!\n"); // 若信号被捕获,处理器执行后返回此处

4.5 alarm():定时器信号#

设置一个定时器,到期后内核向进程发送SIGALRM信号。
原型:unsigned int alarm(unsigned int seconds);
返回值:剩余秒数(若之前有未到期的定时器),否则0。

示例:5秒后触发SIGALRM

#include <signal.h>
#include <stdio.h>
 
void handle_alarm(int signum) {
    printf("5秒到!\n");
}
 
int main() {
    signal(SIGALRM, handle_alarm);
    alarm(5); // 5秒后发送SIGALRM
    pause();  // 等待信号
    return 0;
}

5. 实践案例#

案例1:捕获Ctrl+CSIGINT)信号#

默认情况下,Ctrl+C触发SIGINT,进程直接终止。以下代码自定义处理逻辑,要求用户确认后再退出:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
 
void sigint_handler(int signum) {
    char buf[32];
    printf("\n确定要退出吗?(y/n) ");
    fflush(stdout); // 刷新缓冲区(printf默认行缓冲,此处无换行需手动刷新)
 
    // 读取用户输入(注意:read是异步信号安全的,printf不是,但此处简化演示)
    if (read(STDIN_FILENO, buf, sizeof(buf)) > 0 && (buf[0] == 'y' || buf[0] == 'Y')) {
        printf("退出中...\n");
        exit(0);
    } else {
        printf("继续运行...\n");
    }
}
 
int main() {
    struct sigaction act;
    act.sa_handler = sigint_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, NULL);
 
    while (1) {
        printf("运行中... (按下Ctrl+C测试)\n");
        sleep(1);
    }
    return 0;
}

运行效果
按下Ctrl+C后,程序不会直接退出,而是提示用户确认,输入y才退出。

案例2:父子进程通过SIGUSR1通信#

SIGUSR1/SIGUSR2是用户自定义信号,可用于进程间传递自定义事件。以下示例中,父进程创建子进程后,向子进程发送SIGUSR1,子进程捕获并打印消息:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
 
// 子进程的SIGUSR1处理器
void child_handler(int signum) {
    printf("子进程(PID: %d)收到SIGUSR1信号!\n", getpid());
}
 
int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    }
 
    if (pid == 0) { // 子进程
        struct sigaction act;
        act.sa_handler = child_handler;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR1, &act, NULL); // 注册处理器
 
        printf("子进程(PID: %d)等待信号...\n", getpid());
        pause(); // 等待父进程信号
        exit(0);
    } else { // 父进程
        sleep(1); // 等待子进程准备就绪
        printf("父进程(PID: %d)向子进程(PID: %d)发送SIGUSR1...\n", getpid(), pid);
        kill(pid, SIGUSR1); // 发送信号
        wait(NULL); // 等待子进程退出
        printf("子进程已退出\n");
    }
 
    return 0;
}

运行输出

子进程(PID: 12345)等待信号...
父进程(PID: 12344)向子进程(PID: 12345)发送SIGUSR1...
子进程(PID: 12345)收到SIGUSR1信号!
子进程已退出

案例3:信号阻塞保护临界区#

在多线程/多进程环境中,若某段代码(如写文件)不希望被信号中断,可通过sigprocmask()阻塞信号,执行完毕后解除阻塞:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
 
void sigint_handler(int signum) {
    printf("\n捕获到SIGINT,但先完成文件写入!\n");
}
 
int main() {
    // 注册SIGINT处理器(确保信号不被忽略)
    signal(SIGINT, sigint_handler);
 
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }
 
    sigset_t mask, oldmask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT); // 阻塞SIGINT
 
    // 开始临界区:阻塞SIGINT
    if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1) {
        perror("sigprocmask failed");
        return 1;
    }
 
    // 写入文件(期间按Ctrl+C不会中断)
    const char *msg = "这是一段需要完整写入的内容\n";
    write(fd, msg, strlen(msg));
    printf("文件写入中...(按Ctrl+C测试)\n");
    sleep(5); // 模拟耗时写入
 
    // 结束临界区:解除SIGINT阻塞
    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
        perror("sigprocmask failed");
        return 1;
    }
 
    close(fd);
    printf("文件写入完成!\n");
    return 0;
}

运行效果
执行程序后5秒内按下Ctrl+C,信号会被阻塞,直到文件写入完成后才触发处理器函数。

6. 信号使用最佳实践#

  1. 优先使用sigaction()而非signal()
    signal()在不同系统中的行为不一致(如是否自动重启被中断的系统调用),sigaction()是POSIX标准接口,功能更全面(支持设置标志、获取旧动作等)。

  2. 信号处理器中仅使用异步信号安全函数
    信号处理是异步的(可能在任意时刻中断进程),因此处理器函数中严禁调用非异步信号安全的函数(如printfmallocfopen等,这些函数可能导致数据竞争或死锁)。
    安全函数列表见man 7 signal-safety,常用的有:write()_exit()signal()sigprocmask()等。
    示例:用write(STDOUT_FILENO, ...)替代printf

    void handler(int signum) {
        const char *msg = "信号触发\n";
        write(STDOUT_FILENO, msg, strlen(msg)); // 安全
    }
  3. 处理被中断的系统调用(EINTR
    某些系统调用(如readwritesleep)在收到信号时会中断并返回-1,同时设置errno=EINTR。需在代码中处理这种情况,例如重试系统调用:

    while ((n = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR);

    或通过sigactionSA_RESTART标志自动重启被中断的系统调用:

    act.sa_flags = SA_RESTART; // 自动重启部分系统调用
  4. 谨慎使用信号阻塞
    长时间阻塞信号可能导致信号丢失(标准信号不排队),仅在临界区(如文件写入、数据更新)短暂阻塞,并及时解除。

  5. 避免在信号处理器中执行复杂逻辑
    信号处理器应尽量简洁(如设置全局标志),复杂逻辑(如日志记录、状态更新)应放在主程序中通过轮询标志处理:

    volatile sig_atomic_t flag = 0; // 全局标志(需用volatile和sig_atomic_t确保原子性)
     
    void handler(int signum) {
        flag = 1; // 处理器仅设置标志
    }
     
    int main() {
        // ... 注册处理器 ...
        while (1) {
            if (flag) {
                // 主程序中处理复杂逻辑
                printf("处理信号事件\n");
                flag = 0;
            }
            sleep(1);
        }
    }
    • volatile:防止编译器优化将变量缓存到寄存器,确保每次读取最新值。
    • sig_atomic_t:确保标志的读写是原子操作,避免数据竞争。
  6. 自定义信号(SIGUSR1/SIGUSR2)需明确文档化
    若使用SIGUSR1/SIGUSR2实现自定义逻辑,需在代码或文档中明确说明信号的用途(如“SIGUSR1用于触发配置重载”),避免与其他组件冲突。

参考资料#

通过本文,相信你已对Linux信号机制有了深入理解。信号作为轻量级IPC工具,在进程控制、异常处理等场景中不可或缺,但也需注意其异步特性带来的复杂性。合理使用信号,可让程序更健壮、更灵活。