Linux 进程信号深度解析(上):信号的产生与本质(含完整案例)

Linux 进程信号深度解析(上):信号的产生与本质(含完整案例)
在这里插入图片描述

🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在 Linux 系统中,信号是进程间异步通信的核心机制,也是操作系统响应异常、处理中断的关键手段。从用户按下Ctrl+C终止进程,到程序触发段错误崩溃,再到定时器超时通知,背后都是信号在发挥作用。本文从生活类比、核心概念、产生方式三个维度,带你吃透 Linux 信号的底层逻辑与实战用法。

一. 信号的快速认知:从生活场景到技术本质

1.1 生活角度理解信号

用 “快递收发” 的场景类比信号处理流程,瞬间理清核心逻辑:

  • 信号识别:你知道快递来了要取(进程能识别系统预设的信号,如SIGINT);
  • 信号产生:快递员打电话通知(信号由内核或其他进程发送);
  • 信号未决:你正在打游戏,5 分钟后才去取(信号产生后未立即处理,处于 “未决” 状态);
  • 信号递达:游戏结束后取快递(进程在合适时机执行信号处理动作);
  • 处理方式
    • 默认动作(打开快递使用);
    • 自定义动作(送给朋友);
    • 忽略(扔在一边)。

核心结论:信号是异步通知,进程无法预知信号何时到来,但提前知道如何处理。

在这里插入图片描述

1.2 技术视角的信号定义

信号是 Linux 中进程间事件异步通知的一种方式,属于 “软中断”—— 模拟硬件中断的行为(硬件中断发给 CPU,信号发给进程),用于处理突发事件(如用户中断、程序异常、定时触发等)。

关键特性:

  • 异步性:信号的产生与进程的控制流程无关,进程执行到任意位置都可能收到信号;
  • 预定义动作:每个信号都有默认处理动作(终止、忽略、Core Dump 等),进程可自定义处理逻辑;
  • 内核转发:信号的产生、发送、递达均由内核管理,进程仅需关注处理动作;
  • 前台进程专属Ctrl+C等终端按键产生的信号,仅发送给前台进程(后台进程需用kill命令发送)。

1.3 查看系统信号:kill -l 命令

Linux 系统支持 64 种信号(34 以下为常规信号,34 以上为实时信号),通过kill -l可查看所有信号的编号和名称:

在这里插入图片描述
kill-l# 输出示例(核心信号):1) SIGHUP 2) SIGINT 3) SIGQUIT 9) SIGKILL 11) SIGSEGV 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 20) SIGTSTP 

核心信号说明(开发高频用到)

信号编号信号名称产生场景默认动作
2SIGINT按下 Ctrl+C终止进程
3SIGQUIT按下 Ctrl+\终止进程 + Core Dump
6SIGABRTabort 函数调用终止进程 + Core Dump
8SIGFPE算术异常(如除零)终止进程 + Core Dump
9SIGKILLkill -9 PID 命令强制终止进程(不可捕捉/忽略)
11SIGSEGV非法内存访问(野指针)终止进程 + Core Dump
14SIGALRMalarm 函数超时终止进程
15SIGTERMkill PID 命令默认信号终止进程(可捕捉)
19SIGSTOPkill -STOP PID暂停进程(不可捕捉/忽略)
20SIGTSTP按下 Ctrl+Z暂停进程

二. 信号的产生:5 种核心方式(含完整案例)

在这里插入图片描述

信号的产生源于 “外部触发” 或 “内部异常”,下面结合实战案例逐一拆解:

  • 大家可以用我这里现在提供的案例代码去进行测试也可以直接用下面每一个板块给出的来测试(图中的一些测试是用的我这里给的)

testsig.cc

#include<iostream>#include<string>#include<unistd.h>#include<signal.h>// sig: 表示的是收到的信号编号voidhandlersig(int sig){ std::cout <<"哈哈, 我正在处理一个信号, pid: "<<getpid()<<" sig number: "<< sig << std::endl;}intmain(){for(int signo =0; signo <=31; signo++)signal(signo, handlersig);// signal(2, handlersig);// signal(2, SIG_DFL);// signal(2, SIG_IGN);// signal(3, handlersig);// signal(4, handlersig);// signal(11, handlersig);int a;while(true){// scanf("%d", &a); std::cout <<"我是一个进程, pid: "<<getpid()<< std::endl;sleep(1);// int *p = nullptr;// *p = 100; // 野指针报错 // int a = 10;// a /= 0; // 除0错误// raise(SIGINT); // 指定信号给当前进程// abort(); // 是6号信号给当前进程}}

mykill.cc

#include<iostream>#include<string>#include<signal.h>voidUsage(std::string proc){ std::cerr <<"Usage:\n\t"<< proc <<" signumber pid\n\n";}intmain(int argc,char* argv[]){if(argc !=3){Usage(argv[0]);return1;}int signumber = std::stoi(argv[1]);int pid = std::stoi(argv[2]); std::cout <<"send "<< signumber <<" to "<< pid << std::endl;int n =kill(pid, signumber);(void)n;return0;}
在这里插入图片描述
✅️这里只是简单的测试了一个场景,我也们还可以发9号信号试试,会发现依旧可以杀掉,这证明了9号信号不能被自定义捕捉和忽略。甚至测试别的场景的话在上述代码上做修改就行

前置储备

在这里插入图片描述


在这里插入图片描述

2.1 系统命令产生信号(kill 命令)

kill命令是发送信号的常用工具,本质是调用kill系统函数,语法:

kill -信号编号 进程PID kill -信号名称 进程PID 

实战案例:用 kill 命令发送 SIGSEGV 信号

// 程序:死循环运行,等待外部信号#include<iostream>#include<unistd.h>intmain(){ std::cout <<"进程PID:"<<getpid()<<",等待信号(用kill命令测试)..."<< std::endl;while(true){sleep(1);}return0;}

操作步骤

  • 编译运行程序,记录 PID(如 213784);
  • 打开新终端,发送 SIGSEGV(11 号信号,段错误):
kill-11213784# 或 kill -SIGSEGV 213784

原终端输出:Segmentation fault (core dumped),进程终止。

2.2 终端按键产生信号(键盘,最常用)

终端通过组合键产生预设信号,用于控制前台进程,核心组合键及对应信号:

在这里插入图片描述

2.2.1 Ctrl+C:SIGINT(2 号信号)

默认动作:终止前台进程,可通过signal函数自定义捕捉。

#include<iostream>#include<unistd.h>#include<signal.h>// 自定义信号处理函数voidsigint_handler(int signum){ std::cout <<"\n进程["<<getpid()<<"]捕获到SIGINT信号(编号:"<< signum <<"),未终止!"<< std::endl;}intmain(){ std::cout <<"进程PID:"<<getpid()<<",等待信号(按Ctrl+C测试)..."<< std::endl;// 注册SIGINT信号的处理函数signal(SIGINT, sigint_handler);// 死循环等待信号while(true){sleep(1); std::cout <<"运行中..."<< std::endl;}return0;}

编译运行

g++ sigint_demo.cc -o sigint_demo ./sigint_demo 

效果:按下Ctrl+C后,进程不会终止,而是执行自定义处理函数也就是打印出一段文字并继续运行。

2.2.2 Ctrl+\:SIGQUIT(3 号信号)

默认动作:终止进程 + 生成 Core Dump 文件(用于事后调试),同样可自定义捕捉。

#include<iostream>#include<unistd.h>#include<signal.h>voidsigquit_handler(int signum){ std::cout <<"\n进程["<<getpid()<<"]捕获到SIGQUIT信号(编号:"<< signum <<")"<< std::endl;}intmain(){ std::cout <<"进程PID:"<<getpid()<<",等待信号(按Ctrl+\\测试)..."<< std::endl;signal(SIGQUIT, sigquit_handler);while(true){sleep(1);}return0;}

关键说明:Core Dump 文件默认关闭,可通过ulimit -c 1024开启(允许最大 1024KB 的 Core 文件),调试时用gdb ./程序名 core.进程号分析。

2.1.3 Ctrl+Z:SIGTSTP(20 号信号)

默认动作:暂停前台进程,将其挂入后台,可通过fg命令恢复前台运行,也可以通过bg把这个后台进程运行起来。

#include<iostream>#include<unistd.h>#include<signal.h>voidsigtstp_handler(int signum){ std::cout <<"\n进程["<<getpid()<<"]捕获到SIGTSTP信号(编号:"<< signum <<")"<< std::endl;}intmain(){ std::cout <<"进程PID:"<<getpid()<<",等待信号(按Ctrl+Z测试)..."<< std::endl;signal(SIGTSTP, sigtstp_handler);while(true){sleep(1); std::cout <<"运行中..."<< std::endl;}return0;}

后台操作示例

# 运行程序后按Ctrl+Z,进程暂停[1]+ Stopped ./sigtstp_demo # 查看后台进程jobs# 将后台进程恢复到前台fg1# 将后台进程运行起来# bg 1

2.3 函数调用产生信号(编程触发)

通过系统函数主动发送信号,核心函数包括killraiseabort,适用于编程场景下的信号触发。

在这里插入图片描述

2.3.1 kill 函数:向指定进程发送信号

函数原型:

#include<sys/types.h>#include<signal.h>intkill(pid_t pid,int sig);
  • pid:目标进程 PID(正数);进程组 ID(负数,发送给组内所有进程);0(发送给当前进程组);-1(发送给所有有权限的进程);
  • sig:信号编号(0 表示检测进程是否存在,不发送信号);
  • 返回值:成功返回 0,失败返回 - 1。

实战:实现简易版 kill 命令(我最开始给的那个mykill也是可以的)

#include<iostream>#include<sys/types.h>#include<signal.h>#include<cstdlib>// 用法:./mykill -信号编号 进程PIDintmain(int argc,char* argv[]){if(argc !=3){ std::cerr <<"用法错误:"<< argv[0]<<" -signum pid"<< std::endl;return1;}// 解析信号编号(去掉前缀"-")int sig = std::stoi(argv[1]+1);// 解析目标进程PID pid_t pid = std::stoi(argv[2]);// 发送信号int ret =kill(pid, sig);if(ret ==0){ std::cout <<"向进程["<< pid <<"]发送信号["<< sig <<"]成功"<< std::endl;}else{perror("kill failed");return1;}return0;}

2.3.2 raise 函数:向当前进程发送信号

函数原型:

#include<signal.h>intraise(int sig);
  • 作用:自己给自己发送信号,等价于kill(getpid(), sig)
  • 返回值:成功返回 0,失败返回非 0。

实战案例

#include<iostream>#include<unistd.h>#include<signal.h>voidsig_handler(int signum){ std::cout <<"进程["<<getpid()<<"]捕获到信号:"<< signum << std::endl;}intmain(){// 注册SIGINT信号处理函数signal(SIGINT, sig_handler); std::cout <<"每隔1秒给自己发送SIGINT信号..."<< std::endl;while(true){sleep(1);raise(SIGINT);// 主动发送信号}return0;}

2.3.3 abort 函数:异常终止进程

函数原型:

#include<stdlib.h>voidabort(void);
  • 作用:向当前进程发送 SIGABRT(6 号信号),强制异常终止,不可被忽略或自定义捕捉;但是他能执行自定义的操作只不过最后依旧会终止
  • 无返回值(必然终止进程)。

实战案例

#include<iostream>#include<unistd.h>#include<signal.h>#include<stdlib.h>voidsigabrt_handler(int signum){ std::cout <<"捕获到信号:"<< signum <<"(但仍会终止)"<< std::endl;}intmain(){// 尝试捕捉SIGABRT(6号信号)signal(SIGABRT, sigabrt_handler); std::cout <<"3秒后调用abort()..."<< std::endl;sleep(3);abort();// 发送SIGABRT,进程终止 std::cout <<"这里不会执行"<< std::endl;return0;}

运行结果

3秒后调用abort()... 捕获到信号:6(但仍会终止) Aborted (core dumped)

2.4 硬件异常产生信号(CPU / 内存错误)

由硬件检测到的异常触发,内核将其解释为对应信号发送给进程,所有硬件异常信号基本上均触发 Core Dump(后续有补充),核心场景包括除零错误(SIGFPE)、非法内存访问(SIGSEGV),也就是8号和11号信号

在这里插入图片描述


在这里插入图片描述

2.4.1 除零错误:SIGFPE(8 号信号)

#include<iostream>#include<signal.h>#include<unistd.h>voidsigfpe_handler(int signum){ std::cout <<"捕获到SIGFPE信号(编号:"<< signum <<"),除零错误!"<< std::endl;// 注意:除零后CPU状态未清理,信号会持续触发,需退出进程exit(1);}intmain(){signal(SIGFPE, sigfpe_handler); std::cout <<"模拟除零错误..."<< std::endl;sleep(1);int a =10; a /=0;// 除零,触发SIGFPEreturn0;}
在这里插入图片描述


在这里插入图片描述

保留问题:进程怎么知道硬件出错的

2.4.2 非法内存访问:SIGSEGV(11 号信号)

#include<iostream>#include<signal.h>#include<unistd.h>voidsigsegv_handler(int signum){ std::cout <<"捕获到SIGSEGV信号(编号:"<< signum <<"),非法内存访问!"<< std::endl;exit(1);}intmain(){signal(SIGSEGV, sigsegv_handler); std::cout <<"模拟野指针访问..."<< std::endl;sleep(1);int* p =nullptr;*p =100;// 访问空指针,触发SIGSEGVreturn0;}
在这里插入图片描述

2.4.3 关键说明

硬件异常产生的信号,本质是 CPU 或硬件检测到错误后,通知内核,再由内核转化为信号发送给进程:

  • 除零错误:CPU 运算单元检测到异常,状态寄存器标记错误,内核解释为 SIGFPE;
  • 非法内存访问:MMU(内存管理单元)检测到地址无效,内核解释为 SIGSEGV;
  • 若不退出进程,异常状态会持续存在,导致信号反复触发
在这里插入图片描述

2.5 软件条件产生信号(定时 / 管道异常)

由软件内部状态触发的信号,核心场景包括定时器超时(alarm函数)、管道破裂(SIGPIPE)等。

在这里插入图片描述
  • 图中就是以管道的例子来引入软件条件的

2.5.1 管道破裂信号(SIGPIPE)

当向无读端的管道写入数据时,内核会向写进程发送 SIGPIPE(13 号信号),默认动作是终止进程。

  • 这里的案例可以参考,但是我个人觉得并不怎么好全在一个文件下,大家可以去看看我之前写的匿名管道那篇博客中的案例,更加规范一点,并且多个场景都覆盖的比较全。
#include<unistd.h>#include<signal.h>#include<iostream>voidsigpipe_handler(int signum){ std::cout <<"捕获到SIGPIPE信号(编号:"<< signum <<"),管道破裂!"<< std::endl;exit(1);}intmain(){int pipefd[2];pipe(pipefd);// 创建管道close(pipefd[0]);// 关闭读端signal(SIGPIPE, sigpipe_handler);char buf[1024]="hello";write(pipefd[1], buf,sizeof(buf));// 向无读端的管道写数据,触发SIGPIPEreturn0;}

2.5.2 alarm 函数:定时器信号(SIGALRM)

函数原型:

#include<unistd.h>unsignedintalarm(unsignedint seconds);
  • 作用:设置定时器,seconds秒后向当前进程发送 SIGALRM(14 号信号);
  • 返回值:0(无之前的定时器)或之前定时器的剩余秒数;
  • 特性:一个进程同时只能有一个活跃的alarm定时器,重复调用会覆盖之前的设置。
在这里插入图片描述

实战案例代码 – 多种场景测试和概念补充(注意看图,关于取消闹钟上图中有)

#include<iostream>#include<signal.h>#include<unistd.h>int cnt =0;voidhandler(int sig){ std::cout <<"我正在处理一个进程, pid: "<<getpid()<<" sig number: "<< sig <<" cnt: "<< cnt << std::endl;alarm(2);// exit(1); // 这里必须要退出,不然使用信号捕捉函数就是死循环}intmain(){alarm(1);// 1秒后发送SIGALRM// int n = alarm(0); // 取消闹钟,返回剩余时间signal(SIGALRM, handler);while(true){ std::cout <<"cnt: "<< cnt << std::endl; cnt++;sleep(1);}return0;}
在这里插入图片描述


在这里插入图片描述

2.5.3 关于 alarm 的应用场景和管理(先描述,再组织)

在这里插入图片描述


在这里插入图片描述

三. 总结及补充知识(重点板块)

3.1 总结:信号产生的核心逻辑

本文详细拆解了 Linux 信号的 5 种产生方式,核心要点总结:

  • 信号是异步通知机制,处理方式分为默认、自定义、忽略三类;
  • 终端按键、kill 命令、系统函数、软件条件、硬件异常是信号产生的核心场景
  • 关键函数:signal(注册处理函数)、kill(发送信号)、alarm(定时信号)、abort(异常终止),raise(给自己发送指定信号);
  • 不可捕捉 / 忽略的信号:SIGKILL(9)、SIGSTOP(19),用于强制控制进程(6号 SIGABRT 其实也算是,比较特殊可以捕捉但是最后依旧会终止进程)。

3.2 键盘怎么能向目标进程发送信号问题的解答(其中还有进程组和作业等重要概念,可以仔细看看)

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


硬件中断图的补充:

在这里插入图片描述

3.3 关于 Term VS Core 以及 core dump的补充

在这里插入图片描述


在这里插入图片描述
  • 子进程core dump的补充图
在这里插入图片描述

概念补充

  • 下列概念比如关于打开使用core文件的操作在Ubuntu24.04这种比较新的版本中是不一样的,具体可以看上面的图示
在这里插入图片描述

3.4 两个问题的补充和解释(感兴趣的可以看一下)

在这里插入图片描述

3.4.1 闹钟的实现机制

  • 问题:闹钟是依靠专门的硬件计时,还是由CPU来计时?如果由CPU计时,它既要处理进程又要计时,负担很重。
  • 解答:闹钟实际上是软件实现的。其计时依赖于操作系统对硬件中断的利用。
    • 计算机中有一个硬件单元——晶振,它会以固定的周期(例如每纳秒)触发时钟中断。
    • 操作系统在内部维护一个全局变量,记录时钟中断发生的次数。从开机开始,每发生一次中断,计数加一。通过这个次数乘以中断周期,就能计算出系统运行的时间(例如计数到100表示已过100纳秒)。
    • 因此,计时并非由CPU主动进行,而是由外部晶振的周期性振动驱动中断,操作系统通过中断次数来换算时间。CPU只在每次中断发生时响应并更新计数。
    • 操作系统的运行也不是连续的,而是“一卡一卡”的——每个时钟中断就像一次“滴答”,系统在这些离散的时刻处理计时、调度等任务。

3.4.2 除零异常与 core dump

  • 问题:除零操作导致死循环,CPU如何识别?是否需要在上下文切换前检查core dumped标志?
  • 解答
    • 除零操作会触发CPU异常,该异常同样会走中断向量表,属于异常处理的一种。CPU检测到除零错误后,会立即转入相应的异常处理程序。
    • 对于非法操作(如除零),系统通常会产生core dump(核心转储),将进程内存映像保存下来便于调试。但注意:core dump只需要发生一次即可,不需要重复检查标志。
    • 如果程序通过信号处理函数自定义捕获了该异常(例如捕捉SIGFPE),那么系统默认的core dump行为就不会发生,因为异常被用户代码接管了。

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:下一篇将继续讲解信号的 “保存” 与 “递达”,包括未决信号集、阻塞信号集、信号捕捉流程等底层逻辑。创作不易,觉得有帮助的话,欢迎点赞、收藏、关注三连~ 后续会持续更新 Linux 信号进阶内容,带你从底层吃透进程通信技术。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

C++的核心--继承

C++的核心--继承

目录 前言 一、继承的概念及定义 二、基类和派生类对象赋值转换 三、继承中的作用域 四、派生类的默认成员函数 五、继承与友元 六、继承与静态成员 七、复杂的菱形继承及菱形虚拟继承 (一)单继承与多继承 (二)菱形继承 (三)菱形虚拟继承 八、继承的总结和反思 结语 前言 在C++ 编程世界里,继承是一项极为关键的特性,它为代码的复用和层次化设计提供了强大支持。掌握继承机制,对于编写高效、可维护的C++ 代码至关重要。今天,就让我们一起深入探究C++ 中的继承。 一、继承的概念及定义 继承是面向对象程序设计实现代码复用的重要手段。它允许我们在保持原有类特性的基础上进行扩展,产生新的类,即派生类。这体现了面向对象程序设计的层次结构,从简单到复杂逐步构建。 定义格式上,以 class Student : public

By Ne0inhk
【C++笔记】STL详解:vector容器的使用

【C++笔记】STL详解:vector容器的使用

前言:         本文在介绍STL框架基础上,进一步讲解了迭代器、auto关键字和范围for循环的使用方法,接下来我们将重点探讨vector类的常用接口及其应用。          一、vector容器的简介             C++ 的 vector 是标准模板库(STL)中最核心且实用的容器之一,其与固定大小的传统数组(如 int arr[10])不同,vector 克服了数组的局限性,它不需要预先确定大小,并且可以动态调整容量。          简单理解为:vector是可变的、经过封装函数功能的数组。                  核心优势:          ①动态扩容:您不需要一开始就告诉它要存多少数据。当空间不够时,它会在底层自动帮您寻找一块更大的内存,把数据搬过去。          ②内存安全:它负责自己内存的分配和释放,大大减少了手动 new 和 delete 带来的内存泄漏风险。          ③功能丰富:它自带了大量现成的工具函数,比如:获取大小、清空数据、在尾部添加数据等。

By Ne0inhk
C++新手入门学习教程(完整版)

C++新手入门学习教程(完整版)

以下教程覆盖了 C++ 学习的各个方面,适合初学者循序渐进地学习。学习过程中,建议初学者多做练习和项目,以加深对理论知识的理解。希望这个教程能为你提供一个清晰的学习路径。 目录 第一章:C++ 简介 1.1 C++ 的历史与演变 1.2 C++ 的特点和优势 1.3 C++ 的应用领域 1.4 C++ 的未来展望 第二章:环境搭建 2.1 安装 C++ 编译器与 IDE Windows Linux Mac 2.2 配置开发环境 2.3 编译与运行示例程序 第三章:基本语法 3.1 C+

By Ne0inhk
Java Web项目怎么创建 & 没有出现web.xml的解决方法

Java Web项目怎么创建 & 没有出现web.xml的解决方法

目录 项目结构图 首先我们先来看下web项目怎么创建 第一步:新建项目 第二步:进去创建项目界面 第三步:添加src文件夹,继续点击next 第四步:勾选web.xml部署 第五步:xml文件显示 这里提供三种解决办法: 第一种:直接生成 第一步: 第二步: 第三步: 第二种:新建项目时勾选web.xml部署 第三种:创建Web.xml 第一步: 第二步: 第三步: 第四步: web.xml里面的内容 项目结构图 这里先给大家看一下正常情况和没有的情况                正常情况:                                            没有: 在上面第二个图中我们可以看到在eclipse上面创建的web项目中没有web.xml 首先我们先来看下web项目怎么创建 第一步:新建项目 点击“file→new→Dynamic Web Project”,如下图所示: 第二步:

By Ne0inhk