Linux信号(上):信号概念、信号产生
文章目录
- 一、信号概念
- 1. 生活角度的信号
- 2. 技术应用角度的信号
- 3. 为什么使用Ctrl+C后,该进程就终止了?
- 真相大白
- 硬件中断
- 4. 信号概念引出
- 5. 信号处理常见方式
- 二、信号产生
- 1. 通过终端按键产生信号
- signal 注册执行动作
- 2. 调用系统函数向进程发信号
- kill函数
- raise函数
- abort函数
- 3. 由硬件异常产生信号
- 4. 由软件条件产生信号
- 5. 核心转储
- 核心转储概念
- 核心转储测试
- 核心转储作用
- core dump标志位
一、信号概念
先输出结论,以支撑我们对信号的理解。
1. 生活角度的信号
- 现实生活中的信号很多,如信号弹、上下课的铃声、红绿灯、古代的狼烟、闹钟……
- 思考一下,你是怎么认识这些信号的?
因为有人教,让我们具有识别信号和处理信号的能力。 - 即使我们现在没有信号产生,我们也知道信号产生后,我们该干什么!
- 信号产生后,我们可能并不立即去处理这个信号,而是在合适的时候去处理(因为可能当前有更重要的事)。但是在信号产生到处理信号的时间窗口里,你必须记住这个信号!
比如你打王者正在推对高地,此时外卖突然到了,你肯定会先拿下对面水晶再去拿外卖
2. 技术应用角度的信号
生活中的你对应计算机世界中的谁?
没错,就是进程啦!
所以:
- 进程必须能够识别+处理信号(这些能力是操作系统设计者内置的一部分)
- 进程即便没有收到信号,也知道哪些信号该怎么处理
- 当进程收到了一个具体的信号时,该进程可能不会立即去处理这个信号,会在合适的时候处理
信号的处理方式:- 默认动作:执行该信号的默认处理动作。
- 忽略: 忽略此信号,但信号被处理了
- 自定义动作(捕捉):由用户自定义动作
- 一个进程从信号产生到处理过程一定会存在时间窗口,因此进程具有临时保存信号的能力。
3. 为什么使用Ctrl+C后,该进程就终止了?
#include <iostream>
#include <unistd.h>using namespace std;int main()
{while(1){cout << "I am process……" << endl;sleep(1);}return 0;
}
运行代码,然后ctrl+c:

为什么进程会终止?
Ctrl+C本质是被进程解释为收到了2号信号SIGINT
命令kill -l可查看所有信号:

1~31号为普通信号,剩余信号为实时信号。
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在
signal.h中找到,例如其中有定义#define SIGINT 2 - 编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。
普通信号具体含义可查看:Linux的31个普通信号含义表
其实,Ctrl+C只能杀掉前台进程。
前台进程与后台进程
- 前台进程:获取键盘输入的进程
前台进程只能有一个,云服务器中默认bash就是我们的前台进程。我们平时./运行就是以前台进程方式运行,此时bash进程就进入等待状态,直到前台进程完成。

- 后台进程:不获取键盘输入的进程
后台进程可以有多个,以./XXX&运行就是以后台进程方式运行,此时前台进程是bash进程,输入指令是有效的,但输入Ctrl+C就没有任何效果了。

总结:
Ctrl-C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像
Ctrl-C这种控制键产生的信号。前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
异步:互不干扰,你做你的,我做我的,事情发生你去做即可,我继续做我的。
真相大白
- 用户在Shell下启动一个前台进程。
- 用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
- 前台进程因为收到信号,进而引起进程退出
什么是硬件中断?
硬件中断
键盘数据是如何输入给内核的?
Ctrl+C又是如何变成信号的?
键盘被摁下,肯定是OS先知道的,那么OS是怎么知道键盘上有数据了呢?
答案都在硬件中断的原理中:

可以看到,硬件中断和进程的信号的流程十分雷同,都是先检测到信号,然后再去执行相应的动作
硬件中断就是硬中断,而信号属于软中断
4. 信号概念引出
- 信号是进程之间事件异步通知的一种方式,属于软中断。
5. 信号处理常见方式
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
- 忽略该信号。
在Linux当中,我们可以通过man手册查看各个信号默认的处理动作
man 7 signal

二、信号产生
1. 通过终端按键产生信号
可以通过键盘组合键发出信号的信号有3个:
Ctrl+C:2号信号SIGINTCtrl+\:3号信号SIGQUITCtrl+Z:1号信号SIGHUP
那么,该如何证明按 ctrl + c 发出的是 2 号信号呢?
前面说过,一个信号配有一个执行动作,并且执行动作是可以修改的,需要用到signal函数(这属于信号处理部分的内容,这里需要提前用一下)
signal 注册执行动作
signal 函数可以用来 修改信号的执行动作,也叫注册自定义执行动作

函数剖析:
- 返回值
sighandler_t:调用成功返回上一个执行方法的值,失败则返回SIG_ERR,并设置错误码
返回值一般我们不用管,重点在于参数:
- 参数1:待操作信号的编号(信号名也行)
- 参数2:自定义的新方法
hander
一个函数指针,意味着需要传递一个 参数为int,返回值为空的函数对象
void handler(int) //函数名可以自定义
提一下:
signal函数是一个 回调函数,当信号发出时,会去调用相应的函数,也就是执行相应的动作
我们先对 2 号信号注册新动作,在尝试按下 ctrl + c,看看它发出的究竟是不是 2 号信号:
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void hander(int signo)
{cout << "catch signal success! signal number:" << "signal" << endl;
}int main()
{signal(2, hander);while(1){cout << "I am process……" << getpid() << endl;sleep(1);}return 0;
}

当我们修改 2 号信号的执行动作后,再次按下 ctrl + c 尝试终止前台进程,结果失败了!执行动作变成了我们注册的新动作
现在我们试着将31个普通信号的执行动作都改了,看看会不会得到一个杀不死的进程:
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void hander(int signo)
{cout << "catch signal success! signal number:" << signo << endl;
}int main()
{//捕捉所有普通信号int cnt = 1;while(cnt <= 31){signal(cnt, hander);cnt++;}while(true){cout << "I am process……" << getpid() << endl;sleep(2);}return 0;
}

大部分信号的执行动作都被修改了,但 9 号信号SIGKILL没有,说明不是所有的信号都是可以被signal捕捉的,比如9和19。
这是操作系统在限制用户,以防产生无敌进程。
2. 调用系统函数向进程发信号
当我们要使用kill命令向一个进程发送信号时,我们可以以kill -信号名/编号 进程ID的形式进行发送。

实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号
kill函数
函数原型如下:
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
kill函数用于向进程ID为pid的进程发送sig号信号,如果信号发送成功,则返回0,否则返回-1。
我们可以用kill函数模拟实现一个kill命令,实现逻辑如下:
#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>using namespace std;void Usage(string proc)
{cout << "Usage:\n\t" << proc << " signum pid\n\n";
}int main(int argvc, char* argv[])
{if(argvc != 3){Usage(argv[0]);return 1;}int signo = stoi(argv[1]);pid_t pid = stoi(argv[2]);kill(pid, signo); return 0;
}
此时我们便模拟实现了一个kill命令,该命令的使用方式为./mykill 进程ID 信号编号。

raise函数
raise是给调用者(也就是当前进程)发送信号,即自己给自己发信号。
函数原型:
#include <sys/types.h>
#include <signal.h>int raise(int sig);
raise函数用于给当前进程发送sig号信号,如果信号发送成功,则返回0,否则返回一个非零值。
不难看出,该函数就是对kill函数封装,只是将参数1 pid固定为getpid()了。
比如,下列代码当中用raise函数每隔一秒向自己发送一个2号信号。
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void hander(int signo)
{cout << "catch signal success! signal number:" << signo << endl;
}int main()
{signal(2, hander);while(true){cout << "I am process……" << getpid() << endl;sleep(1);//每过1秒发送一次2号信号raise(2);}return 0;
}

abort函数
abort函数可以向当前进程发送6号信号SIGABRT,使得当前进程异常终止。
函数原型:
#include <stdlib.h>void abort(void);
abort函数是一个无参数无返回值的函数。
不难看出,该函数就是对kill函数封装:
- 将
参数1 pid固定为getpid()。 - 将
参数2 sig固定为6
其实并没有那么简单,看下面示例就知道了。
例如,下列代码当中每隔一秒向当前进程发送一个SIGABRT信号。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>using namespace std;void hander(int signo)
{cout << "catch signal success! signal number:" << signo << endl;
}int main()
{signal(6, hander);while(true){cout << "I am process……" << getpid() << endl;sleep(1);//发送6号信号abort();}return 0;
}

与之前不同的是,虽然我们对SIGABRT信号进行了捕捉,并且在收到SIGABRT信号后执行了我们给出的自定义方法,但是当前进程依然执行默认动作让进程异常终止了。
原来:abort函数的作用是异常终止进程,exit函数的作用是正常终止进程,而abort本质是通过向当前进程发送SIGABRT信号而终止进程的,因此使用exit函数终止进程可能会失败,但使用abort函数终止进程总是成功的。
3. 由硬件异常产生信号
程序崩溃原理
在C/C++程序中,如果出现除0、访问野指针、越界访问等错误时,程序会崩溃终止。为什么呢?
本质上是因为程序在运行过程中收到了操作系统发送的信号而被终止的,那操作系统是如何识别一个进程是否触发了某个问题呢?
对于除0错误
算术运算是由CPU完成的,CPU中有一堆的寄存器,当我们程序需要对两个数进行算术运算时,是先将这两个数分别放到两个寄存器中,然后进行算术运算并将结果写回到寄存器中。但如果计算中出现了错误(如除0错误),CPU中有一个状态寄存器,该寄存器头部有一个比特位,称为溢出标志位,计算出现错误就会将其设为1。OS看到溢出标志位就会立刻识别错误,向进程发送相应信号终止进程。本质就是操作系统去直接找到这个进程的task_struct,并向该进程的位图中写入8信号,写入8号信号后这个进程就会在合适的时候被终止。
对于访问野指针错误和越界访问错误
我们知道,我们要访问一个变量,一定是要通过页表的映射,将虚拟地址转化为物理地址,然后才能进行相应的访问操作。

实际上,它们在页表映射会失败,具体原因如下:
- 其实,通过页表将虚拟地址转化为物理地址的工作是由MMU(内存管理单元)来做的,
- MMU,它是一种负责处理CPU的内存访问请求的计算机硬件,现在的MMU已经集成到CPU当中了。
- 当需要进行虚拟地址到物理地址的映射时,我们先将页表的左侧的虚拟地址导给MMU,然后MMU会计算出对应的物理地址,我们再通过这个物理地址进行相应的访问。
- 而MMU既然是硬件单元,那么它当然也有相应的状态信息,当我们要访问野指针、越界访问类似的不属于我们的虚拟地址时,MMU在进行虚拟地址到物理地址的转换时就会失败而出现错误,然后将对应的错误写入到自己的状态信息当中
- 这时硬件上面的信息也会立马被操作系统识别,然后将对应进程发送对应信号,终止该进程。

因为CPU是硬件,所以称这类错误是硬件异常。
4. 由软件条件产生信号
SIGPIPE信号
其实这种方式我们之前j学习进程间通信的管道时就接触过了:管道读写时,如果读端关闭,那么操作系统会发送信号终止写端,这个就是 软件条件 引发的信号发送,发出的是13号 SIGPIPE 信号
SLGALRM信号
操作系统为我们提供了一个闹钟
调用alarm函数可以设定一个闹钟,也就是告诉操作系统在若干时间后会发送SIGALRM信号给当前进程。
alarm函数的函数原型如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
函数功能:让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程。
alarm函数的返回值:
- 如果上一个闹钟还有剩余时间,则返回剩余时间,否则返回 0
系统中可能不止一个闹钟,所以 OS 需要 先描述,再组织,将这些闹钟管理起来
注意:
- 闹钟可能会提前醒来,就会存在剩余时间
- 闹钟是一次性的,只能响一次
5. 核心转储
核心转储概念
Linux中提供了一种系统级别的能力,当一个进程在出现异常的时候,OS可以将该进程在异常的时候,核心代码部分进行 核心转储,将内存中进程的相关数据,全部 dump到磁盘中,一般会在当前进程的运行目录下,形成 core.pid 这样的二进制文件(核心转储文件)
对于某些信号来说,当终止进程后,需要进行 core dump,产生核心转储文件
比如:3号 SIGQUIT、4号 SIGILL、5号 SIGTRAP、6号 SIGABRT、7号 SIGBUS、8号 SIGFPE、11号 SIGSEGV、24号 SIGXCPU、25号 SIGXFSZ、31号 SIGSYS 都是可以产生核心转储文件的
原因就是这些信号的Action是Core。
还记得man 7 signal里的那个表吗?

这个Action是什么?
Term和Core都代表着终止进程,但Core在终止进程的时候会进行一个动作,这就是核心转储

然而,在云服务器中,核心转储是默认被关闭的,我们可以通过使用ulimit -a命令查看当前资源限制的设定。

其中,第一行显示core文件的大小为0,即表示核心转储是被关闭的。
我们可以通过ulimit -c size命令来设置core文件的大小。

core文件的大小设置完毕后,就是将核心转储功能打开了。
如果要关闭核心转储功能,将core文件的大小重新设置为0就关闭了。
ulimit -c 0
核心转储测试
先写⼀个死循环程序:
#include <iostream>
#include <unistd.h>int main()
{while(true){cout << "I am process……" << getpid() << endl;sleep(1);}return 0;
}
前台运⾏这个程序,然后在终端键⼊Ctrl-\(3号信号的Action是Core),就会发现终止进程后会显示core dumped。

并且会在当前路径下生成一个core文件,该文件以一串数字为后缀,而这一串数字实际上就是发生这一次核心转储的进程的PID。
提一下: ulimit命令改变的是Shell进程的Resource Limit,但test进程的PCB是由Shell进程复制而来的,所以也具有和Shell进程相同的Resource Limit值。
可以看到,核心转储文件非常大,在实际使用中,一个正在运转的服务器如果因为错误而被信号终止并产生核心转储文件,但为了确保用户体验,服务器可能会被立即重新运行,然后又被该错误终止,如此循环会产生大量该文件,直到磁盘被占满然后机器卡死,这是非常严重的问题。所以云服务器一般默认是关闭的。
核心转储作用
既然核心转储文件这么大,它里面有什么内容呢?又有什么用呢?
核心作用是调试。
gcc / g++编译时加上-g生成可调试文件- 直接运行程序,生成 core-dump 文件
gdb程序 进入调试模式core-file + core.pid利用核心转储文件,快速定位至出错的地方

可以看到,调试时,核心转储文件可以帮助我们直接定位到出错的地方
这种调试方式叫做 事后调试
core dump标志位
还记得过去我们谈进程等待waitpid函数的参数status吗?
pid_t waitpid(pid_t pid, int *status, int options);

若进程是正常终止的,那么status的次低8位就表示进程的退出状态,即退出码。若进程是被信号所杀,那么status的低7位表示终止信号,而第8位比特位是core dump标志,即进程终止时是否进行了核心转储。
