引言
在Linux系统中,信号机制是实现进程间异步通信的重要方式,其生命周期可分为三个关键阶段:信号产生→信号保存→信号处理。这种机制如同我们生活中的红绿灯系统:信号随时可能异步产生(红灯亮起),操作系统需要暂存信号(等待行人到达路口),最终在合适时机执行处理(做出停止或通行决策)。
信号基础
- 信号分类
- 普通信号(1-31):经典UNIX信号,如SIGINT(2)/SIGKILL(9)
- 实时信号(34-64):扩展信号,支持队列化处理
- 查看命令:kill -l显示62个有效信号(缺失0/32/33号)
- 生活化类比
- 红绿灯规则:认识信号(颜色识别)→定义行为(停/行)
- 外卖通知:异步响应(游戏挂机时接电话)→延迟处理(推完高地再取餐)
- 闹钟提醒:默认动作(起床)→忽略处理(蒙头继续睡)→自定义响应(关闭闹钟后晨练)
信号特性
- 异步性
信号产生与处理存在时间窗口,如同快递员送货与收件人活动的独立时序。这种异步特性要求系统必须通过pending位图等机制暂存未决信号。 - 内核级流转
当信号触发时,CPU从用户态陷入内核态完成信号检测,再返回用户态执行处理程序。这种状态切换如同交警临时接管交通指挥权。 - 处理三态
处理方式 | 系统行为 | 生活案例 |
默认动作 | SIG_DFL(终止/忽略等) | 绿灯通行 |
忽略信号 | SIG_IGN | 屏蔽骚扰电话 |
自定义处理 | 注册handler函数 | 特殊门铃响应机制 |
理解信号的预备工作
信号的本质认知
- 信号接收主体
- 信号的本质是操作系统向进程发送的异步通知,所有信号操作均以进程为基本单位
- 验证方式:kill -9 PID命令通过进程ID定向发送信号
- 进程识别机制
- 识别前提:
- 认知能力:通过代码预定义信号处理逻辑(程序员编写)
- 响应能力:操作系统维护的进程控制块(PCB)提供支持
- 类比理解:如同人类通过教育习得红绿灯规则(认知)并形成条件反射(响应)
- 识别前提:
信号存储原理
存储维度 | 技术实现 | 核心特性 |
数据结构 | PCB中的pending位图(32位无符号整型) | 比特位映射信号编号(1-31) |
编码规则 | 比特位索引=信号编号-1 | 0表示未接收,1表示已接收 |
修改权限 | 仅操作系统可操作 | 用户空间无法直接访问内核数据 |
信号发送本质
- 操作实质
- 所有信号发送操作最终表现为修改目标进程PCB中的信号位图
- 示例:发送9号信号 = 将pending位图的第9位设为1
- 权限体系
- 内核独占:只有操作系统有权修改PCB结构
- 用户接口:通过系统调用(如kill())触发内核级操作
- 命令本质:kill命令是封装系统调用的用户态工具
关键设计思想
1.异步处理机制
- 信号产生与处理的时间窗口分离设计
- 当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理进程本身必须对信号有保存能力。
- 类比场景:外卖通知到达时用户可能正在执行高优先级任务
2.内核态管控
-
PCB是内核维护的数据结构对象,管理者是操作系统,所以我们未来发送信号的方式,本质上都是通过操作系统向目标进程发送信号的
- 系统调用作为安全边界保障操作合法性
- 用户程序通过注册handler实现有限自定义
- 信号生命周期全程由操作系统调度
[用户空间] [内核空间]│ │├─ kill命令 ││ │ ││ └─ 触发系统调用 │▼ ▼
进程A发送信号请求 → 系统调用接口 → 内核修改进程B的PCB位图│▼信号暂存于pending位图│▼等待进程B进入可中断状态
信号的产生
一、通过终端按键产生信号
- Ctrl+C的本质
- 硬件中断触发:键盘控制器检测到Ctrl+C组合键时产生中断
- 内核转换机制:操作系统将中断转换为SIGINT(2)信号
- 默认处理动作:立即终止前台进程(对应SIG_DFL标志)
-
仅影响前台进程(带+号的进程)
二、调用系统函数向进程发信号
-
kill 给任意进程发送任意信号
#include <signal.h>
int kill(pid_t pid, int sig);
函数 | 作用域 | 等效操作 | 典型信号 |
raise() | 当前进程 | kill(getpid(), sig) | SIGTERM |
abort() | 强制终止 | kill(getpid(),6) | SIGABRT |
alarm() | 定时触发 | 内核定时器 | SIGALRM |
三、由软件条件产生信号
alarm系统调用
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
软件条件 --"闹钟"其实就是用软件实现的。任意一个进程,都可以通过alarm系统调用在内核中设置闹钟,OS内可能会存在着很多的闹钟,那么操作系统要不要管理这些闹钟呢?要。
四、硬件异常产生信号
信号产生不一定由用户显示发送,也可以由操作系统自动发送。
异常类型 | 触发信号 | 典型场景 |
除零错误 | SIGFPE(8) | int a = 5/0; |
非法内存访问 | SIGSEGV(11) | 野指针解引用 |
非法指令 | SIGILL(4) | 损坏的可执行文件 |
状态寄存器:每个CPU运算结果都会更新状态寄存器标志位
一旦除0,在硬件上CPU先出异常,然后被操作系统识别,操作系统将溢出标志位的异常转化为对应的信号,发送给目标进程 。操作系统如何知道我除0了呢?语言上写的C/C++代码最底层都属于硬件异常,然后操作系统将硬件问题转化为软件问题,向你的目标进程发信号,终止这个进程,这就是我们除0进程会崩溃的原因。收到信号,不一定会引起进程退出,如果没有退出,这个信号的行为还会被调度到。我们没有能力去修正CPU所维护的状态寄存器,所以当进程被切换的时候,状态信息被保存,进程恢复的时候,操作系统会再次识别到CPU里的状态寄存器,从而一直发送信号。
MMU检测:内存管理单元校验虚拟地址合法性
graph LR
A[虚拟地址] --> B(页表查询)
B --> C{物理页存在?}
C -->|是| D[正常访问]
C -->|否| E[触发SIGSEGV]
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>using namespace std;static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " pid signo\n"<< std::endl;
}// int cnt = 0;
void catchSig(int signo)
{std::cout << "获取到一个信号,信号编号是: " << std::endl;// exit(1);// alarm(1);
}
// ./myprocess pid signo
int main(int argc, char *argv[])
{// for(int signo = 1; signo <= 31; signo++)// {// signal(signo, catchSig);// }// while(true) // {// cout << "我在运行: " << getpid() <<endl;// sleep(1);// }// 核心转储// while (true)// {// int a[10];// // a[10000] = 106;// }// 4. 软件条件 -- "闹钟"其实就是用软件实现的// IO其实很慢// 统计1S左右,我们的计算机能够将数据累计多少次!// signal(SIGALRM, catchSig);// alarm(1);// while(true)// {// cnt++;// }// 3. 产生信号的方式:硬件异常产生信号// 信号产生,不一定非得用户显示的发送!// signal(SIGFPE, catchSig);// int a = 10;// // 如何证明?// // 受到信号,不一定会引起进程退出 -- 没有退出,有可能还会被调到// // -- CPU内部的寄存器只有一份,但是寄存器中的内容,属于当前进程的上下文!// // 你有没有能力或者动作修正这个问题呢?没有// // 当进程被切换的时候,就有无数次状态寄存器被保存和回复的过程// // 所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1// // OS如何得知应该给当前进程发送8号信号的-- OS怎么知道我除0了呢??,CPU会异常// a /= 0; // 为什么除0 会终止进程?当前进程会受到来自OS系统的信号(告知),SIGFPE// signal(11, catchSig);// while (true)// {// std::cout << "我在运行中...." << std::endl;// sleep(1);// int *p = nullptr;// // p = nullptr; // 1// // OS怎么知道呢??我野指针了呢?// *p = 100; //为什么 野指针 就会崩溃呢?因为OS会给当前进程发送指定的11号信号// }// 2. 系统调用向目标进程发送信号// kill()可以想任意进程发送任意信号// raise() 给自己 发送 任意信号kill(getpid(), 任意信号)// abort() 给自己 发送 指定的信号SIGABRT, kill(getpid(), SIGABRT)// 关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程// 信号的意义:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样!// int cnt = 0;// while(cnt <= 10)// {// printf("cnt: %d, pid: %d\n", cnt++, getpid());// sleep(1);// // if(cnt >= 5) abort(); // kill(getpid(), signo)// // if(cnt >= 5) raise(9); // kill(getpid(), signo)// }// if(argc != 3)// {// Usage(argv[0]);// exit(1);// }// pid_t pid = atoi(argv[1]);// int signo = atoi(argv[2]);// int n = kill(pid, signo);// if(n != 0)// {// perror("kill");// }// 1. 通过键盘发送信号// while(true)// {// std::cout << "hello world" << std::endl;// sleep(1);// }
}
进程退出时的核心转储问题
核心转储的概念
当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据核心转储到磁盘中。
Term与Core终止的本质区别
在Linux信号机制中,进程的异常退出分为两种类型:
- Term(SIGTERM)
- 正常终止:操作系统直接结束进程,不保留额外信息
- 典型场景:kill -15 PID 或 Ctrl+C 触发的默认终止
- 特点:无调试信息留存,适用于预期内的进程终止
- Core(SIGSEGV/SIGFPE等)
- 异常终止:操作系统在终止进程前生成核心转储文件
- 典型场景:段错误(SIGSEGV)、除零错误(SIGFPE)
- 特点:保存进程崩溃时的完整内存镜像,支持事后调试
结语
本文主要讲解了信号的预备知识、信号的产生以及核心转储等内容,下一篇我们将详细介绍信号的保存和递达处理。