
在掌握了 Linux 进程信号的产生、保存、捕捉核心流程后,真正的开发实战中,我们还会遇到一系列 “坑点” 和 “高级用法”—— 信号处理时的函数重入导致数据错乱、编译器优化让信号标志位失效、子进程退出产生的僵尸进程问题…… 这些都是进程信号学习中绕不开的重点,也是面试和开发中的高频考点。 本文作为 Linux 进程信号的进阶拓展篇,将聚焦可重入函数、volatile 关键字、SIGCHLD 信号三大核心知识点,从问题根源、底层原理、实战案例、解决方案四个维度层层拆解,同时补充用户态 / 内核态的底层细节和系统调用的追踪技巧。下面就让我们正式开始吧!
信号的异步性意味着信号处理函数可能在任意时刻打断主程序的执行流程—— 主程序执行到一半,突然跳转到信号处理函数,处理完后再切回主程序继续执行。这种特性极易引发函数重入问题,导致数据错乱、程序崩溃,这也是信号处理中最容易踩的坑。
当一个函数被不同的控制流程调用,在第一次调用还未执行完毕时,再次进入该函数执行,这种现象称为函数重入。
在信号场景中,重入的典型场景是:
主程序正在执行函数 A → 产生信号,触发信号处理函数 → 信号处理函数中又调用了函数 A → 函数 A 发生重入。
用通俗的话讲:可重入函数是 “独来独往” 的,只访问自己的局部变量和参数;不可重入函数是 “爱占共享资源” 的,会访问全局 / 静态资源,或调用其他不可重入函数。
我们以链表插入为例,模拟信号处理中最典型的重入问题,直观感受数据错乱的原因。
主程序调用insert函数向全局链表插入节点node1,执行到一半时,被信号处理函数打断;信号处理函数也调用insert函数向同一个全局链表插入节点node2,执行完成后切回主程序;主程序继续执行insert的剩余逻辑,最终导致链表数据错乱。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
using namespace std;
// 定义链表节点结构
typedef struct node
{
int val;
struct node* next;
} node_t;
// 全局链表头节点(共享资源)
node_t* head = NULL;
// 定义两个全局节点
node_t node1 = {10, NULL};
node_t node2 = {20, NULL};
// 链表插入函数(向头部插入,不可重入)
void insert(node_t* p)
{
// 步骤1:将新节点的next指向原头节点
p->next = head;
// 模拟耗时操作,增加信号中断的概率
usleep(100000);
// 步骤2:将头节点更新为新节点
head = p;
cout << "插入节点" << p->val << "完成" << endl;
}
// 信号处理函数:调用insert插入node2
void sig_handler(int signo)
{
cout << "\n信号处理函数执行:插入node2" << endl;
insert(&node2);
}
int main()
{
// 注册SIGINT信号处理函数,按下Ctrl+C触发
signal(SIGINT, sig_handler);
cout << "进程PID:" << getpid() << endl;
cout << "主程序执行:插入node1(按下Ctrl+C触发信号)" << endl;
// 主程序插入node1,大概率会被信号中断
insert(&node1);
// 遍历链表,查看结果
cout << "\n最终链表:";
node_t* cur = head;
while (cur)
{
cout << cur->val << " -> ";
cur = cur->next;
}
cout << "NULL" << endl;
return 0;
}g++ reentrant.c -o reentrant
./reentrant运行后立即按下 Ctrl+C,触发信号中断,输出结果如下:
进程PID:12345
主程序执行:插入node1(按下Ctrl+C触发信号)
信号处理函数执行:插入node2
插入节点20完成
插入节点10完成
最终链表:10 -> NULL从结果可以看到,节点 20 明明插入成功,但最终链表中只有节点 10,数据发生了错乱,根源在于insert 函数是不可重入的,且访问了全局链表头节点,具体执行流程如下:
insert(&node1),完成步骤 1:node1->next = NULL(原 head 为 NULL),随后进入耗时操作;sig_handler;insert(&node2),完成步骤 1:node2->next = NULL,步骤 2:head = &node2,插入完成,此时head指向 node2;insert(&node1)的步骤 2:head = &node1,将head重新指向 node1;node2被 “覆盖”,链表中只有node1,数据错乱。
只要满足以下任意一个条件,该函数就是不可重入的,绝对不能在信号处理函数中调用:
head,多个流程同时修改会导致数据不一致;要编写支持信号场景的可重入函数,需遵循以下核心原则,简单来说就是 “不碰共享资源,只靠自己”:
在信号处理中,避免重入问题的核心思路是 **“让信号处理函数尽可能简单”**,具体有三种解决方案,优先级从高到低:
信号处理函数不执行任何复杂逻辑,仅设置一个全局标志位,主程序轮询该标志位,检测到标志位被置位后,再在主程序中执行具体的处理逻辑。核心优势:信号处理函数执行时间极短,被中断的概率低,且主程序的处理逻辑在单流程中执行,无重入问题。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
using namespace std;
typedef struct node
{
int val;
struct node* next;
} node_t;
node_t* head = NULL;
node_t node1 = {10, NULL};
node_t node2 = {20, NULL};
// 全局标志位:标记是否收到SIGINT信号
volatile sig_atomic_t sig_received = 0;
// 链表插入函数(不可重入,仅在主程序中调用)
void insert(node_t* p)
{
p->next = head;
usleep(100000);
head = p;
cout << "插入节点" << p->val << "完成" << endl;
}
// 信号处理函数:仅设置标志位(原子操作)
void sig_handler(int signo)
{
sig_received = 1;
cout << "\n收到SIGINT信号,置位标志位" << endl;
}
int main()
{
signal(SIGINT, sig_handler);
cout << "进程PID:" << getpid() << endl;
cout << "主程序执行:插入node1(按下Ctrl+C触发信号)" << endl;
// 主程序插入node1
insert(&node1);
// 轮询标志位,检测到信号后执行后续处理
if (sig_received)
{
cout << "主程序检测到信号,插入node2" << endl;
insert(&node2);
}
// 遍历链表
cout << "\n最终链表:";
node_t* cur = head;
while (cur)
{
cout << cur->val << " -> ";
cur = cur->next;
}
cout << "NULL" << endl;
return 0;
} 运行结果:无论何时按下 Ctrl+C,链表插入都不会错乱,因为insert函数仅在主程序单流程中执行。
使用sigaction注册信号处理函数时,通过sa_mask字段设置临时屏蔽集,在信号处理函数执行期间,屏蔽当前信号和其他可能引发重入的信号,避免嵌套中断。核心优势:从根本上阻止了重入的发生,适合必须在信号处理函数中执行少量逻辑的场景。
若信号处理函数中必须执行 I/O、内存操作,优先使用系统调用(如 write、read、brk)替代标准库函数(如 printf、malloc),系统调用是内核实现的,具有原子性和可重入性。注意:系统调用的功能较为基础,需要自己封装上层逻辑。
答案:
write(STDOUT_FILENO, ...)系统调用实现打印,write 是可重入的。#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;
// 信号处理函数中使用write系统调用打印
void sig_handler(int signo)
{
const char* msg = "收到SIGINT信号,安全打印\n";
// write是系统调用,可重入
write(STDOUT_FILENO, msg, strlen(msg));
}
int main()
{
signal(SIGINT, sig_handler);
while (true)
{
cout << "主程序正常打印..." << endl;
sleep(1);
}
return 0;
} 在信号处理中,我们经常会用全局标志位来实现主程序和信号处理函数的通信,但在开启编译器优化后(如-O2),会出现标志位被修改但主程序检测不到的情况,信号仿佛 “失效” 了 —— 这一问题的根源是编译器的寄存器优化,而解决它的关键就是volatile 关键字。
我们先看一个简单的案例,直观感受问题现象:主程序轮询全局标志位flag,信号处理函数修改flag,开启编译器优化后,主程序永远检测不到flag的变化。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 全局标志位,未加volatile
int flag = 0;
// 信号处理函数:将flag从0置1
void sig_handler(int sig)
{
printf("信号处理函数:flag = 0 → 1\n");
flag = 1;
}
int main()
{
// 注册SIGINT信号,Ctrl+C触发
signal(SIGINT, sig_handler);
printf("进程PID:%d,flag初始值:%d\n", getpid(), flag);
printf("按下Ctrl+C修改flag,主程序轮询flag...\n");
// 轮询flag,为0则一直循环
while (!flag);
// 若flag置1,退出循环并打印
printf("主程序检测到flag=1,进程正常退出\n");
return 0;
}gcc volatile.c -o volatile
./volatile运行后按下 Ctrl+C,输出结果如下:
进程PID:12346,flag初始值:0
按下Ctrl+C修改flag,主程序轮询flag...
^C信号处理函数:flag = 0 → 1
主程序检测到flag=1,进程正常退出 现象:信号处理函数修改flag后,主程序检测到变化,退出循环,程序正常结束。
gcc volatile.c -o volatile -O2
./volatile运行后按下 Ctrl+C,输出结果如下:
进程PID:12347,flag初始值:0
按下Ctrl+C修改flag,主程序轮询flag...
^C信号处理函数:flag = 0 → 1
^C信号处理函数:flag = 0 → 1
^C信号处理函数:flag = 0 → 1 现象:信号处理函数确实执行了,flag被修改为 1,但主程序的while (!flag)循环永远执行,检测不到flag的变化,信号仿佛 “失效” 了。
为什么开启-O2优化后会出现这种问题?核心原因是编译器对全局变量的寄存器缓存优化,具体分析如下:
while (!flag)是一个死循环,编译器在-O2优化下,会认为flag是一个只读变量(主程序中没有修改flag的代码);flag的值缓存到 CPU 的寄存器中,主程序的while循环直接检测寄存器中的值,而不是内存中的实际值;flag的变化,循环无法退出。简单来说:编译器优化让主程序 “读不到” 内存中被信号处理函数修改后的真实值,导致数据不一致。
volatile是 C/C++ 的关键字,中文译为 “易变的”,其核心作用是:
告知编译器,被该关键字修饰的变量是 “易变的”,不允许对其进行寄存器缓存优化,对该变量的任何读 / 写操作,都必须直接操作/访问内存中的实际值,而不能操作寄存器中的缓存值。
简单来说,volatile打破了编译器的寄存器优化,保证了变量的内存可见性—— 任何流程对变量的修改,其他流程都能立即看到内存中的真实值。
只需将全局标志位flag用volatile修饰,即可解决上述问题,修改后的核心代码如下:
// 加volatile修饰,禁止编译器优化
volatile int flag = 0;gcc volatile.c -o volatile -O2
./volatile运行后按下 Ctrl+C,输出结果如下:
进程PID:12348,flag初始值:0
按下Ctrl+C修改flag,主程序轮询flag...
^C信号处理函数:flag = 0 → 1
主程序检测到flag=1,进程正常退出 现象:即使开启-O2优化,主程序也能立即检测到内存中flag的变化,循环退出,程序正常结束。
在 Linux 进程信号开发中,volatile几乎是全局标志位的 “标配”,使用时需遵循以下规范,避免误用:
只有主程序和信号处理函数共享的变量(如标志位)需要加volatile,局部变量无需加 —— 局部变量存储在栈中,每个控制流程有独立的栈,不会被编译器优化到寄存器。
sig_atomic_t类型使用(更安全) sig_atomic_t是 C 标准定义的原子数据类型,其特点是:对该类型变量的读 / 写操作是原子的,不会被信号中断。
在信号场景中,推荐将volatile和sig_atomic_t结合使用,定义全局标志位:
// 信号通信的全局标志位:volatile保证内存可见性,sig_atomic_t保证操作原子性
volatile sig_atomic_t flag = 0; 为什么需要原子性?若变量是 int 类型(4 字节),某些 CPU 对其的写操作可能分为 “低 2 字节 + 高 2 字节” 两步,若在第一步执行后被信号中断,会导致变量值不完整;而sig_atomic_t的操作是一步完成的,避免了这种问题。
重要误区:很多开发者认为volatile能保证多线程 / 多流程的同步,这是错误的!
volatile的唯一作用:保证变量的内存可见性,禁止编译器优化;volatile不保证操作的原子性(除了sig_atomic_t),也不保证多流程的同步性;volatile变量,仍会导致数据竞争,需要通过锁、信号量等同步机制保护。简单来说:volatile 解决 “读不到真实值” 的问题,同步机制解决 “多流程同时修改” 的问题。
volatile会禁止编译器对变量的优化,增加了内存访问的开销,因此仅在异步场景(信号、多线程、中断)中使用,普通同步场景无需加,避免不必要的性能损耗。
答案:
volatile修饰主程序和信号处理函数共享的全局 / 静态标志位,并结合sig_atomic_t类型,保证操作的原子性,定义为volatile sig_atomic_t flag = 0;;在 Linux 进程管理中,僵尸进程是一个经典问题:子进程退出后,父进程未及时回收其退出状态,子进程的 PCB 会一直保留在系统中,占用系统资源,最终导致系统资源耗尽。
传统的解决方案是父进程主动调用 wait/waitpid(阻塞或轮询),但阻塞会导致父进程无法处理自身工作,轮询会增加程序复杂度 —— 而SIGCHLD 信号为我们提供了一种异步、优雅的解决方案,让父进程 “被动接收” 子进程的退出通知,按需回收。
子进程退出后,会向父进程发送SIGCHLD 信号,并将自己置为僵尸状态(Zombie),等待父进程回收;若父进程:
则子进程的 PCB 会一直保留,成为僵尸进程。
wait(NULL),父进程阻塞等待子进程退出,期间无法处理自身工作,效率低下;waitpid(-1, NULL, WNOHANG),父进程轮询检测子进程是否退出,需要写循环逻辑,程序复杂度高,且存在轮询开销。SIGCHLD 是 Linux 的17 号信号,是子进程退出时向父进程发送的通知信号,其核心特性如下:
这是最常用的解决方案:父进程自定义 SIGCHLD 的处理函数,在函数中调用非阻塞式 waitpid回收所有退出的子进程,主进程可正常处理自身工作,无需阻塞 / 轮询。
WNOHANG):因为多个子进程可能同时退出,会发送多个 SIGCHLD 信号,而常规信号不支持排队,只会触发一次处理函数,因此需要循环调用 waitpid,回收所有退出的子进程;status获取,可解析子进程的退出码、终止信号等。#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
// SIGCHLD信号处理函数:异步回收所有退出的子进程
void sigchld_handler(int sig)
{
const char* msg = "SIGCHLD信号触发,开始回收子进程...\n";
write(STDOUT_FILENO, msg, strlen(msg));
pid_t cpid;
// 循环非阻塞回收:WNOHANG表示非阻塞,-1表示回收所有子进程
while ((cpid = waitpid(-1, NULL, WNOHANG)) > 0)
{
char buf[128];
snprintf(buf, sizeof(buf), "成功回收子进程,PID:%d\n", cpid);
write(STDOUT_FILENO, buf, strlen(buf));
}
}
int main()
{
// 1. 自定义捕捉SIGCHLD信号
signal(SIGCHLD, sigchld_handler);
printf("父进程PID:%d,创建3个子进程...\n", getpid());
// 2. 创建3个子进程
for (int i = 0; i < 3; i++)
{
pid_t pid = fork();
if (pid == 0)
{
// 子进程:运行3秒后退出
printf("子进程PID:%d,运行3秒后退出\n", getpid());
sleep(3);
exit(0);
}
else if (pid < 0)
{
perror("fork failed");
exit(1);
}
sleep(1); // 避免子进程同时创建
}
// 3. 父进程正常处理自身工作(死循环模拟)
while (true)
{
printf("父进程正在处理工作...\n");
sleep(2);
}
return 0;
}gcc sigchld.c -o sigchld
./sigchld输出结果如下:
父进程PID:12349,创建3个子进程...
子进程PID:12350,运行3秒后退出
父进程正在处理工作...
子进程PID:12351,运行3秒后退出
父进程正在处理工作...
子进程PID:12352,运行3秒后退出
父进程正在处理工作...
SIGCHLD信号触发,开始回收子进程...
成功回收子进程,PID:12350
成功回收子进程,PID:12351
成功回收子进程,PID:12352
父进程正在处理工作...
父进程正在处理工作... 这是一种更简洁的解决方案:Linux 系统中,若父进程通过sigaction将 SIGCHLD 的处理动作显式置为SIG_IGN(忽略),则子进程退出后会自动回收,不会产生僵尸进程,且父进程无需做任何额外操作。
signal(SIGCHLD, SIG_IGN),部分系统中该方式不生效;#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
// 1. 显式将SIGCHLD的处理动作置为SIG_IGN,让内核自动回收子进程
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = SIG_IGN; // 显式忽略
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
printf("父进程PID:%d,创建3个子进程,内核自动回收僵尸进程...\n", getpid());
// 2. 创建3个子进程
for (int i = 0; i < 3; i++)
{
pid_t pid = fork();
if (pid == 0)
{
printf("子进程PID:%d,运行2秒后退出\n", getpid());
sleep(2);
exit(0);
}
else if (pid < 0)
{
perror("fork failed");
exit(1);
}
sleep(1);
}
// 3. 父进程正常处理工作
while (true)
{
printf("父进程正在处理工作...\n");
sleep(2);
}
return 0;
}ps -aux | grep defunct(defunct 表示僵尸进程); 在 SIGCHLD 的处理函数中,我们可以通过waitpid的第二个参数status解析子进程的退出原因和退出码,便于调试和日志记录,核心解析宏如下:
宏函数 | 作用 |
|---|---|
WIFEXITED(status) | 判断子进程是否正常退出(如 exit、return),是则返回非 0,否则返回 0 |
WEXITSTATUS(status) | 若 WIFEXITED 为真,获取子进程的正常退出码(exit 的参数) |
WIFSIGNALED(status) | 判断子进程是否被信号终止,是则返回非 0,否则返回 0 |
WTERMSIG(status) | 若 WIFSIGNALED 为真,获取终止子进程的信号编号 |
WCOREDUMP(status) | 判断子进程终止时是否产生 core dump 文件,是则返回非 0 |
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
void sigchld_handler(int sig)
{
pid_t cpid;
int status;
while ((cpid = waitpid(-1, &status, WNOHANG)) > 0)
{
char buf[256];
if (WIFEXITED(status))
{
// 正常退出
snprintf(buf, sizeof(buf), "子进程%d正常退出,退出码:%d\n", cpid, WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
{
// 被信号终止
snprintf(buf, sizeof(buf), "子进程%d被信号终止,信号编号:%d\n", cpid, WTERMSIG(status));
}
write(STDOUT_FILENO, buf, strlen(buf));
}
}
int main()
{
signal(SIGCHLD, sigchld_handler);
printf("父进程PID:%d\n", getpid());
// 创建子进程1:正常退出,退出码5
pid_t pid1 = fork();
if (pid1 == 0)
{
sleep(2);
exit(5);
}
// 创建子进程2:被SIGKILL信号终止
pid_t pid2 = fork();
if (pid2 == 0)
{
while (true) { sleep(1); } // 死循环
}
// 父进程3秒后杀掉子进程2
sleep(3);
kill(pid2, SIGKILL);
while (true) { sleep(1); }
return 0;
}父进程PID:12353
子进程12354正常退出,退出码:5
子进程12355被信号终止,信号编号:9结果分析:成功解析出子进程 1 的正常退出码 5,以及子进程 2 被 9 号信号(SIGKILL)终止。
答案:会丢失。因为 SIGCHLD 是常规信号(17 号),常规信号不支持排队,多个信号产生后仅会记录一次,因此处理函数只会被触发一次。
解决方案:在处理函数中循环调用非阻塞式 waitpid,直到返回 0(无更多子进程可回收),确保回收所有退出的子进程。
答案:若使用阻塞式 wait (NULL),当处理函数被触发后,若此时没有子进程退出,wait 会阻塞处理函数,导致父进程无法处理其他信号,甚至卡死。而非阻塞式 waitpid(WNOHANG)会立即返回,不会阻塞。
答案:会。SIGCHLD 的触发条件包括子进程退出、暂停(SIGSTOP)、恢复(SIGCONT),因此在处理函数中,waitpid 可能会回收不到子进程(因为子进程只是暂停,未退出)。
解决方案:无需处理,waitpid 会返回 0,循环结束即可。
若在 SIGCHLD 的处理函数中调用 fork 创建子进程,可能会因信号屏蔽、栈空间等问题导致创建失败,建议所有子进程的创建逻辑都放在主程序中。
要真正理解 Linux 进程信号的所有细节,必须掌握用户态与内核态的核心概念,以及系统调用的底层原理 —— 信号的产生、捕捉、处理全程都伴随着用户态和内核态的切换,而信号相关的函数(如 sigaction、kill)本质上都是对系统调用的封装。
Linux 作为多任务操作系统,为了保证系统的安全性和稳定性,将 CPU 的执行权限分为内核态(Ring 0)和用户态(Ring 3)(基于 Intel CPU 的权限分级),核心目的是:
限制用户权限,防止用户进程随意操作硬件、修改内核数据,导致系统崩溃或被攻击。
简单来说,内核态是 “超级管理员”,拥有最高权限;用户态是 “普通用户”,权限受限,只能做自己的事。
对比维度 | 内核态(Ring 0) | 用户态(Ring 3) |
|---|---|---|
权限等级 | 最高权限,无限制 | 低权限,严格受限 |
内存访问 | 可访问整个虚拟地址空间(0-4G),包括内核空间(3-4G)和用户空间(0-3G) | 仅能访问用户空间(0-3G),无法访问内核空间(3-4G) |
执行代码 | 操作系统内核代码、驱动程序、系统调用 | 用户应用程序代码、标准库函数 |
触发方式 | 系统调用、中断、异常 | 进程启动后默认的执行状态 |
错误影响 | 执行错误会导致系统崩溃(如内核 panic) | 执行错误仅会导致当前进程崩溃,不影响系统 |
32 位 Linux 系统的虚拟地址空间大小为4GB,内核将其划分为两部分,所有进程共享相同的内核空间,拥有独立的用户空间:

核心结论:操作系统的内核代码是所有进程共享的,运行在每个进程的虚拟地址空间中—— 这也是进程能快速进行态切换的根本原因。
进程的用户态和内核态并非固定不变,而是会根据执行需求相互切换,信号的处理全程都伴随着态切换(如信号捕捉的 “两进两出”),核心切换场景如下:
这是提权的过程,进程从低权限切换到高权限,只能通过以下 3 种方式触发,无其他途径:
int 0x80或syscall指令触发软中断,进入内核态;信号相关的触发:
这是降权的过程,进程处理完内核态的工作后,主动切回用户态,继续执行原流程:
中断 / 系统调用 / 异常处理完成后,内核恢复进程的用户态执行上下文(寄存器、程序计数器等),CPU 切换回用户态。
信号相关的切换:
我们在代码中调用的所有信号相关函数(如 signal、sigaction、kill)、进程管理函数(如 fork、waitpid),本质上都是标准库对内核系统调用的封装—— 库函数为我们屏蔽了底层的汇编指令、寄存器操作,提供了简洁的 C 语言接口。
以fopen 函数为例,我们追踪其底层实现,看一个库函数是如何最终调用系统调用的:
fopen("test.txt", "r")(标准 I/O 库函数);__fopen_internal,再调用_IO_file_fopen;syscall汇编指令触发软中断,进入内核态;sys_open函数,完成文件打开操作,返回文件描述符给用户态。核心结论:所有对内核资源的操作,最终都必须通过系统调用进入内核态执行,用户态无法直接操作内核资源。
信号捕捉的完整流程,本质上是用户态与内核态的四次切换(两进两出),再次回顾加深理解:
sigreturn系统调用,再次进入内核态;Linux 进程信号是操作系统的核心知识点,从基础的产生、保存、捕捉,到进阶的可重入函数、volatile、SIGCHLD,每一个知识点都关联着底层原理,没有捷径可走,唯有理解原理 + 亲手实战才能真正掌握。 本文的所有案例都经过实际环境验证,建议大家亲手编译运行,通过修改代码参数(如开启 / 关闭优化、修改信号处理逻辑)加深理解;同时,在实际开发中,一定要遵循信号处理的极简原则—— 信号处理函数越简单,出现问题的概率越低。 信号的学习并未结束,后续还可以深入研究实时信号(34-64)、信号与线程的交互、sigqueue 发送带参数的信号等进阶内容,关注我,持续解锁 Linux 内核与 C/C++ 开发的核心干货!