文章目录
- 1. 信号的基本概念
-
- 1.1 生活中的信号:从类比到技术
- 1.2 信号的处理方式
- 1.3 信号的定义与核心特征
- 2. 信号的生命周期
-
- 2.1 信号的三大阶段:产生 → 保存 → 处理
- 2.2 为什么要保存信号?
- 3. 信号的产生方式
-
- 3.1 通过 kill 命令产生信号
-
- 3.1.1 命令行发送信号
- 3.1.2 signal 函数注册信号处理(系统调用)
- 3.2 通过键盘产生信号
-
- 3.2.1 `Ctrl+C` → SIGINT(2号信号)
- 3.2.2 `Ctrl+\\` → SIGQUIT(3号信号)
- 3.2.3 `Ctrl+Z` → SIGTSTP(19号信号)
- 3.3 通过硬件异常产生信号
-
- 3.3.1 除零错误 → SIGFPE(8号信号)
- 3.3.2 野指针/段错误 → SIGSEGV(11号信号)
- 3.3.3 硬件异常转软件异常的完整流程
- 3.4 通过函数主动产生信号
-
- 3.4.1 kill:向任意进程发信号
- 3.4.2 raise:自己给自己发信号
- 3.4.3 abort:异常终止进程
- 3.5 通过软件条件产生信号
-
- 3.5.1 alarm:闹钟信号
- 4. 核心转储(Core Dump)
-
- 4.1 什么是 Core Dump?
- 4.2 如何开启 Core Dump
- 4.3 如何使用 Core Dump 调试
- 4.4 为什么云服务器默认关闭 Core Dump?
- 5. 补充细节

1. 信号的基本概念
1.1 生活中的信号:从类比到技术
信号 vs 信号量:有没有关系?没有关系,就像老婆饼里没有老婆一样。 罗列生活中的信号,提炼相关结论:
- 信号产生之后,你知道怎么处理吗?知道。
- 信号处理的方法,在信号产生之前就已经准备好了。
- 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理,什么时候处理,合适的时候。
- 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。 认识信号 → 识别产生 → 动作处理
1.2 信号的处理方式
信号的处理有三种:
信号的处理就叫做信号捕捉。
1.3 信号的定义与核心特征
给信号下定义:外部或者其他人或者硬件给进程发送的一种异步的事件通知机制——告诉进程什么事情发生了。
- 异步:多种事件,彼此不影响,同时发生。
- 通知:告诉进程发生什么事情了。
2. 信号的生命周期
2.1 信号的三大阶段:产生 → 保存 → 处理
信号产生 → 信号保存 → 信号处理 
2.2 为什么要保存信号?
因为信号不会被立即处理,所以才需要保存起来。
3. 信号的产生方式

用来对特定信号进行捕捉的函数。 查看信号: 
- 1-31:标准信号
- 31-64:实时信号(32和33通常保留或被内核占用)
使用的时候既可以使用名字,也可以使用数字。但是注意没有0号信号。
3.1 通过 kill 命令产生信号
3.1.1 命令行发送信号
kill –[编号] [进程编号]
进程的默认动作是终止信号。
3.1.2 signal 函数注册信号处理(系统调用)
如何让进程执行指定动作:signal()
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
// signo:是收到了几号信号,才让我执行的
void sig_handler(int signo)
{
std::cout << "catch signal: " << signo << std::endl;
exit(10);
}
int main()
{
// 调用一次就可以了,不用放到循环里
signal(SIGINT, sig_handler);
signal(3, sig_handler);
while (true)
{
std::cout << "test sig…, pig " << getpid() << std::endl;
sleep(1);
}
return 0;
}
结论:Ctrl+C 终止进程,本质上是通过键盘给目标进程发送二号信号,处理动作就是终止。
3.2 通过键盘产生信号
3.2.1 Ctrl+C → SIGINT(2号信号)
Ctrl+C 是向目标进程发送二号信号(SIGINT),默认动作终止进程。
3.2.2 Ctrl+\\ → SIGQUIT(3号信号)
Ctrl+\\ 是向目标进程发送三号信号(SIGQUIT),默认动作终止进程。
3.2.3 Ctrl+Z → SIGTSTP(19号信号)
Ctrl+Z 是向目标进程发送十九号信号(SIGTSTP),默认暂停进程。用 kill -18 将其唤醒,默认变成后台进程,不再从键盘获取输入。 我们怎么知道信号的默认动作是什么?
man 7 signal
查看7号手册。 
键盘发送只能控制前台进程,无法控制后台进程——因为只有前台进程才能获取键盘输入! jobs 查看后台进程。 为什么 bash 进程自己不对信号做响应?因为 bash 将信号全部忽略了。但是9号信号依旧能杀掉 bash 进程。
Makefile忽略告警:-w,显示全部告警:-Wall
3.3 通过硬件异常产生信号
如果我们的程序 div 0、野指针等,程序就会崩溃——为什么他会崩溃?
3.3.1 除零错误 → SIGFPE(8号信号)
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void sig_handler(int signo)
{
std::cout << "catch signal: " << signo << std::endl;
exit(10);
}
int main()
{
signal(SIGINT, sig_handler);
signal(3, sig_handler);
while (true)
{
std::cout << "test sig…, pig " << getpid() << std::endl;
sleep(1);
// 模拟除0
int a = 10;
a /= 0;
// 模拟野指针
int *p = nullptr;
*p = 100;
}
return 0;
}
本质是因为:程序因为异常,导致收到了信号,让进程终止了。

标志寄存器(EFlags):标志位表明当前计算是否可信。计算溢出时,标志位置为1,表明运算出现异常,当前CPU执行当前代码的一部分。
3.3.2 野指针/段错误 → SIGSEGV(11号信号)
为什么野指针也会触发硬件异常?操作系统识别到 MMU 报错。
3.3.3 硬件异常转软件异常的完整流程
当前CPU执行当前进程代码的一部分,但是OS如何知道当前进程是谁?操作系统内核中有一个全局的指针,永远指向当前进程正在运行的进程。 收到8号信号,用户捕捉到了这个信号,打印,没有让进程退出!OS会继续调度它,寄存器的内容都是当前进程的硬件上下文,依旧接着执行。这个进程一直在,CPU就会一直给你发消息,就会陷入死循环。 问题:为什么会收到信号?OS怎么知道当前进程 div 0 或者野指针? 进程是如何保存信号的?[1-31]用什么数据结构保存?用位图保存。信号被保存到进程的 task_struct 里。
比特位的位置 = 信号编号;比特位的内容(1 / 0)表示是否收到信号。
你如何理解向目标进程“发送”信号?本质是修改信号位图,也就是写信号。
信号由谁来写?信号位图在 task_struct 中,修改位图本质就是修改内核数据结构,只有操作系统有资格修改。发送信号的方式有很多种,但是最终只能由 OS 向目标进程写信号。
- div 0 本质是 CPU 出错了。
- 野指针本质是 MMU + 页表转化不了,MMU 报错。这都属于硬件报错。OS 必须知道硬件报错了,操作系统是硬件的管理者,必须知道谁把我的硬件搞坏了——当前正在运行的进程。这个时候操作系统就会对目标进程写信号,就可以将硬件异常转变为软件异常,让你的进程直接崩溃。
3.4 通过函数主动产生信号
3.4.1 kill:向任意进程发信号

实现一个 kill 命令: mykill.cc
#include <iostream>
#include <signal.h>
void Usage(const std::string &cmd)
{
std::cout << "Usage: " << cmd << " signumber who " << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signumber = std::stoi(argv[1]);
pid_t pid = std::stoi(argv[2]);
int n = kill(pid, signumber);
(void)n;
return 0;
}
loop.cc
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "我是一个进程: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
Makefile
.PHONY:all
all:loop_process mykill
mykill:mykill.cc
g++ -w -o $@ $^
loop_process:loop.cc
g++ -w -o $@ $^
.PHONY: clean
clean:
rm -f mykill loop_process
信号是谁发的?OS。谁让的?用户!
3.4.2 raise:自己给自己发信号

raise 说白了就是自举,也就是自己给自己发信号。
#include <iostream>
#include <signal.h>
void hander(int sig)
{
std::cout << "进程捕捉到信号: " << sig << " pid: " << getpid() << std::endl;
}
int main()
{
signal(2, hander);
while (true)
{
std::cout << "进程正在运行: " << getpid() << std::endl;
sleep(2);
raise(2);
}
return 0;
}
这个系统调用就相当于:
kill(getpid(), number);
3.4.3 abort:异常终止进程

abort 引起进程直接终止,给调用者进程发送指定信号。 这个系统调用就相当于:
void abort(void)
{
int n = kill(getpid(), SIGABRT);
(void)n;
}
通常用来进行终止进程的,类似于 exit。很多项目里喜欢用它来终止进程,因为出异常的时候可以产生 Core 文件,根据 Core 文件来获取错误信息。
abort 收到的是6号信号(SIGABRT),是可以被捕捉的:
#include <iostream>
#include <signal.h>
void hander(int sig)
{
std::cout << "进程捕捉到信号: " << sig << " pid: " << getpid() << std::endl;
}
int main()
{
signal(6, hander);
while (true)
{
std::cout << "进程正在运行: " << getpid() << std::endl;
sleep(2);
abort();
}
return 0;
}

这个就比较特殊了,既会执行 hander 方法,又会 Aborted。前面的对信号的修改是"非此即彼"的,这里是“即可又可”的。
3.5 通过软件条件产生信号
信号产生方式总结:
发送信号的方式很多,围绕着用户、硬件、软件各种场景展开的——借助 OS 之手向目标进程写信号。
3.5.1 alarm:闹钟信号
给特定的进程设定闹钟: 
seconds 秒之后的闹钟,OS 会给调用者发送一个 14号信号(SIGALRM),默认动作是终止。 怎么证明是收到了14号信号? 
为什么往显示器上打的时候只打了10000多?往函数里面写不打印就写了1亿多。 因为如果你不断 ++ 的过程中,其实你是在不断访问外设。不断访问显示器,不断 IO,所以效率就降低了。想要输出的结论就是 IO 的效率低。为什么要存在文件内核缓冲区,也就是为了增加效率。不仅仅是外设,还有网卡硬件通过网络传递进来的效率也很低。
alarm 设置的闹钟是一次性的,那么怎么样让其不是一次性的呢?反复设置。 alarm 的返回值:alarm 的返回值是上一个闹钟剩余时间。 
所以千万不要将闹钟的设置放到 while 循环里,这个闹钟不会被触发,因为每次循环都在重新调 alarm,就会将上次的闹钟取消,所以你的闹钟永远不会被触发。闹钟参数设置为0,叫做取消闹钟。
#include <iostream>
#include <signal.h>
int cnt = 1;
void hander(int sig)
{
std::cout << "进程捕捉到信号: " << sig << " pid: " << getpid()
<< " cnt: " << cnt << std::endl;
int n = alarm(2);
(void)n;
std::cout << "上一个闹钟的剩余时间: " << n << std::endl;
}
int main(int argc, char *argv[])
{
// signal(SIGALRM, hander);
alarm(200);
sleep(1);
int n = alarm(0); // 取消闹钟
std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << " n= " << n << std::endl;
while (true)
{
std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << " n= " << n << std::endl;
sleep(1);
}
return 0;
}
如何理解闹钟?如何理解闹钟是如何实现的? 闹钟实际上是一个计时器!操作系统里面有很多闹钟,OS 要不要管理所有的闹钟?要管理,先描述再组织! 描述结构体里肯定有:
找到超时时间最近的,后面的就都不会超时了。所以我们可以利用最小堆!堆顶不超时,其余的都不会超时。如果堆顶超时,你就出堆,并将函数回调,给目标进程发送14号信号,堆就会终止了。但是 OS 没有用这种方式,OS 使用的是一种有顺序的顺序表,说堆是为了便于大家理解。
4. 核心转储(Core Dump)
4.1 什么是 Core Dump?

进程退出了,核心转储。
- Term:没有异常的情况下,就是单纯终止
- Core:进程本身有错误,收到信号退出,可能会发生核心转储,设置进程退出 status core dumped 标志位给父进程 上面二者都是终止进程。 核心转储:为什么退出?在哪里退出?在内存中 -> 便于用户调试 -> 进程运行时异常信息,转储到磁盘中!核心转储是为了后续 debug。
4.2 如何开启 Core Dump
云服务器默认是将核心转储功能关闭的。
ulimit -c 10240
开启核心转储。
4.3 如何使用 Core Dump 调试
这里引出一种实用的调试技巧,将你的代码开启 cgdb 开始调试,直接输入:
core-file core.(编号)
就可以直接定位到错误的地方。 形成 core 是方便我们进行事后调试的,所以说核心转储是为了后续 debug 的。
4.4 为什么云服务器默认关闭 Core Dump?
为什么云服务器默认是把核心转储功能关闭的?很少用到,使用也是因为这样的错误是很不稳定的。我们在写软件服务的时候,进程可能会因为各种问题挂掉,这个时候运维平台会将软件自动立即重启,挂一次重启一次,挂一次重启一次,core 文件会很占存储空间,所以一般是关掉的。
我怎么知道当前的信息发生了核心转储呢? bash -> 子进程(执行我们的代码)-> Floating point exception 或 Floating point exception (core dumped) -> bash bash 进程等待子进程的退出信息,bash 进程拿到子进程的退出信息之后,从终止信号中得到 core dump 标志位。
5. 补充细节
细节一:信号自定义捕捉,如果你的捕捉方法不退出,进程可能会不退出了。如果我把所有信号都自定义了,那就都不退出了?(变成后台进程了)
注意:9号信号(SIGKILL)不可被自定义,是管理员信号。
细节二:信号处理是谁处理?有没有创建新进程?信号处理是进程自己做的。显然没有创建新的进程 细节三:
- SIG_DFL:默认处理,执行该信号的系统默认动作
- SIG_IGN:忽略。就是处理了,但是什么动作都不做
9号(SIGKILL)和 19号(SIGSTOP)不仅不可被忽略,也不可被自定义捕捉。
signal(SIGINT, sig_handler);
发送信号的本质:就是 OS 写信号,利用系统调用。
欢迎大家批评指正!!!

