
目录
[1. 信号快速认识](#1. 信号快速认识)
[1-1 ⽣活⻆度的信号](#1-1 ⽣活⻆度的信号)
[1-2 技术应⽤⻆度的信号](#1-2 技术应⽤⻆度的信号)
[1-2-1 ⼀个样例](#1-2-1 ⼀个样例)
[1-2-2 ⼀个系统函数](#1-2-2 ⼀个系统函数)
[1-3 信号概念](#1-3 信号概念)
[1-3-1 查看信号编辑](#1-3-1 查看信号编辑)
[1-3-2 信号处理](#1-3-2 信号处理)
[2. 产⽣信号](#2. 产⽣信号)
[2-1 通过终端按键产⽣信号](#2-1 通过终端按键产⽣信号)
[2-1-1 基本操作](#2-1-1 基本操作)
[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-5 硬件异常产⽣信号](#2-5 硬件异常产⽣信号)
[2-5-1 模拟除0](#2-5-1 模拟除0)
[2-5-2 模拟野指针](#2-5-2 模拟野指针)
[3. 保存信号](#3. 保存信号)
[3-1 信号其他相关常⻅概念](#3-1 信号其他相关常⻅概念)
[3-2 在内核中的表⽰](#3-2 在内核中的表⽰)
[4. 捕捉信号](#4. 捕捉信号)
[4-1 信号捕捉的流程](#4-1 信号捕捉的流程)
[5. 可重⼊函数](#5. 可重⼊函数)
1. 信号快速认识
1-1 ⽣活⻆度的信号
•
你在⽹上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临
时,你该怎么处理快递。也就是你能"识别快递"
•
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。
那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的⾏为
并不是⼀定要⽴即执⾏,可以理解成"在合适的时候去取"。
•
在收到通知,再到你拿到快递期间,是有⼀个时间窗⼝的,在这段时间,你并没有拿到快递,但是
你知道有⼀个快递已经来了。本质上是你"记住了有⼀个快递要去取"
•
当你时间合适,顺利拿到快递之后,就要开始处理快递了。⽽处理快递⼀般⽅式有三种:1. 执⾏默
认动作(幸福的打开快递,使⽤商品)2. 执⾏⾃定义动作(快递是零⻝,你要送给你你的⼥朋友)
3. 忽略快递(快递拿上来之后,扔掉床头,继续开⼀把游戏)
•
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
基本结论:
•
你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
•
信号产⽣之后,你知道怎么处理吗?知道。如果信号没有产⽣,你知道怎么处理信号吗?
知道。所以,信号的处理⽅法,在信号产⽣之前,已经准备好了。
处理信号,⽴即处理吗?我可能正在做优先级更⾼的事情,不会⽴即处理?什么时候?合
适的时候。
•
信号到来 | 信号保存 | 信号处理
•
怎么进⾏信号处理啊?a.默认 b.忽略 c.⾃定义, 后续都叫做信号捕捉。
1-2 技术应⽤⻆度的信号
1-2-1 ⼀个样例
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);
}
}
$ g++ sig.cc -o sig
$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C
⽤⼾输⼊命令,在Shell下启动⼀个前台进程
•
⽤⼾按下 Ctrl+C ,这个键盘输⼊产⽣⼀个硬件中断,被OS获取,解释成信号,发送给⽬标前台进
程
•
前台进程因为收到信号,进⽽引起进程退出
1-2-2 ⼀个系统函数
cpp
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:信号编号[后⾯解释,只需要知道是数字即可]
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法
⽽其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下,这⾥需要引⼊⼀
个系统调⽤函数
开始测试
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(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
我是进程: 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!
I am a process, I am waiting signal!
1-3 信号概念
信号是进程之间事件异步通知的⼀种⽅式,属于软中断。
1-3-1 查看信号
每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义
编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各⾃在什么条件
下产⽣,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

1-3-2 信号处理
( sigaction 函数稍后详细介绍),可选的处理动作有以下三种:
•
忽略此信号。
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(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212681
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C^C^C^C^C^CI am a process, I am waiting signal! // 输⼊ctrl+c毫⽆反应
I am a process, I a
执⾏该信号的默认处理动作
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(SIGINT/*2*/, SIG_DFL);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C // 输⼊ctrl+c,进程退出,就是默认动作
提供⼀个信号处理函数,要求内核在处理该信号时切换到⽤⼾态执⾏这个处理函数,这种⽅式称为⾃
定义捕捉(Catch)⼀个信号。
cpp
1 // 就是开始的样例
上⾯的所有内容,我们都没有做⾮常多的解释,主要是先⽤起来,然后渗透部分概念和共识,下⾯我 们从理论和实操两个层⾯,来进⾏对信号的详细学习、论证和理解。为了保证条理,我们采⽤如下思路来进⾏阐述:
2. 产⽣信号
当前阶段:
2-1 通过终端按键产⽣信号
2-1-1 基本操作
•
Ctrl+C (SIGINT) 已经验证过,这⾥不再重复
•
Ctrl+\(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);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 213056
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\我是: 213056, 我获得了⼀个信号: 3
// 注释掉13⾏代码
$ ./sig
我是进程: 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\Quit
Ctrl+Z(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);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ ./sig
我是进程: 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z我是: 213552, 我获得了⼀个信号: 20
// 注释掉13⾏代码
$ ./sig
我是进程: 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+ Stopped ./sig
whb@bite:~/code/test$ jobs
[1]+ Stopped ./sig
1
2-2 调⽤系统命令向进程发信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
while(true){
sleep(1);
}
}
$ g++ sig.cc -o sig // step 1
$ ./sig & // step 2
$ ps ajx |head -1 && ps ajx | grep sig // step 3
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
211805 213784 213784 211805 pts/0 213792 S 1002 0:00 ./sig
⾸先在后台执⾏死循环程序,然后⽤kill命令给它发SIGSEGV信号。
cpp
$ kill -SIGSEGV 213784
$ // 多按⼀次回⻋
[1]+ Segmentation fault ./sig
213784 是 sig 进程的pid。之所以要再次回⻋才显⽰ Segmentation fault ,是因为在
213784 进程终⽌掉之前已经回到了Shell提⽰符等待⽤⼾输⼊下⼀条命令, Shell 不希望
Segmentation fault 信息和⽤⼾的输⼊交错在⼀起,所以等⽤⼾输⼊命令之后才显⽰。
•
指定发送某种信号的 kill 命令可以有多种写法,上⾯的命令还可以写成 kill -11
213784 , 11 是信号 SIGSEGV 的编号。以往遇到的段错误都是由⾮法内存访问产⽣的,⽽这个
程序本⾝没错,给它发SIGSEGV也能产⽣段错误
2-3 使⽤函数产⽣信号
2-3-1 kill
kill 命令是调⽤ kill 函数实现的。 kill 函数可以给⼀个指定的进程发送指定的信号。
cpp
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.
2-3-2 raise
raise 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。
cpp
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.
2-3-3 abort
abort 函数使当前进程接收到信号⽽异常终⽌。
cpp
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
// 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值
2-4 由软件条件产⽣信号
SIGPIPE 是⼀种由软件条件产⽣的信号,在"管道"中已经介绍过了。本节主要介绍 alarm 函数
和 SIGALRM 信号。
cpp
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 previ‐
ously scheduled alarm.
调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发
SIGALRM 信号,该信号的默认处理动作是终⽌当前进程。
•
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个⽐⽅,某⼈要⼩睡⼀觉,设定闹
钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡⼀会⼉,于是重新设定闹钟为15分钟之后响,"以
前设定的闹钟时间还余下的时间"就是10分钟。如果seconds值为0,表⽰取消以前设定的闹钟,函数
的返回值仍然是以前设定的闹钟时间还余下的秒数。
2-5 硬件异常产⽣信号
硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前
进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进
程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送
给进程。
2-5-1 模拟除0
cpp
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
// v1
int main()
{
//signal(SIGFPE, handler); // 8) SIGFPE
sleep(1);
int a = 10;
a/=0;
while(1);
return 0;
}
2-5-2 模拟野指针
cpp
//默认⾏为
[hb@localhost code_test]$ cat sig.c
#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;
while(1);
return 0;
}
[hb@localhost code_test]$ ./sig
Segmentation fault (core dumped)
[hb@localhost code_test]$
//捕捉⾏为
[hb@localhost code_test]$ cat sig.c
#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;
while(1);
return 0;
}
[hb@localhost code_test]$ ./sig
[hb@localhost code_test]$ ./sig
catch a sig : 11
catch a sig : 11
catch a sig : 11
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。
3. 保存信号
当前阶段
3-1 信号其他相关常⻅概念
•
实际执⾏信号的处理动作称为信号递达(Delivery)
信号从产⽣到递达之间的状态,称为信号未决(Pending)。
•
进程可以选择阻塞 (Block )某个信号。
•
被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
•
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动
作。
3-2 在内核中的表⽰
信号在内核中的表⽰ 意图
每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动
作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上
图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
•
SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻
塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
•
SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤⼾⾃定义函数
sighandler。
如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?POSIX.1允许系统递送该信
号⼀次或多次。Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之
前产⽣多次可以依次放在⼀个队列⾥。本章不讨论实时信号。
4. 捕捉信号
当前阶段
4-1 信号捕捉的流程
如果信号的处理动作是⽤⼾⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。
由于信号处理函数的代码是在⽤⼾空间的,处理过程⽐较复杂,举例如下:
⽤⼾程序注册了 SIGQUIT 信号的处理函数 sighandler 。
•
当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态。
•
在中断处理完毕后要返回⽤⼾态的 main 函数之前检查到有信号 SIGQUIT 递达。
•
内核决定返回⽤⼾态后不是恢复 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 函
数, sighandler 和 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个
独⽴的控制流程。
•
sighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态。
•
如果没有新的信号要递达,这次再返回⽤⼾态就是恢复 main 函数的上下⽂继续执⾏了。、
5. 可重⼊函数
main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的
时候,因为硬件中断使进程切换到内核,再次回⽤⼾态之前检查到有信号待处理,于是切换 到
sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的
两步都做完之后从sighandler返回内核态,再次回到⽤⼾态就从main函数调⽤的insert函数中继续
往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后 向
链表中插⼊两个节点,⽽最后只有⼀个节点真正插⼊链表中了。
像上例这样,insert函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函
数,这称为重⼊,insert函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为 不可
重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重⼊(Reentrant) 函数。想⼀
下,为什么两个不同的控制流程调⽤同⼀个函数,访问它的同⼀个局部变量或参数就不会造成错乱?
如果⼀个函数符合以下条件之⼀则是不可重⼊的:
•
调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
•
调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。