跳到主要内容
Linux 进程信号入门:从原理到实战 | 极客日志
C++
Linux 进程信号入门:从原理到实战 Linux 进程信号是操作系统实现异步通信的核心机制,常被称为软中断。本文通过生活场景类比快递流程,解析信号的产生、暂存与处理逻辑。结合 C++ 实战代码,演示了 SIGINT、SIGQUIT 等常见终端信号的默认行为,并深入讲解了如何使用 signal 函数自定义信号处理方式,包括忽略、捕捉及默认动作。同时阐述了前台与后台进程的信号差异,以及信号异步性的内核态切换时机,帮助读者建立扎实的底层认知。
Linux 进程信号入门:从原理到实战
在 Linux 系统中,进程信号是实现进程间异步通信的核心机制,也是操作系统对进程进行事件通知的重要手段。无论是日常按下 Ctrl+C 终止进程,还是程序出现段错误、除零异常,背后都是信号在发挥作用。很多初学者觉得信号晦涩难懂,其实它的逻辑和我们生活中的场景高度相似。
一、从生活场景理解信号
想要搞懂 Linux 进程信号,不如先从身边的小事说起。信号的核心逻辑在生活中随处可见,理解了生活中的'信号处理',就能轻松迁移到 Linux 系统中。
1.1 快递的故事:完美映射信号处理流程
想象一个场景:你在家打游戏,网购的快递到了,快递员在楼下给你打电话通知取件。这个过程和 Linux 进程处理信号的逻辑几乎一模一样:
信号识别 :看到电话就知道是快递到了,不需要额外解释。这对应 Linux 进程对信号的内置识别能力,内核程序员在设计进程时,已经让进程能识别系统中所有合法信号,比如 Ctrl+C 对应的 2 号信号 SIGINT、段错误对应的 11 号信号 SIGSEGV。
信号延迟处理 :正在打关键团战,需要 5 分钟后才能下楼取件。这对应信号的异步特性,进程收到信号后,不会立即处理,而是在合适的时候处理(比如进程从内核态返回到用户态时),因为进程可能正在执行优先级更高的任务。
信号暂存 :5 分钟内没取快递,但心里记着'有快递要取'。这对应信号的暂存机制,进程收到信号后,如果暂时不处理,会将信号保存在进程控制块(PCB)中,直到处理时机到来。
异步特性 :无法准确知道快递员什么时候会打电话。这对应信号的异步性,信号对于进程的控制流程来说是异步的,进程无法预知信号何时到来,可能在执行代码的任意位置收到信号。
信号的三种处理方式 :等你打完游戏取到快递后,会有三种选择:
默认处理 :开心地拆开快递,使用里面的商品。对应进程对信号的默认动作,比如 SIGINT 的默认动作是终止进程,SIGSEGV 的默认动作是终止进程并生成核心转储文件。
自定义处理 :快递是零食,转手送给了女朋友。对应进程的自定义捕捉,通过注册信号处理函数,让进程收到信号后执行自己定义的逻辑。
忽略处理 :把快递拿回家扔在床头,继续打游戏。对应进程忽略信号,收到信号后不做任何操作,比如 SIGCHLD 信号的默认动作就是忽略。
1.2 生活场景到 Linux 信号的核心结论
从快递的故事,我们可以提炼出 Linux 进程信号的几个核心结论:
进程对信号的识别能力是内置的,无需后天学习,内核已经为进程预设了所有合法信号的识别逻辑;
信号的处理方法在信号产生前就已准备好,进程无论是否收到信号,都知道该如何处理每一种信号(默认/忽略/自定义);
信号处理并非立即执行,进程会在合适的时机处理,核心原因是信号具有异步性,且进程可能在执行高优先级任务;
信号的完整生命周期是:信号产生 → 信号保存 → 信号处理;
信号的处理方式只有三种:默认处理(SIG_DFL)、忽略处理(SIG_IGN)、自定义捕捉(Catch)。
二、技术视角:Linux 进程信号的初体验
理解了生活中的信号逻辑,我们回到 Linux 系统本身,通过实战代码和终端操作,直观感受进程信号的存在和作用,搞懂 Ctrl+C 终止进程的底层原理,以及如何通过代码修改信号的处理方式。
2.1 第一个实验:Ctrl+C 的本质 —— 向前台进程发送 2 号信号 SIGINT
我们先写一个简单的死循环 C++ 程序,让进程一直运行,然后通过 Ctrl+C 终止它,看看背后发生了什么。
代码实现:sig_hello.cpp
#include <iostream>
std;
{
( ) {
cout << << () << << endl;
( );
}
;
}
#include <unistd.h>
using
namespace
int main ()
while
true
"I am a process, my PID is: "
getpid
", waiting signal!"
sleep
1
return
0
编译运行
g++ sig_hello.cpp -o sig_hello
./sig_hello
运行后,终端会不断打印进程 PID 和提示信息,此时按下 Ctrl+C,进程会立即终止。这背后的底层逻辑是什么?
我们按下 Ctrl+C 时,键盘输入会产生一个硬件中断,被操作系统(OS)捕获;OS 将这个硬件中断解释为 2 号信号 SIGINT(中断信号);OS 将 SIGINT 信号发送给当前的前台进程(也就是我们运行的 sig_hello 进程);前台进程收到 SIGINT 信号后,执行默认处理动作——终止进程。
这就是 Ctrl+C 终止进程的完整流程,核心是 OS 将硬件操作转换为信号,发送给进程并触发默认处理。
2.2 第二个实验:修改信号处理方式 —— 让 Ctrl+C 不再终止进程 既然进程对信号的处理方式可以自定义,那我们能不能通过代码修改 SIGINT 信号的处理方式,让按下 Ctrl+C 后进程不终止,而是执行我们自定义的逻辑?答案是肯定的,我们可以使用 ANSI C 标准的 signal 函数来修改信号的处理动作,该函数的作用是为指定信号注册自定义处理函数。
2.2.1 signal 函数介绍 #include <signal.h>
typedef void (*sighandler_t ) (int ) ;
sighandler_t signal (int signum, sighandler_t handler) ;
signum:要处理的信号编号,比如 2 代表 SIGINT,3 代表 SIGQUIT;
handler:信号处理函数的指针,有三种取值:
自定义函数指针:进程收到信号后执行该函数;
SIG_IGN:忽略该信号;
SIG_DFL:执行该信号的默认处理动作。
返回值 :成功返回该信号原来的处理函数指针,失败返回 SIG_ERR。
2.2.2 代码实现:sig_catch.cpp #include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sig_handler (int signum) {
cout << "我是进程 [" << getpid () << "],捕获到信号:" << signum << ",我没有终止!" << endl;
}
int main () {
cout << "我是主进程,PID:" << getpid () << endl;
signal (SIGINT, sig_handler);
while (true ) {
cout << "进程正在运行,等待信号..." << endl;
sleep (1 );
}
return 0 ;
}
2.2.3 编译运行 g++ sig_catch.cpp -o sig_catch
./sig_catch
运行后,再次按下 Ctrl+C,会发现进程不再终止,而是打印我们自定义的信息,比如:
我是主进程,PID:12345
进程正在运行,等待信号...
进程正在运行,等待信号...
^C我是进程 [12345] ,捕获到信号:2 ,我没有终止!
进程正在运行,等待信号...
2.2.4 实验思考:为什么进程不终止了? 因为我们通过 signal 函数将 SIGINT 信号的处理方式从默认终止修改为自定义执行 sig_handler 函数,进程收到信号后,会执行我们定义的逻辑,而不是默认的终止动作。
这个实验也印证了一个重要点:signal 函数只是设置信号的捕捉行为,并不是直接调用处理函数。如果信号没有产生,注册的自定义函数永远不会被执行。
2.3 关键知识点:前台进程与后台进程的信号差异 在上面的实验中,我们提到 Ctrl+C 产生的信号只能发送给前台进程,这是 Linux 系统的一个重要规则,我们需要明确前台进程和后台进程的信号差异:
前台进程 :在 Shell 中直接运行的进程,占据终端的输入输出,能接收终端控制键产生的信号,比如 Ctrl+C(SIGINT)、Ctrl+\(SIGQUIT)、Ctrl+Z(SIGTSTP);
后台进程 :在命令后加 & 运行的进程,不占据终端的输入输出,无法接收终端控制键产生的信号,比如 ./sig_catch &;
Shell 的进程管理规则 :Shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程能接收终端的信号。
实战验证:后台进程不接收 Ctrl+C 信号
./sig_catch &
jobs
kill -2 进程 PID
三、Linux 信号的核心概念:你必须知道的基础定义 通过前面的实战,我们已经直观感受了信号的作用,接下来我们正式定义 Linux 进程信号的核心概念,为后续深入学习打下理论基础,这些概念是理解信号的基石。
3.1 信号的官方定义 信号是进程之间事件异步通知的一种方式,属于软中断。
异步通知 :进程无需主动轮询,信号会在任意时刻到来,进程在合适的时机处理即可;
软中断 :信号是软件层面模拟硬件中断的机制,硬件中断是发给 CPU 的,而信号是发给进程的,两者的处理流程相似,但作用层级不同。
3.2 如何查看 Linux 系统中的所有信号 Linux 系统为进程预设了大量信号,编号从 1 开始,其中 34 号及以上为实时信号,34 号以下为常规信号(也叫非实时信号),我们日常开发中主要使用常规信号。
执行 kill -l 后,终端会输出 Linux 系统的所有信号,部分关键常规信号如下:
信号编号 信号名 产生条件 默认动作 1 SIGHUP 进程所属的终端挂起 终止进程 2 SIGINT 终端按下 Ctrl+C 终止进程 3 SIGQUIT 终端按下 Ctrl+\ 终止进程并生成 core dump 9 SIGKILL kill -9 进程 PID 终止进程(不可捕捉、不可忽略) 11 SIGSEGV 非法内存访问(段错误) 终止进程并生成 core dump 14 SIGALRM 闹钟超时(alarm 函数) 终止进程 15 SIGTERM kill 进程 PID(默认信号) 终止进程(可捕捉、可忽略) 17 SIGCHLD 子进程终止 / 暂停 忽略信号 20 SIGTSTP 终端按下 Ctrl+Z 暂停进程
重要注意 :9 号信号 SIGKILL 和 19 号信号 SIGSTOP 是 Linux 系统中不可捕捉、不可忽略的信号,这是为了保证操作系统能绝对控制进程,避免进程通过自定义信号处理方式变成'无法杀死的僵尸进程'。
3.3 信号的三种处理动作 正如我们在生活场景中总结的,Linux 进程对信号的处理动作只有三种,这是内核为进程预设的基本规则,所有信号都遵循这个规则:
3.3.1 执行默认动作(SIG_DFL) 这是绝大多数信号的默认处理方式,不同信号的默认动作不同,主要包括:
Term :终止进程(如 SIGINT、SIGTERM、SIGALRM);
Core :终止进程并生成 core dump 文件(如 SIGQUIT、SIGSEGV、SIGFPE),core dump 文件用于事后调试;
Stop :暂停进程(如 SIGTSTP、SIGSTOP);
Cont :继续运行暂停的进程(如 SIGCONT);
Ign :忽略信号(如 SIGCHLD)。
3.3.2 忽略信号(SIG_IGN) 进程收到信号后,不做任何操作,直接忽略。比如我们可以通过 signal(SIGINT, SIG_IGN) 让进程忽略 Ctrl+C 信号,此时按下 Ctrl+C,进程不会有任何反应。
代码验证:忽略 SIGINT 信号 #include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main () {
cout << "进程 PID:" << getpid () << ",已忽略 SIGINT 信号" << endl;
signal (SIGINT, SIG_IGN);
while (true ) {
cout << "进程正在运行..." << endl;
sleep (1 );
}
return 0 ;
}
编译运行后,无论按多少次 Ctrl+C,进程都不会终止,因为进程已经忽略了 SIGINT 信号。
3.3.3 自定义捕捉(Catch) 通过 signal 或 sigaction 函数为信号注册自定义处理函数,进程收到信号后,执行我们定义的逻辑,这是信号最灵活的处理方式,也是开发中最常用的方式。
前面的 sig_catch.cpp 就是自定义捕捉的典型例子,这里再补充一个关键点:自定义处理函数的参数。自定义信号处理函数的格式是 void handler(int signum),其中 signum 参数会自动传入当前收到的信号编号,这让我们可以用一个函数处理多个信号。
代码验证:一个函数处理多个信号 #include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sig_handler (int signum) {
switch (signum) {
case 2 :
cout << "捕获到 SIGINT(2 号)信号,Ctrl+C 无效!" << endl;
break ;
case 3 :
cout << "捕获到 SIGQUIT(3 号)信号,Ctrl+\\ 无效!" << endl;
break ;
case 20 :
cout << "捕获到 SIGTSTP(20 号)信号,Ctrl+Z 无效!" << endl;
break ;
default :
cout << "捕获到未知信号:" << signum << endl;
break ;
}
}
int main () {
cout << "进程 PID:" << getpid () << ",已注册多信号处理函数" << endl;
signal (SIGINT, sig_handler);
signal (SIGQUIT, sig_handler);
signal (SIGTSTP, sig_handler);
while (true ) {
cout << "进程正在运行..." << endl;
sleep (1 );
}
return 0 ;
}
编译运行后,按下 Ctrl+C、Ctrl+\、Ctrl+Z,进程都会执行对应的自定义逻辑,而不是默认动作,实现了一个函数处理多个信号的效果。
3.4 信号的宏定义 在 Linux 系统中,所有信号的名称都是宏定义,对应的头文件是 <signal.h>,宏定义的本质是将信号编号转换为易记的名称,方便开发人员使用。
#define SIGINT 2
#define SIGQUIT 3
#define SIGKILL 9
#define SIGSEGV 11
#define SIGTERM 15
#define SIGCHLD 17
#define SIGTSTP 20
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
可以看到,SIG_DFL 和 SIG_IGN 本质上是将 0 和 1 强制转换为信号处理函数的指针类型,让它们能作为 signal 函数的第二个参数。
四、信号的异步性:进程控制流程的'意外插曲' 信号的异步性是其最核心的特性之一,也是理解信号的关键难点,我们需要单独拿出来重点讲解,因为它决定了信号的处理时机和方式。
4.1 什么是信号的异步性
同步执行 :进程的代码按顺序执行,执行完一行再执行下一行,流程完全可控;
异步事件 :信号会在任意时刻到来,进程无法预知信号的产生时间,可能在执行代码的任意位置收到信号,比如进程在执行循环、函数调用、系统调用时,都可能收到信号。
举个例子:进程正在执行 for 循环的第 100 次迭代,此时 OS 向进程发送了 SIGINT 信号,进程会在合适的时机暂停循环,处理信号,处理完成后再继续执行循环的第 100 次迭代,这就是异步性的体现。
4.2 为什么信号不能立即处理 很多初学者会有疑问:进程收到信号后,为什么不立即处理,而是要等到'合适的时机'?主要有两个原因:
进程可能在执行高优先级任务 :比如进程正在执行内核态的代码(如系统调用),此时处理信号可能会破坏内核数据结构,导致系统崩溃,因此需要等进程从内核态返回到用户态时再处理;
信号处理的原子性 :如果进程在执行一个不可中断的操作(如修改全局变量),此时处理信号可能会导致数据不一致,因此需要等操作完成后再处理。
4.3 信号的处理时机:内核态 → 用户态的切换时刻
用户态 :进程执行自己的用户代码(如我们写的 C/C++ 代码),权限较低,不能直接访问硬件和内核数据;
内核态 :进程执行内核代码(如系统调用、中断处理),权限较高,可以访问硬件和内核数据。
进程调用 read、write、sleep 等系统调用时,会从用户态切换到内核态;
系统调用执行完成后,进程会从内核态切换回用户态。
信号的处理时机 :进程从内核态返回到用户态的瞬间,OS 会检查进程的 PCB 中是否有未处理的信号,如果有,就会先处理信号,处理完成后再返回到用户态执行进程的正常代码。
这是 Linux 系统规定的唯一信号处理时机,也是保证信号处理安全性的关键。
五、常见终端信号的实战:除了 Ctrl+C,还有这些操作 除了 Ctrl+C 对应的 SIGINT 信号,我们在终端中常用的 Ctrl+\ 和 Ctrl+Z 也会产生对应的信号,分别是 SIGQUIT(3 号)和 SIGTSTP(20 号),我们来实战验证这两个信号的作用,进一步加深对信号的理解。
5.1 Ctrl+\:SIGQUIT 信号,终止进程并生成 core dump SIGQUIT 信号的默认动作是终止进程并生成 core dump 文件,core dump 文件是进程的内存镜像文件,包含了进程终止时的内存数据、寄存器状态等信息,用于事后调试(Post-mortem Debug)。
实战验证:SIGQUIT 信号的默认动作
./sig_hello
^\Quit (core dumped)
ls -l core*
注意 :Linux 系统默认关闭 core dump 功能,若没有生成 core 文件,可以通过以下命令开启:
5.2 Ctrl+Z:SIGTSTP 信号,暂停前台进程 SIGTSTP 信号的默认动作是暂停前台进程,将进程从前台切换到后台,状态变为 Stopped,暂停的进程可以通过 fg 命令恢复到前台,或通过 bg 命令让其在后台继续运行。
实战验证:SIGTSTP 信号的默认动作
./sig_hello
^Z[1]+ Stopped ./sig_hello
jobs
fg %1
bg %1
kill -9 进程 PID
我们也可以通过 signal 函数自定义 SIGQUIT 和 SIGTSTP 信号的处理方式,让它们不再执行默认动作,代码和前面的 sig_catch.cpp 类似,只需将信号编号改为 3 和 20 即可。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online