【Linux】进程信号(1)
1. 信号快速认识
信号是进程之间事件异步通知的一种方式,属于软中断。
1-1 生活角度的信号
通过 “等待快递” 场景,类比信号的核心逻辑:
- 能 “识别快递”:即使快递未到,也知道如何处理(对应进程识别信号是内核内置特性,信号未产生时,处理方法已准备好)。
- 收到快递通知后可延迟取件:正在打游戏时,5min 后再取(对应信号产生后不立即处理,进程优先处理高优先级任务,在合适时处理)。
- 延迟期间 “记住有快递”:收到通知到取件的时间窗内,明确有快递待处理(对应信号产生后,内核会保存信号状态)。
- 处理快递的三种方式:
- 默认动作:打开快递使用商品;
- 自定义动作:将零食快递送给女朋友;
- 忽略:快递放床头,继续打游戏(对应信号处理的三种方式:默认、忽略、自定义,自定义也称信号捕捉)。
- 快递到来异步:无法确定快递员打电话的时间(对应信号产生是异步的,进程无法预判信号到来时机)。
1-2 技术应用角度的信号
1-2-1 一个样例(默认信号处理)
// sig.cc
#include <iostream>
#include <unistd.h>
int main()
{while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
- 操作与现象:
- 编译运行:
g++ sig.cc -o sig
,./sig
,进程循环打印信息; - 按下
Ctrl+C
,进程退出。
- 编译运行:
- 背后逻辑:
- 用户启动前台进程;
Ctrl+C
产生硬件中断,被 OS 获取并解释为信号,发送给前台进程;- 前台进程收到信号,执行默认处理动作(退出)。
1-2-2 一个系统函数(signal 函数)与自定义信号处理
1. signal 函数
- 功能:ANSI C 标准的信号处理函数,用于设置特定信号的处理动作。
- 头文件:
#include <signal.h>
- 函数原型:
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
- 参数说明:
signum
:信号编号(如 2 对应 SIGINT 信号);handler
:函数指针,指定信号的处理动作,收到对应信号时回调执行该函数。
2. 自定义信号处理示例(证明 Ctrl+C 对应 SIGINT 信号)
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}
int main()
{std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT /*2*/, handler);while (true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
- 操作与现象:
- 编译运行:
g++ sig.cc -o sig
,./sig
,打印进程 ID 并循环输出信息; - 按下
Ctrl+C
,打印 “我是:进程 ID, 我获得了一个信号: 2”,进程不退出,继续循环。
- 编译运行:
3. 思考与注意
- 进程不退出的原因:通过
signal(SIGINT, handler)
将 SIGINT 信号的处理动作从默认(退出)改为自定义函数handler
,收到信号时执行自定义逻辑而非退出。 - 该例子说明:信号处理可自定义,需提前通过 signal 函数设置处理动作。
- 生活类比:进程是 “你”,操作系统是 “快递员”,信号是 “快递”,发信号类似 “快递员打电话通知”。
- 关键注意:
- signal 函数仅设置信号处理动作,不直接调用处理动作;若信号未产生,自定义函数不执行;
Ctrl+C
产生的信号仅发给前台进程;命令后加&
可将进程放后台运行,Shell 无需等待进程结束即可接收新命令;- 信号相对于进程控制流程是异步的,进程用户空间代码执行到任意位置都可能收到 SIGINT 信号;
- 前后台进程:
- 后台进程无法从标准输入中获取内容!
- 前台进程能从键盘获取标准输入,因为键盘只有一个,输入数据一定是给一个确定的进程的。
- 两者都能向标准输出上打印。
- Shell 可同时运行 1 个前台进程和多个后台进程。
- 补充一部分命令,前后台移动:
- 在要运行的命令末尾加上
&
符号即可让程序在后台运行。 - jobs查看所有的后台进程。
- fg 任务号,将特定的进程提到前台。
- ctrl+z:进程切换到后台。
- bg 任务号,让后台进程恢复运行。
- 在要运行的命令末尾加上
1-3 信号概念
1-3-1 查看信号
- 信号属性:每个信号有编号和宏定义名称,宏定义在
signal.h
中(如#define SIGINT 2
)。 - 信号分类:编号 34 以上为实时信号,本章仅讨论编号 34 以下的信号。
- 查看方式:通过
man 7 signal
查看各信号的产生条件、默认处理动作等详细说明。
1-3-2 信号处理(三种可选动作)
1. 忽略此信号
- 代码示例:
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl; } int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT /*2*/, SIG_IGN); // 设置忽略信号的宏while (true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);} }
- 操作与现象:编译运行后,按下
Ctrl+C
无反应,进程继续循环打印。
2. 执行该信号的默认处理动作
- 代码示例:
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) { std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl; } int main() { std::cout << "我是进程: " << getpid() << std::endl; signal(SIGINT/*2*/, SIG_DFL); while(true){ std::cout << "I am a process, I am waiting signal!" << std::endl; sleep(1); } }
- 操作与现象:编译运行后,按下
Ctrl+C
,进程退出(执行 SIGINT 信号的默认动作)。
3. 自定义捕捉信号
- 方式:提供信号处理函数,内核处理信号时切换到用户态执行该函数(即 1-2-2 中自定义信号处理的样例)。
- 源码细节:
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */ #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */ /* Type of a signal handler. */ typedef void (*__sighandler_t) (int);
__sighandler_t
(信号处理函数指针)类型,分别表示默认动作、忽略信号。
2. 产生信号
信号的产生并非随机,而是由特定场景触发,主要分为终端按键、系统命令、函数调用、软件条件、硬件异常五大类,以下结合具体代码与操作,梳理各类信号的产生方式、现象及核心逻辑。
2-1 通过终端按键产生信号
终端按键触发的信号是用户与前台进程交互的常用方式,核心是通过键盘输入触发硬件中断,由操作系统解释为对应信号并发送给前台进程。
2-1-1 基本操作(3 种核心按键信号)
1. Ctrl+\(SIGQUIT,3 号信号)
- 功能:发送终止信号,默认动作是 “终止进程 + 生成 core dump 文件”(用于事后调试),可通过
signal
函数自定义处理。 - 代码示例:
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl; } int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGQUIT/*3*/, handler); // 自定义SIGQUIT处理动作while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);} }
- 操作与现象:
- 编译运行:
g++ sig.cc -o sig && ./sig
,进程循环打印; - 按下
Ctrl+\
,打印 “我是:进程 ID, 我获得了一个信号: 3”,进程不退出; - 注释
signal(SIGQUIT, handler)
后重新运行,按下Ctrl+\
,进程终止并提示Quit
(执行默认动作)。
- 编译运行:
2. Ctrl+Z(SIGTSTP,20 号信号)
- 功能:发送停止信号,默认动作是 “将前台进程挂起至后台”,可通过
signal
函数自定义处理。 - 代码示例:
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl; } int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGTSTP/*20*/, handler); // 自定义SIGTSTP处理动作while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);} }
- 操作与现象:
- 编译运行后按下
Ctrl+Z
,打印 “我是:进程 ID, 我获得了一个信号: 20”,进程不退出; - 注释
signal(SIGTSTP, handler)
后重新运行,按下Ctrl+Z
,进程挂起至后台,终端提示[1]+ Stopped ./sig
; - 查看后台进程:
jobs
(显示挂起的进程);如需恢复前台运行:fg 进程ID
。
- 编译运行后按下
2-1-2 核心原理与注意
- OS 如何识别键盘输入:键盘输入触发硬件中断,中断控制器将中断信号发给 CPU,CPU 转去执行中断处理程序(OS 内核代码),内核解析按键含义并转换为对应信号,发送给前台进程。
- 信号与硬件中断的类比:信号是 “软件层面的中断”—— 硬件中断发给 CPU,信号发给进程;两者均是 “突发通知”,但作用对象与层级不同。
2-2 调用系统命令向进程发信号
通过kill
系统命令可主动向指定进程发送信号,无需依赖终端交互,核心是通过命令指定 “信号类型” 与 “目标进程 ID”。
操作示例(以 SIGSEGV 信号为例)
- 准备后台运行的死循环进程:
#include <iostream> #include <unistd.h> #include <signal.h> int main() {while(true){ sleep(1); } // 死循环,后台运行 }
- 编译与后台启动:
$ g++ sig.cc -o sig $ ./sig & # 后台运行,终端返回进程ID(如213784)
- 查看进程 ID:
$ ps ajx | head -1 && ps ajx | grep sig # 筛选sig进程信息 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 211805 213784 213784 211805 pts/0 213792 S 1002 0:00 ./sig
- 发送 SIGSEGV(11 号信号,段错误信号):
$ kill -SIGSEGV 213784 # 或 kill -11 213784(11是SIGSEGV编号) $ # 多按一次回车,终端提示 [1]+ Segmentation fault ./sig(进程因SIGSEGV终止)
关键说明
- 信号命令的多种写法:
kill -信号名称 进程ID
(如kill -SIGSEGV 213784
)或kill -信号编号 进程ID
(如kill -11 213784
),两者等价。 - 终端提示延迟原因:发送信号时,Shell 可能已回到提示符等待输入,为避免信息交错,Shell 会在用户下次输入后再显示进程终止信息(如
Segmentation fault
)。
2-3 使用函数产生信号
通过系统调用函数可在代码中主动产生信号,常用函数包括kill
(给指定进程发信号)、raise
(给自己发信号)、abort
(给自己发终止信号)。
2-3-1 kill 函数
功能:给指定 PID 的进程发送指定信号,是
kill
命令的底层实现。头文件:
#include <sys/types.h>
、#include <signal.h>
函数原型:
int kill(pid_t pid, int sig);
- 参数:
pid
(目标进程 ID,正数为指定进程,-1 为所有进程)、sig
(信号编号 / 名称); - 返回值:成功返回 0,失败返回 - 1(并设置
errno
)。
- 参数:
代码示例(实现自定义 kill 命令):
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <signal.h> // 用法:./mykill -signumber pid(如./mykill -2 213784) int main(int argc, char *argv[]) {if(argc != 3) // 检查参数数量{std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;return 1;}int signum = std::stoi(argv[1]+1); // 提取信号编号(跳过前缀“-”)pid_t pid = std::stoi(argv[2]); // 提取目标进程IDint n = kill(pid, signum); // 发送信号return 0; }
2-3-2 raise 函数
功能:给当前进程发送指定信号(“自己给自己发信号”)。
头文件:
#include <signal.h>
函数原型:
int raise(int sig);
- 参数:
sig
(信号编号 / 名称); - 返回值:成功返回 0,失败返回非 0。
- 参数:
代码示例:
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) {std::cout << "获取了一个信号: " << signumber << std::endl; } int main() {signal(2/*SIGINT*/, handler); // 自定义2号信号处理while(true){sleep(1);raise(2); // 每秒给自己发一次2号信号} }
运行效果:编译运行后,每秒打印 “获取了一个信号: 2”,进程持续运行。
2-3-3 abort 函数
功能:使当前进程异常终止,底层是给自己发送SIGABRT 信号(6 号信号)。
头文件:
#include <stdlib.h>
函数原型:
void abort(void);
- 无参数,无返回值(进程必然终止,不会返回)。
代码示例:
#include <iostream> #include <unistd.h> #include <stdlib.h> #include <signal.h> void handler(int signumber) {std::cout << "获取了一个信号: " << signumber << std::endl; } int main() {signal(SIGABRT/*6*/, handler); // 自定义6号信号处理while(true){sleep(1);abort(); // 每秒给自己发一次SIGABRT信号} }
运行效果:
- 编译运行后,打印 “获取了一个信号: 6”,随后进程终止并提示
Aborted
(即使自定义处理,abort
仍会强制终止进程); - 注释
signal(SIGABRT, handler)
后运行,直接提示Aborted
(执行默认动作)。
- 编译运行后,打印 “获取了一个信号: 6”,随后进程终止并提示
2-4 由软件条件产生信号
软件条件指由软件内部状态或操作触发信号,典型场景包括 “管道断裂(SIGPIPE)”“定时器超时(SIGALRM)”,此处重点介绍alarm
函数与 SIGALRM 信号。
2-4-1 alarm 函数基础
- 功能:设定 “闹钟”,告诉内核在
seconds
秒后给当前进程发送SIGALRM 信号(14 号信号),默认处理动作是终止进程。 - 头文件:
#include <unistd.h>
- 函数原型:
unsigned int alarm(unsigned int seconds);
- 参数:
seconds
(闹钟秒数,0 表示取消之前的闹钟); - 返回值:若之前有未到期的闹钟,返回剩余秒数;否则返回 0。
- 参数:
2-4-2 基本验证(体会 IO 效率)
通过两个代码示例对比 “IO 操作多少” 对计数结果的影响,验证 SIGALRM 的触发逻辑。
示例 1:IO 操作多(打印频繁)
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{int count = 0;alarm(1); // 1秒后发送SIGALRMwhile(true){std::cout << "count : " << count << std::endl; // 频繁IO(打印)count++;}return 0;
}
- 运行效果:1 秒后进程被 SIGALRM 终止,计数结果较小(IO 操作耗时,循环次数少)。
示例 2:IO 操作少(仅计数)
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{std::cout << "count : " << count << std::endl; // 仅1次IOexit(0);
}
int main()
{signal(SIGALRM, handler); // 自定义SIGALRM处理(避免进程直接终止)alarm(1);while (true) { count++; } // 无额外IO,仅计数return 0;
}
- 运行效果:1 秒后执行
handler
,打印计数结果(如count : 492333713
),计数远大于示例 1(IO 少,循环速度快)。
结论:
- SIGALRM 会准时触发(1 秒后必然发送);
- IO 操作会消耗时间,导致相同时间内的循环次数减少(IO 效率低)。
2-4-3 设置重复闹钟
alarm
是 “一次性闹钟”,若需重复触发,需在 SIGALRM 的处理函数中重新调用alarm
,结合pause
函数(等待信号)实现循环。
代码示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>
using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;void handler(int signo)
{for(auto &f : gfuncs) { f(); } // 执行预设函数(可扩展)std::cout << "gcount : " << gcount << std::endl;int remaining = alarm(1); // 重新设置1秒闹钟,返回上次剩余时间std::cout << "剩余时间 : " << remaining << std::endl;
}int main()
{alarm(1); // 首次设置闹钟signal(SIGALRM, handler);while (true){pause(); // 阻塞等待信号,收到信号后返回-1std::cout << "我醒来了..." << std::endl;gcount++;}
}
pause
函数补充:- 原型:
int pause(void);
,功能是阻塞进程,直到收到一个 “能终止进程或触发自定义处理函数” 的信号; - 返回值:仅当信号被捕捉且处理函数返回时,返回 - 1(并设置
errno=EINTR
)。
- 原型:
运行效果:
- 终端 1 运行
./alarm
,首次 1 秒后打印 “gcount : 0”“剩余时间 : 0”,之后每秒重复; - 终端 2 执行
kill -14 进程ID
(主动发送 SIGALRM),终端 1 会提前打印,且 “剩余时间” 显示上次闹钟的剩余秒数(如 13)。
- 终端 1 运行
结论:
- 闹钟需重复设置才会持续触发;
alarm(0)
可取消当前闹钟,返回之前的剩余时间。
2-4-4 软件条件的核心理解
软件条件是 “由软件逻辑触发的信号”,如:
alarm
的 “时间到期” 是软件定时器条件;- 向已关闭的管道写数据(触发 SIGPIPE)是软件状态条件;
- 这些条件由内核检测,满足时内核主动向进程发送对应信号。
2-5 硬件异常产生信号
硬件异常(如 CPU 运算错误、内存访问错误)由硬件检测后通知内核,内核将异常解释为对应信号并发送给当前进程,典型场景包括 “除零(SIGFPE)”“非法内存访问(SIGSEGV)”。
2-5-1 模拟除零(SIGFPE,8 号信号)
- 代码示例:
#include <stdio.h> #include <signal.h> void handler(int sig) {printf("catch a sig : %d\n", sig); // 自定义8号信号处理 } int main() {// signal(SIGFPE, handler); // 注释则执行默认动作(终止进程)sleep(1);int a = 10;a /= 0; // 除零操作,触发硬件异常while(1);return 0; }
- 运行效果:
- 注释
signal(SIGFPE, handler)
:进程终止,提示Floating point exception (core dumped)
; - 启用
signal(SIGFPE, handler)
:持续打印 “catch a sig : 8”(除零异常未清理,CPU 状态寄存器保持异常标记,内核反复发送 SIGFPE)。
- 注释
2-5-2 模拟野指针(SIGSEGV,11 号信号)
- 代码示例(默认动作):
#include <stdio.h> #include <signal.h> void handler(int sig) {printf("catch a sig : %d\n", sig); } int main() {// signal(SIGSEGV, handler); // 注释则执行默认动作sleep(1);int *p = NULL; // 野指针(空指针)*p = 100; // 非法内存写入,触发MMU硬件异常while(1);return 0; }
- 运行效果:
- 注释
signal(SIGSEGV, handler)
:进程终止,提示Segmentation fault (core dumped)
; - 启用
signal(SIGSEGV, handler)
:持续打印 “catch a sig : 11”(非法内存访问状态未清理,内核反复发送 SIGSEGV)。
- 注释
2-5-3 Core Dump(核心转储)
1. 什么是 Core Dump?
进程异常终止时,将用户空间内存数据保存到磁盘文件(默认名core
),用于事后调试(如通过gdb ./sig core
查看崩溃时的内存状态)。
2. 启用 Core Dump
默认情况下,系统禁止产生 Core Dump(避免敏感信息泄露),需通过ulimit
命令修改限制:
$ ulimit -c 1024 # 允许Core Dump文件最大为1024块(约512KB)
$ ulimit -a # 查看当前限制,确认“core file size”为1024
3. 验证 Core Dump(以 SIGQUIT 为例)
- 前台运行死循环进程,按下
Ctrl+\
(SIGQUIT 默认动作:终止 + Core Dump); - 终端生成
core
文件,可通过gdb ./sig core
调试:bash
$ gdb ./sig core (gdb) bt # 查看崩溃时的函数调用栈
4. 子进程 Core Dump 检测
- 代码示例:
#include <iostream> #include <unistd.h> #include <stdlib.h> #include <signal.h> #include <sys/wait.h> int main() {if (fork() == 0) // 子进程{sleep(1);int a = 10;a /= 0; // 除零,触发SIGFPE(默认Core Dump)exit(0);}int status = 0;waitpid(-1, &status, 0); // 等待子进程退出,获取状态// 解析状态:退出信号(低7位)、是否Core Dump(第8位)printf("exit signal: %d, core dump: %d\n", status&0x7F, (status>>7)&1);return 0; }
- 运行效果:打印 “exit signal: 8, core dump: 1”(8 是 SIGFPE 编号,1 表示产生 Core Dump)。
2-6 总结思考
- 所有信号产生最终由 OS 执行的原因:OS 是进程的管理者,负责进程的创建、调度、资源分配,只有 OS 能访问进程的 PCB(保存信号相关状态),因此信号的 “产生、发送、状态记录” 必须由 OS 完成。
- 信号是否立即处理:否。进程会优先执行当前高优先级任务(如用户代码),仅在 “安全点”(如系统调用返回、中断处理结束)检查是否有未处理信号,再进行处理。
- 信号的临时记录位置:记录在进程的 PCB 中。PCB 中有 “信号位图”(每个位对应一个信号,1 表示信号已产生未处理)和 “信号处理动作表”(记录每个信号的处理方式)。
- 进程未收信号时是否知道处理方式:是。进程创建时,PCB 中的 “信号处理动作表” 会初始化(默认动作由内核预设,如 SIGINT 默认终止),即使信号未产生,进程已明确每个合法信号的处理方式。
- OS 向进程发送信号的完整过程:
- 触发信号产生条件(如终端按键、函数调用、硬件异常);
- OS 内核检测到条件,确定目标进程与对应信号;
- 内核修改目标进程 PCB 中的 “信号位图”(将对应位置 1);
- 进程执行到 “安全点” 时,检查 PCB 中的信号位图;
- 若有未处理信号,内核根据 PCB 中的 “信号处理动作表” 执行对应动作(默认 / 忽略 / 自定义);
- 处理完成后,内核将信号位图中对应位清 0。