Linux信号(Signal)

目录

一、学习路线图

二、什么是信号?

三、必须掌握的信号

四、信号的生命周期

五、三种处理方式

六、核心API

[1. signal() ------ 老古董](#1. signal() —— 老古董)

[2. kill() / raise() ------ 发送信号](#2. kill() / raise() —— 发送信号)

[3. alarm() / pause() ------ 定时与等待](#3. alarm() / pause() —— 定时与等待)

[4. sigaction() ------ 真正的王者](#4. sigaction() —— 真正的王者)

七、信号集与阻塞(进阶)

八、可重入函数(最重要的坑)

故事:(在介绍信号处理方式中有使用到printf函数(已标注不安全))

什么是可重入?

[Async-Signal-Safe 函数白名单:](#Async-Signal-Safe 函数白名单:)

黑名单(千万别用):

正确模式:

九、常见编程模式

[1. 优雅退出](#1. 优雅退出)

[2. 超时控制](#2. 超时控制)

[3. 子进程回收(防僵尸进程)](#3. 子进程回收(防僵尸进程))

十、调试命令

十一、常见错误表


一、学习路线图

别急着写代码,先看地图。我们按这个顺序来,保证学习不迷路:

二、什么是信号?

一句话定义: 信号是Linux系统发给进程的一种异步通知,告诉进程"嘿,出事了,处理一下!"

生活类比:

想象你在专心打游戏(主进程运行),突然电话响了(信号产生)。

  1. 你暂停游戏,去接电话(阻塞 当前进程,递送信号)。
  2. 你处理电话里的事(执行信号处理函数)。
  3. 挂了电话,继续打游戏(恢复现场)。

一句话总结: 信号就是软件层面的"中断",它是异步的,你永远不知道它下一秒会不会来。

三、必须掌握的信号

Linux有几十种信号,但常用的就那么几个。记住这张表,能应付90%的场景:

信号名 编号 默认动作 能否捕获 使用场景
SIGINT 2 终止 用户按 Ctrl+C
SIGTERM 15 终止 kill 命令默认发送,请求优雅退出
SIGKILL 9 强制终止 核弹选项,系统强制杀进程
SIGSTOP 19 暂停 暂停进程(类似 Ctrl+Z
SIGSEGV 11 终止+CoreDump 段错误(访问非法内存)
SIGCHLD 17 忽略 子进程退出,通知父进程收尸
SIGPIPE 13 终止 往已关闭的管道/Socket写数据
SIGALRM 14 终止 定时器到期

⚠️ 注意: SIGKILL (9) 和 SIGSTOP (19) 是"上帝信号",进程自己无法捕获或忽略它们,这是操作系统保留的最后手段。

一句话总结: SIGTERM 是礼貌的"请离开",SIGKILL 是直接"拔电源"。

四、信号的生命周期

信号不是发出去就立刻执行的,它有4个阶段:

  1. 产生: 键盘按下 Ctrl+C,或者代码调用了 kill()
  2. 注册: 内核在进程的PCB(进程控制块)里把信号标记为"未决"(Pending)。
  3. 递送: 进程从内核态返回用户态时,检查有没有未决信号。如果有且没被阻塞,就递送给进程。
  4. 处理: 进程执行对应的动作(忽略、默认、或自定义函数)。

一句话总结: 信号产生后不一定马上执行,可能会在"未决"队列里排队。

五、三种处理方式

收到信号后,进程通常有三种选择:

  1. 忽略 (SIG_IGN): 假装没听见。
    • 例子: 服务器程序通常忽略 SIGPIPE,防止因为客户端断开导致服务崩溃。
  2. 默认 (SIG_DFL): 听系统的安排。
    • 例子: SIGINT 默认就是终止进程。
  3. 自定义 (函数指针): 自己写个函数处理。
    • 例子: 捕获 SIGTERM 来保存数据再退出。

代码示例:

cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void my_handler(int signum) {
    // 注意:printf 在信号处理函数中是不安全的!
    printf("收到信号 %d,但我偏不退出!\n", signum);
}

int main() {
    // 1. 自定义处理 SIGINT (Ctrl+C)
    signal(SIGINT, my_handler);
    
    // 2. 忽略 SIGTERM (kill 默认信号)
    signal(SIGTERM, SIG_IGN);
    
    while(1) {
        printf("运行中...\n");
        sleep(1);
    }
    return 0;
}

测试:

  • 测试 SIGINT (Ctrl+C)
  • 测试 SIGTERM(需要另开终端)
cpp 复制代码
# 运行程序后,直接在终端按
Ctrl + C

# 终端2:查看进程PID并发送SIGTERM
ps aux | grep test
kill <PID>

因为SIGTERM被忽略,kill <PID>无法杀死进程,进程继续运行,SIGKILL (9)无法被捕获,可通过kill -9 <PID>强制杀死

六、核心API

1. signal() ------ 老古董

这是最早期的接口,简单但不可靠(不同系统实现不一样)。

cpp 复制代码
signal(SIGINT, handler); // 注册handler处理SIGINT

缺点:信号处理完一次后可能会自动重置为默认行为(取决于系统),导致第二次按 Ctrl+C 就退出了。

2. kill() / raise() ------ 发送信号

  • kill(pid, sig):给指定进程发信号。
  • raise(sig):给自己发信号。
cpp 复制代码
kill(1234, 9); // 强制杀死PID为1234的进程
raise(SIGUSR1); // 自己给自己发个自定义信号

3. alarm() / pause() ------ 定时与等待

  • alarm(seconds):设置一个定时器,时间到发 SIGALRM
  • pause():让进程挂起,直到有信号到来。

4. sigaction() ------ 真正的王者

这是POSIX标准推荐的接口,稳定、可靠。

完整例子:

cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handler(int sig, siginfo_t *info, void *context) {
    printf("收到信号 %d, 来自进程 %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    // 1. 清空结构体
    sigemptyset(&sa.sa_mask);
    // 2. 设置标志位:SA_SIGINFO表示使用sa_sigaction而不是sa_handler
    sa.sa_flags = SA_SIGINFO;
    // 3. 注册处理函数
    sa.sa_sigaction = handler;
    
    // 4. 生效
    sigaction(SIGUSR1, &sa, NULL);
    
    printf("等待信号...\n");
    while(1) pause();
    return 0;
}

七、信号集与阻塞(进阶)

有时候,我们不希望信号立刻打断我们(比如正在写关键数据),这时候需要阻塞信号。

核心类型: sigset_t
5个操作函数:

  1. sigemptyset():清空集合
  2. sigfillset():填满集合(全选)
  3. sigaddset():加入某个信号
  4. sigdelset():删除某个信号
  5. sigprocmask()设置阻塞掩码(核心!)

例子:临时屏蔽 SIGINT 5秒

cpp 复制代码
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT); // 把SIGINT加入屏蔽名单

// 阻塞 SIGINT
sigprocmask(SIG_BLOCK, &block_set, &old_set);

printf("接下来的5秒,按Ctrl+C无效...\n");
sleep(5);

// 解除阻塞,恢复原状
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf("恢复响应Ctrl+C\n");

阻塞不是丢弃,信号会在"未决"队列里等着,解除阻塞后立刻处理。

八、可重入函数(最重要的坑)

这是新手最容易死的地方!

故事:(在介绍信号处理方式中有使用到printf函数(已标注不安全))

主程序正在 printf("Hello"),写到一半,信号来了!

信号处理函数里也调用了 printf("Signal!")
printf 内部有锁(线程安全),此时主程序的锁还没释放,处理函数又要加锁。
结果: 死锁!程序卡死。

什么是可重入?

一个函数在被中断后 再次被调用,依然能正常工作

Async-Signal-Safe 函数白名单:

在信号处理函数里,只能调用这些安全的函数:

  • write() (注意不是 printf)
  • read()
  • _exit() (注意不是 exit)
  • signal() (部分系统)

黑名单(千万别用):

  • printf, malloc, new, strtok, exit

正确模式:

信号处理函数只做一件事:设置全局标志位

cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <atomic> // 或者用 volatile sig_atomic_t

// 必须是 volatile,防止编译器优化
volatile sig_atomic_t quit_flag = 0;

void handler(int sig) {
    quit_flag = 1; // 只做这一件事!
}

int main() {
    signal(SIGINT, handler);
    while (!quit_flag) {
        // 主循环正常干活
        printf("Working...\n");
        sleep(1);
    }
    printf("检测到退出标志,优雅退出。\n");
    return 0;
}

一句话总结: 信号处理函数里不要做任何复杂的事,只改一个 volatile 变量。

九、常见编程模式

1. 优雅退出

如上例所示,利用 volatile sig_atomic_t 标志位跳出循环,释放资源。

2. 超时控制

结合 alarmsigaction

cpp 复制代码
// 设置5秒闹钟
alarm(5);
// 如果5秒内没收到信号,pause会返回
pause(); 
// 如果超时,信号处理函数会介入

3. 子进程回收(防僵尸进程)

父进程不需要 wait 阻塞,而是捕获 SIGCHLD

cpp 复制代码
void sigchld_handler(int sig) {
    // 必须用 while 循环,因为可能多个子进程同时退出
    while(waitpid(-1, NULL, WNOHANG) > 0);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigaction(SIGCHLD, &sa, NULL);
    // ... fork子进程 ...
}

十、调试命令

程序跑飞了?用这些命令看看:

  • kill -l:列出所有支持的信号。
  • cat /proc/<pid>/status | grep Sig:查看进程当前的信号掩码和未决信号。
  • strace -e signal=all ./你的程序:跟踪程序接收到的所有信号(调试神器!)。

十一、常见错误表

现象 原因 解决
信号处理函数没被调用 函数签名写错了 确保函数签名是void handler(int)
程序莫名其妙被杀掉 写入了关闭的Socket,内核会发送 SIGPIPE 信号,其默认动作是终止进程 signal(SIGPIPE, SIG_IGN)
printf 卡死或乱码 在信号处理函数中调用了 printf、malloc 等不可重入函数导致死锁 改用 write 或只设标志位
read/accept 返回 -1 系统调用被信号打断,函数返回 -1 并将 errno 设置为 EINTR 检查 errno == EINTRcontinue
相关推荐
承渊政道1 小时前
【动态规划算法】(两个数组的DP问题深度剖析与求解方法)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
近津薪荼2 小时前
C++ vector容器底层深度剖析与模拟实现
开发语言·c++
广州山泉婚姻2 小时前
C++ STL Vector 入门与实战全攻略
c语言·c++
蓝天居士2 小时前
Linux网络驱动之Fixed-Link(29)
linux·运维·网络
qeen872 小时前
【算法笔记】简单贪心
c++·笔记·算法·贪心算法
一叶龙洲2 小时前
Ubuntu24.04向日葵远程控制
linux·运维·ubuntu
似水এ᭄往昔3 小时前
【Linux】--文件系统之软硬链接
linux·运维·服务器
AI进化营-智能译站3 小时前
ROS2 C++开发系列19-枚举定义机器人状态机|随机数生成仿真测试数据流
java·c++·ai·机器人
叶 落3 小时前
Ubuntu 通过 Docker 安装 Mysql8
linux·ubuntu·docker