文章目录
-
- Linux进程信号(一):信号的快速认识与五种产生方式
- 一、信号快速认识
-
- [1.1 生活角度的信号](#1.1 生活角度的信号)
- [1.2 技术应用角度的信号](#1.2 技术应用角度的信号)
-
- [1.2.1 一个样例](#1.2.1 一个样例)
- [1.2.2 signal系统函数](#1.2.2 signal系统函数)
- [1.3 信号概念](#1.3 信号概念)
-
- [1.3.1 查看信号](#1.3.1 查看信号)
- [1.3.2 信号处理](#1.3.2 信号处理)
- 二、产生信号
-
- [2.1 通过终端按键产生信号](#2.1 通过终端按键产生信号)
-
- [2.1.1 基本操作](#2.1.1 基本操作)
- [2.1.2 理解OS如何得知键盘有数据](#2.1.2 理解OS如何得知键盘有数据)
- [2.1.3 初步理解信号起源](#2.1.3 初步理解信号起源)
- [2.2 调用系统命令向进程发信号](#2.2 调用系统命令向进程发信号)
- [2.3 使用函数产生信号](#2.3 使用函数产生信号)
-
- [2.3.1 kill函数](#2.3.1 kill函数)
- [2.3.2 raise函数](#2.3.2 raise函数)
- [2.3.3 abort函数](#2.3.3 abort函数)
- [2.4 由软件条件产生信号](#2.4 由软件条件产生信号)
-
- [2.4.1 基本alarm验证 - 体会IO效率问题](#2.4.1 基本alarm验证 - 体会IO效率问题)
- [2.4.2 设置重复闹钟](#2.4.2 设置重复闹钟)
- [2.4.3 如何理解软件条件](#2.4.3 如何理解软件条件)
- [2.4.4 如何简单快速理解系统闹钟](#2.4.4 如何简单快速理解系统闹钟)
- [2.5 硬件异常产生信号](#2.5 硬件异常产生信号)
-
- [2.5.1 模拟除0](#2.5.1 模拟除0)
- [2.5.2 模拟野指针](#2.5.2 模拟野指针)
- [2.5.3 子进程退出core dump](#2.5.3 子进程退出core dump)
- [2.5.4 Core Dump](#2.5.4 Core Dump)
- [2.6 总结思考](#2.6 总结思考)
- 三、本篇总结
- 附录:常用信号速查表
Linux进程信号(一):信号的快速认识与五种产生方式
💬 欢迎讨论 :前面我们学习了进程间通信的管道、共享内存等机制,它们主要用于进程间的数据传输。但有没有一种更轻量级的通信方式,可以让一个进程"通知"另一个进程发生了某个事件呢?答案是信号!信号是Linux中最古老、最经典的进程间通信方式,也是理解操作系统运行机制的重要窗口。本篇将带你从生活例子出发,深入理解信号的本质和五种产生方式。
👍 点赞、收藏与分享:这篇文章包含大量代码实验、图示说明,以及对信号机制的深入剖析,内容循序渐进且实用,如果对你有帮助,请点赞、收藏并分享!
🚀 循序渐进:建议按顺序阅读,从生活例子理解概念,再通过代码实验验证原理。
一、信号快速认识
1.1 生活角度的信号
在学习技术概念之前,我们先从生活中的例子理解"信号"的本质。
场景:网购快递
假设你在网上买了很多件商品,正在等待不同商品的快递到来:
bash
1. 识别快递
即便快递还没到,你也知道快递来了该怎么处理
→ 你能"识别"不同的快递
2. 记住快递
快递员到了楼下,你正在打游戏,5分钟后才能去取
→ 在这5分钟内,你知道有快递要取,但没有立即执行
→ 你"记住了"有一个快递要去取
3. 处理快递
时间合适后,你下楼拿到快递,开始处理:
① 默认动作:开心地打开,使用商品
② 自定义动作:是零食,要送给女朋友
③ 忽略:扔在床头,继续开一把游戏
4. 异步性
快递到来的整个过程,对你来说是异步的
你不能准确断定快递员什么时候给你打电话
映射到信号机制:
| 生活场景 | 信号机制 | 说明 |
|---|---|---|
| 你 | 进程 | 接收和处理信号的主体 |
| 快递员 | 操作系统 | 发送信号的实体 |
| 快递 | 信号 | 通知进程发生了某个事件 |
| 打电话通知 | 信号产生 | OS向进程发送信号 |
| 记住要取快递 | 信号保存 | 进程记录收到的信号 |
| 下楼取快递 | 信号递达 | 进程处理信号 |
| 处理方式 | 信号捕捉 | 默认/忽略/自定义 |
📌 基本结论:
bash
1. 怎么识别信号?
→ 识别信号是内置的,由内核程序员实现
2. 信号的处理方法什么时候准备好?
→ 在信号产生之前,处理方法就已经准备好了
3. 信号产生后立即处理吗?
→ 不一定,可能正在做优先级更高的事情
→ 会在"合适的时候"处理
4. 信号处理的三个阶段:
信号到来 → 信号保存 → 信号处理
5. 信号处理的三种方式:
a. 默认动作
b. 忽略信号
c. 自定义捕捉
1.2 技术应用角度的信号
1.2.1 一个样例
最常见的信号:Ctrl+C
cpp
// 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);
}
return 0;
}
bash
$ g++ sig.cc -o sig
$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C ← 按下Ctrl+C,进程退出
发生了什么?
bash
时间线:
t1: 用户输入命令,Shell启动前台进程
t2: 进程运行中,用户按下Ctrl+C
t3: 键盘硬件产生硬件中断
t4: OS捕获中断,解释为SIGINT信号(2号)
t5: OS向前台进程发送SIGINT信号
t6: 进程收到信号,执行默认处理动作:退出
完整流程图:
bash
用户 键盘 OS 进程
│ │ │ │
├─按Ctrl+C──→ │ │ │
│ ├─硬件中断──→ │ │
│ │ ├─解释为SIGINT │
│ │ ├─发送信号───→ │
│ │ │ ├─收到2号信号
│ │ │ ├─执行默认动作
│ │ │ └─退出
1.2.2 signal系统函数
函数原型:
c
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 参数说明:
// signum:信号编号(后面解释)
// handler:函数指针,表示信号的处理动作
// 当收到对应的信号,就回调执行handler方法
//
// 返回值:
// 成功:返回之前的信号处理函数
// 失败:返回SIG_ERR
开始测试:
cpp
#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;
// 修改2号信号的处理动作为自定义函数handler
signal(SIGINT/*2*/, handler);
while(true) {
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
bash
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2 ← 捕捉到信号,但没有退出
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!
📌 思考:
-
这里进程为什么不退出?
- 因为我们修改了2号信号的处理动作
- 原来的默认动作是"终止进程"
- 现在的动作是"调用handler函数"
- handler函数只是打印信息,不退出
-
这个例子能说明哪些问题?
- 信号处理是进程自己处理的
- 信号的处理方式可以被修改
- signal函数设置的是"处理方法",而不是立即调用
-
请结合生活例子解释信号处理过程?
- 进程就是你
- 操作系统就是快递员
- 信号就是快递
- 发信号的过程就是给你打电话
- handler函数就是你的处理方式
📌 注意:
bash
重要提示:
1. signal函数仅仅是设置信号的捕捉行为,
并不是直接调用处理动作
2. 如果信号没有产生,设置的捕捉函数永远也不会被调用!
3. Ctrl-C产生的信号只能发给前台进程
- 一个命令后面加&可以放到后台运行
- Shell可以同时运行一个前台进程和任意多个后台进程
- 只有前台进程才能接到Ctrl-C这种控制键产生的信号
4. 信号相对于进程的控制流程来说是异步的
- 用户随时可能按下Ctrl-C
- 进程的用户空间代码执行到任何地方都有可能收到信号
渗透:& 和 nohup
bash
# 后台运行进程
$ ./sig &
[1] 12345
我是进程: 12345
I am a process, I am waiting signal!
...
$ # Shell立即返回,可以输入新命令
$ # 按Ctrl+C不会杀死后台进程
# nohup:忽略挂断信号
$ nohup ./sig &
[1] 12346
nohup: ignoring input and appending output to 'nohup.out'
$ # 即使关闭终端,进程也会继续运行
1.3 信号概念
定义:
信号是进程之间事件异步通知的一种方式,属于软中断。
1.3.1 查看信号
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到:
c
#define SIGINT 2 // 中断信号(Ctrl+C)
查看所有信号:
bash
$ man 7 signal
Signal Value Action Comment
────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating-point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
...
字段说明:
| 字段 | 含义 |
|---|---|
| Signal | 信号名称 |
| Value | 信号编号 |
| Action | 默认处理动作 |
| Comment | 信号说明 |
默认动作:
| 动作 | 含义 |
|---|---|
| Term | 终止进程 |
| Core | 终止进程并产生core dump |
| Ign | 忽略信号 |
| Stop | 暂停进程 |
| Cont | 继续执行被暂停的进程 |
📌 注意:
bash
编号34以上的是实时信号,本章只讨论编号34以下的信号。
1.3.2 信号处理
可选的处理动作有以下三种:
1. 忽略此信号(SIG_IGN)
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
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);
}
return 0;
}
bash
$ ./sig
我是进程: 212681
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C^C^C^C^C ← 输入Ctrl+C毫无反应
I am a process, I am waiting signal!
2. 执行该信号的默认处理动作(SIG_DFL)
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
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);
}
return 0;
}
bash
$ ./sig
我是进程: 212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C ← 输入Ctrl+C,进程退出(默认动作)
3. 提供一个信号处理函数(自定义捕捉)
cpp
// 就是1.2.2节的样例
void handler(int signumber)
{
std::cout << "我是: " << getpid()
<< ", 我获得了一个信号: " << signumber << std::endl;
}
int main()
{
signal(SIGINT, handler); // 自定义捕捉
// ...
}
注意看源码:
c
#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);
// SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型
二、产生信号
上面的内容让我们快速认识了信号,下面我们从理论和实操两个层面,详细学习信号的产生方式。
2.1 通过终端按键产生信号
2.1.1 基本操作
1. Ctrl+C(SIGINT,2号信号)
前面已经验证过,这里不再重复。
2. Ctrl+\(SIGQUIT,3号信号)
SIGQUIT可以发送终止信号并生成core dump文件(后面详谈)。
cpp
#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); // 捕捉3号信号
while(true) {
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
bash
$ ./sig
我是进程: 213056
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\我是: 213056, 我获得了一个信号: 3 ← 按Ctrl+\
# 如果注释掉signal(SIGQUIT, handler);
$ ./sig
我是进程: 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\Quit ← 按Ctrl+\,进程退出并产生core dump
3. Ctrl+Z(SIGTSTP,20号信号)
SIGTSTP可以发送停止信号,将当前前台进程挂起到后台。
cpp
#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); // 捕捉20号信号
while(true) {
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
bash
$ ./sig
我是进程: 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z我是: 213552, 我获得了一个信号: 20 ← 按Ctrl+Z
# 如果注释掉signal(SIGTSTP, handler);
$ ./sig
我是进程: 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+ Stopped ./sig ← 进程被挂起
$ jobs
[1]+ Stopped ./sig
$ fg ← 将后台进程调到前台继续执行
I am a process, I am waiting signal!
2.1.2 理解OS如何得知键盘有数据
键盘输入的完整流程:
bash
用户按键
│
├─ 键盘硬件检测到按键
├─ 键盘控制器生成扫描码
├─ 通过中断控制器(8259A)向CPU发送中断请求
├─ CPU暂停当前指令,查询中断向量表
├─ 跳转到键盘中断处理程序
│
└─ OS中断处理程序:
├─ 读取键盘扫描码
├─ 解释为ASCII字符
├─ 检查是否为特殊按键(Ctrl+C等)
├─ 如果是特殊按键,生成对应信号
└─ 发送信号给前台进程
硬件中断机制:
bash
外部设备(键盘)
│
├─硬件中断──→ 中断控制器(8259A)
│
├─发送中断请求──→ CPU
│
├─查询中断向量表
├─执行中断处理程序
└─返回用户程序
2.1.3 初步理解信号起源
📌 重要理解:
bash
信号的本质:
- 信号是从纯软件角度,模拟硬件中断的行为
- 硬件中断是发给CPU
- 信号是发给进程
相似性:
- 都是异步通知机制
- 都有处理程序
- 都可以被屏蔽/阻塞
不同点:
- 硬件中断:硬件层面,CPU响应
- 信号:软件层面,进程响应
对比图:
bash
硬件中断 信号
──────── ────
外部设备产生 OS/进程/键盘产生
发给CPU 发给进程
中断向量表 信号处理表
中断处理程序 信号处理函数
可被屏蔽(IF标志) 可被阻塞(block表)
2.2 调用系统命令向进程发信号
示例代码:
cpp
#include <iostream>
#include <unistd.h>
int main()
{
while(true) {
sleep(1);
}
return 0;
}
bash
# step 1: 编译
$ g++ sig.cc -o sig
# step 2: 后台运行
$ ./sig &
[1] 213784
# step 3: 查看进程
$ ps ajx | head -1 && ps ajx | grep sig
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
211805 213784 213784 211805 pts/0 213792 S 1002 0:00 ./sig
使用kill命令发送信号:
首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号:
bash
$ kill -SIGSEGV 213784
$ # 多按一次回车
[1]+ Segmentation fault ./sig
📌 注意:
bash
1. 213784是sig进程的pid
2. 之所以要再次回车才显示"Segmentation fault",
是因为在进程终止前已经回到Shell提示符等待输入下一条命令,
Shell不希望错误信息和用户的输入交错在一起,
所以等用户输入命令之后才显示
3. 指定发送某种信号的kill命令可以有多种写法:
kill -SIGSEGV 213784
kill -11 213784 ← 11是信号SIGSEGV的编号
kill -SEGV 213784
4. 以往遇到的段错误都是由非法内存访问产生的,
而这个程序本身没错,给它发SIGSEGV也能产生段错误
2.3 使用函数产生信号
2.3.1 kill函数
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
函数原型:
c
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
RETURN VALUE
On success (at least one signal was sent), zero is returned.
On error, -1 is returned, and errno is set appropriately.
样例:实现自己的kill命令
cpp
// mykill.cc
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <cstdlib>
// 用法: mykill -signumber pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;
return 1;
}
int number = std::stoi(argv[1] + 1); // 去掉'-'
pid_t pid = std::stoi(argv[2]);
int n = kill(pid, number);
if(n < 0)
{
perror("kill");
return 2;
}
std::cout << "发送信号 " << number << " 给进程 " << pid << " 成功" << std::endl;
return 0;
}
bash
$ g++ mykill.cc -o mykill
# 启动一个测试进程
$ ./sig &
[1] 214123
# 使用我们自己的mykill发送信号
$ ./mykill -2 214123
发送信号 2 给进程 214123 成功
[1]+ Terminated ./sig
$ ./mykill -9 214123
发送信号 9 给进程 214123 成功
2.3.2 raise函数
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
函数原型:
c
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
样例:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这一处打印
std::cout << "获取了一个信号: " << signumber << std::endl;
}
int main()
{
signal(2, handler); // 先对2号信号进行捕捉
// 每隔1秒,自己给自己发送2号信号
while(true)
{
sleep(1);
raise(2);
}
return 0;
}
bash
$ g++ raise.cc -o raise
$ ./raise
获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
^C
📌 理解:
bash
raise(sig) 等价于 kill(getpid(), sig)
raise函数的优点:
- 代码更简洁
- 不需要获取进程ID
- 语义更清晰(给自己发信号)
2.3.3 abort函数
abort函数使当前进程接收到SIGABRT信号而异常终止。
函数原型:
c
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
// 就像exit函数一样,abort函数总是会成功的,所以没有返回值
样例:
cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这一处打印
std::cout << "获取了一个信号: " << signumber << std::endl;
}
int main()
{
signal(SIGABRT, handler);
while(true)
{
sleep(1);
abort();
}
return 0;
}
bash
$ g++ Abort.cc -o Abort
$ ./Abort
获取了一个信号: 6 ← 实验可以得知,abort给自己发送的是固定6号信号
Aborted ← 虽然捕捉了,但是还是要退出
# 注释掉signal(SIGABRT, handler);
$ ./Abort
Aborted
📌 注意:
bash
abort()的特殊性:
1. 固定发送SIGABRT(6号)信号
2. 即使捕捉了信号,进程最终还是会退出
3. 用于程序异常终止,产生core dump
4. 常用于断言(assert)失败时调用
2.4 由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在"管道"中已经介绍过了。本节主要介绍alarm函数和SIGALRM信号。
alarm函数原型:
c
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
RETURN VALUE
alarm() returns the number of seconds remaining until any previously
scheduled alarm was due to be delivered, or zero if there was no
previously scheduled alarm.
📌 说明:
bash
1. 调用alarm函数可以设定一个闹钟,
也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号
2. SIGALRM信号的默认处理动作是终止当前进程
3. 返回值是0或者是以前设定的闹钟时间还余下的秒数
- 打个比方:某人要小睡一觉,设定闹钟为30分钟后响,
20分钟后被人吵醒了,还想多睡一会儿,
于是重新设定闹钟为15分钟后响,
"以前设定的闹钟时间还余下的时间"就是10分钟
4. 如果seconds值为0,表示取消以前设定的闹钟,
函数的返回值仍然是以前设定的闹钟时间还余下的秒数
2.4.1 基本alarm验证 - 体会IO效率问题
程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。
实验1:IO多
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
alarm(1); // 1秒后发送SIGALRM信号,默认终止进程
while(true)
{
std::cout << "count : " << count << std::endl;
count++;
}
return 0;
}
bash
$ g++ alarm.cc -o alarm
$ ./alarm
count : 0
count : 1
count : 2
...
count : 107148
count ← 1秒后被信号终止
实验2:IO少
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "count : " << count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while(true)
{
count++; // 只计数,不打印
}
return 0;
}
bash
$ g++ alarm.cc -o alarm
$ ./alarm
count : 492333713 ← 1秒后打印计数,数值巨大!
📌 结论:
bash
1. 闹钟会响一次,默认终止进程
2. 有IO效率低
- 实验1:1秒内只数了10万次
- 实验2:1秒内数了4亿多次
- 差距:约4000倍!
3. IO操作(如std::cout)非常耗时
- 涉及系统调用
- 涉及设备驱动
- 涉及硬件操作
4. 这个实验说明了为什么:
"程序大部分时间都在等待IO"
2.4.2 设置重复闹钟
代码样例:
cpp
#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 n = alarm(1); // 重设闹钟,会返回上一次闹钟的剩余时间
std::cout << "剩余时间 : " << n << std::endl;
}
int main()
{
// 可以注册多个任务函数
// gfuncs.push_back([](){ std::cout << "我是一个内核刷新操作" << std::endl; });
// gfuncs.push_back([](){ std::cout << "我是一个检测进程时间片的操作" << std::endl; });
// gfuncs.push_back([](){ std::cout << "我是一个内存管理操作" << std::endl; });
std::cout << "我的进程pid是: " << getpid() << std::endl;
alarm(1); // 一次性的闹钟,超时alarm会自动被取消
signal(SIGALRM, handler);
while(true)
{
pause(); // 阻塞等待信号
std::cout << "我醒来了..." << std::endl;
gcount++;
}
return 0;
}
pause函数:
c
NAME
pause - wait for signal
SYNOPSIS
#include <unistd.h>
int pause(void);
DESCRIPTION
pause() causes the calling process (or thread) to sleep until a
signal is delivered that either terminates the process or causes
the invocation of a signal-catching function.
RETURN VALUE
pause() returns only when a signal was caught and the signal-
catching function returned. In this case, pause() returns -1,
and errno is set to EINTR.
运行测试:
bash
# 窗口1
$ ./alarm
我的进程pid是: 216982
我醒来了...
gcount : 0
剩余时间 : 0
我醒来了...
gcount : 1
剩余时间 : 0
# 窗口2: 提前唤醒它
$ kill -14 216982
# 窗口1继续显示:
我醒来了...
gcount : 2
剩余时间 : 13 ← 提前唤醒,剩余时间
我醒来了...
gcount : 3
剩余时间 : 0
📌 结论:
bash
1. 闹钟设置一次,起效一次
2. 重复设置的方法:
在handler中再次调用alarm(1)
3. 如果提前唤醒(比如发送其他信号),
返回值是剩余的秒数
4. alarm(0)可以取消闹钟
2.4.3 如何理解软件条件
定义:
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。
包括但不限于:
bash
1. 定时器超时
- alarm函数设定的时间到达
- 产生SIGALRM信号
2. 软件异常
- 向已关闭的管道写数据
- 产生SIGPIPE信号
3. 子进程状态变化
- 子进程退出
- 产生SIGCHLD信号
4. 其他软件事件
- 用户自定义条件
- 产生SIGUSR1/SIGUSR2信号
与硬件异常的区别:
| 类型 | 触发方式 | 例子 | 信号 |
|---|---|---|---|
| 硬件异常 | CPU检测到 | 除0、野指针 | SIGFPE、SIGSEGV |
| 软件条件 | OS判断 | 定时器、管道 | SIGALRM、SIGPIPE |
2.4.4 如何简单快速理解系统闹钟
系统闹钟的本质是OS必须自身具有定时功能,并能让用户设置这种定时功能。
内核定时器数据结构:
c
struct timer_list {
struct list_head entry;
unsigned long expires; // 超时时间
void (*function)(unsigned long); // 处理方法
unsigned long data;
struct tvec_t_base_s *base;
};
简化理解(堆结构):
bash
[最小堆]
│
┌─────────┼─────────┐
│ │ │
timer1 timer2 timer3
expires=10 expires=15 expires=20
handler1 handler2 handler3
时间推进:
t=0: 启动定时器
t=10: timer1超时,调用handler1
t=15: timer2超时,调用handler2
t=20: timer3超时,调用handler3
📌 注意:
bash
实际内核使用的是"时间轮"(timing wheel)的数据结构,
比堆更高效,但原理类似。
我们这里为了简单理解,可以把它想象成堆结构。
2.5 硬件异常产生信号
硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。
例子:
bash
1. 除以0:
CPU的运算单元产生异常
→ 内核将异常解释为SIGFPE信号发送给进程
2. 非法内存访问:
MMU(内存管理单元)产生异常
→ 内核将异常解释为SIGSEGV信号发送给进程
2.5.1 模拟除0
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
signal(SIGFPE, handler); // 8号信号
sleep(1);
int a = 10;
a除0操作
while(1);
return 0;
}
bash
$ gcc sig.c -o sig
$ ./sig
catch a sig : 8
catch a sig : 8
catch a sig : 8
catch a sig : 8
... ← 一直产生8号信号
📌 思考:为什么一直有8号信号产生?
bash
原因分析:
1. CPU执行除0指令时,运算单元检测到异常
2. CPU中有状态寄存器,记录了异常状态标记位
3. OS检查状态寄存器,发现异常存在
4. OS调用对应的异常处理方法(发送信号)
5. 但是!我们并没有清理现场:
- 没有清理内存
- 没有关闭进程打开的文件
- 没有切换进程
- CPU寄存器内容还保留着
6. 所以除0异常会一直存在,信号就一直发出
正确的处理方式:
cpp
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
exit(1); // 退出进程,清理现场
}
2.5.2 模拟野指针
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
signal(SIGSEGV, handler); // 11号信号
sleep(1);
int *p = NULL;
*p = 100; // 野指针解引用
while(1);
return 0;
}
bash
# 默认行为
$ gcc sig.c -o sig
$ ./sig
Segmentation fault (core dumped)
# 捕捉行为
$ ./sig ← 启用handler
catch a sig : 11
catch a sig : 11
catch a sig : 11
... ← 一直产生11号信号
原理同除0,不再赘述。
2.5.3 子进程退出core dump
cpp
#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
exit(0);
}
// 父进程
int status = 0;
waitpid(-1, &status, 0);
printf("exit signal: %d, core dump: %d\n",
status & 0x7F, // 低7位是信号编号
(status >> 7) & 1); // 第8位是core dump标志
return 0;
}
bash
$ ./sig
exit signal: 8, core dump: 1
status的位图结构:
bash
status(int类型,32位):
┌─────┬─────┬─────────────────┬─────────────────┐
│ ... │core │ exit signal(7位)│ exit code(8位) │
│ │dump │ │ │
└─────┴─────┴─────────────────┴─────────────────┘
高位 第8位 低7位(0-6) 次低8位(8-15)
如果进程被信号终止:
- 低7位:信号编号
- 第8位:是否产生core dump
- 次低8位:无意义
如果进程正常退出:
- 低7位:0
- 次低8位:exit code

2.5.4 Core Dump
什么是Core Dump?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是
core,这叫做Core Dump。
作用:
bash
1. 事后调试(Post-mortem Debug)
- 进程异常终止通常是因为有Bug
- 比如非法内存访问导致段错误
- 事后可以用调试器检查core文件以查清错误原因
2. 保留现场
- core文件保存了进程的完整状态
- 包括所有变量的值
- 包括调用栈信息
设置core文件大小:
bash
# 查看当前限制
$ ulimit -a
core file size (blocks, -c) 0 ← 默认不允许产生core文件
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
...
# 设置允许产生core文件(最大1024KB)
$ ulimit -c 1024
$ ulimit -a
core file size (blocks, -c) 1024 ← 已修改
...
为什么默认是0?
bash
安全原因:
- core文件中可能包含用户密码等敏感信息
- 默认不允许产生,防止信息泄露
开发调试阶段:
- 可以用ulimit命令改变这个限制
- 允许产生core文件用于调试
测试core dump:
cpp
#include <stdio.h>
int main()
{
int *p = NULL;
*p = 100; // 触发段错误
return 0;
}
bash
$ ulimit -c 1024
$ gcc sig.c -o sig -g ← -g选项产生调试信息
$ ./sig
Segmentation fault (core dumped) ← 产生了core文件
$ ls -lh core
-rw------- 1 user user 180K Jan 23 10:30 core
# 使用gdb调试core文件
$ gdb sig core
GNU gdb (GDB) 8.1
...
Core was generated by `./sig'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000400526 in main () at sig.c:6
6 *p = 100;
(gdb) bt ← 查看调用栈
#0 0x0000000000400526 in main () at sig.c:6
(gdb) p p ← 查看变量p的值
$1 = (int *) 0x0 ← 确实是空指针
(gdb) quit
2.6 总结思考
📌 重要思考题:
-
上面所说的所有信号产生,最终都要由OS来执行,为什么?
- OS是进程的管理者
- 只有OS能修改进程的PCB
- 信号的本质是修改进程PCB中的pending位图
-
信号的处理是否是立即处理的?
- 不是立即处理
- 在"合适的时候"处理
- 具体时机:从内核态返回用户态时检查pending
-
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?
- 是的,需要记录
- 记录在PCB的pending位图中
-
记录在哪里最合适呢?
- PCB(进程控制块)
- 因为PCB是内核管理进程的核心数据结构
- 下一篇会详细讲解
-
一个进程在没有收到信号的时候,能否知道自己应该对合法信号作何处理呢?
- 能
- 处理方法在信号产生之前就已经准备好了
- 记录在PCB的handler数组中
-
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
- 下一篇详细讲解
五种信号产生方式总结:
| 方式 | 函数/操作 | 信号 | 特点 |
|---|---|---|---|
| 终端按键 | Ctrl+C//Z | SIGINT/SIGQUIT/SIGTSTP | 硬件中断触发 |
| 系统命令 | kill命令 | 任意信号 | 用户主动发送 |
| 系统函数 | kill/raise/abort | 任意/当前/SIGABRT | 编程控制 |
| 软件条件 | alarm/管道 | SIGALRM/SIGPIPE | 软件状态触发 |
| 硬件异常 | 除0/野指针 | SIGFPE/SIGSEGV | CPU检测异常 |
三、本篇总结
本篇我们从生活例子出发,深入学习了信号的概念和五种产生方式。
核心知识点:
-
信号的本质
- 进程间事件异步通知的一种方式
- 软件模拟硬件中断的机制
- 三个阶段:信号到来 → 信号保存 → 信号处理
-
信号处理的三种方式
- 默认动作(SIG_DFL)
- 忽略信号(SIG_IGN)
- 自定义捕捉(handler函数)
-
五种信号产生方式
- 终端按键(Ctrl+C//Z)
- 系统命令(kill)
- 系统函数(kill/raise/abort)
- 软件条件(alarm/管道)
- 硬件异常(除0/野指针)
-
重要理解
- 信号处理不是立即的,在合适的时候
- 信号处理方法在信号产生前就准备好了
- 信号相对于进程是异步的
- IO操作非常耗时,影响程序效率
完整流程图:
bash
信号产生 信号保存 信号递达
──────── ──────── ────────
键盘/函数/ PCB的pending 从内核态返回用户态时
异常/条件 位图记录信号 检查pending和block
│ │ │
├──────────────────────→├──────────────────────→│
│ │ │
│ 如果被阻塞 │
│ 暂不处理 检查handler
│ │ ────────
│ 如果未阻塞 │ 默认 │
│ 等待递达 │ 忽略 │
│ │ │ 自定义 │
└───────────────────────┴───────────────────┴───────→
执行处理动作
下一篇预告:
在下一篇中,我们将深入学习:
bash
1. 信号在内核中如何保存?
- PCB中的pending位图
- PCB中的block位图
- PCB中的handler数组
2. 信号集操作函数
- sigemptyset/sigfillset
- sigaddset/sigdelset
- sigismember
- sigprocmask
- sigpending
3. 实验验证
- 打印pending位图
- 阻塞信号
- 解除阻塞
- 观察信号递达
💬 总结:通过本篇的学习,我们从生活例子理解了信号的本质,掌握了五种信号产生方式,并通过大量代码实验验证了信号的基本行为。信号机制是Linux进程间通信的重要方式,也是理解操作系统运行机制的重要窗口。下一篇我们将深入内核,看看信号是如何被保存和管理的。
👍 点赞、收藏与分享:如果这篇文章对你有帮助,请点赞、收藏并分享给更多需要的人!有任何问题欢迎在评论区讨论。
附录:常用信号速查表
| 信号名 | 编号 | 默认动作 | 说明 | 产生方式 |
|---|---|---|---|---|
| SIGHUP | 1 | Term | 终端挂断 | 终端关闭 |
| SIGINT | 2 | Term | 终端中断 | Ctrl+C |
| SIGQUIT | 3 | Core | 终端退出 | Ctrl+\ |
| SIGILL | 4 | Core | 非法指令 | CPU异常 |
| SIGABRT | 6 | Core | 异常终止 | abort() |
| SIGFPE | 8 | Core | 浮点异常 | 除0 |
| SIGKILL | 9 | Term | 强制终止 | kill -9 |
| SIGSEGV | 11 | Core | 段错误 | 野指针 |
| SIGPIPE | 13 | Term | 管道破裂 | 写关闭的管道 |
| SIGALRM | 14 | Term | 定时器 | alarm() |
| SIGTERM | 15 | Term | 终止信号 | kill默认 |
| SIGCHLD | 17 | Ign | 子进程状态变化 | 子进程退出 |
| SIGCONT | 18 | Cont | 继续执行 | fg/bg |
| SIGSTOP | 19 | Stop | 停止执行 | kill -19 |
| SIGTSTP | 20 | Stop | 终端停止 | Ctrl+Z |
📌 注意:
- Term:终止进程
- Core:终止进程并产生core dump
- Ign:忽略信号
- Stop:暂停进程
- Cont:继续执行被暂停的进程