目录
1 信号产生的原理
1.1 信号是如何记录存储的?
【分析】
一个操作系统中,存在着多个进程,每个进程有着多种信号;所以操作系统要管理这些的信号--先描述,在组织!
描述:普通型号1~31一共有31种,对于一个进程来说,预先已经知道对于这31种信号的处理方法,所以OS只需要告诉进程某个信号是否存在--只有两种状态--位图结构!
组织:在学习进程时候,我们应该了解了每个进程都对应一个task_struct(PCB),在这个task_struct中,记录着进程的各种信息,各种信息中同样也包括信号的记录。信号在task_struct中是以位图的方式记录的,task_struct有变量signal,可以把它的类型理解成无符号整数。比特位的位置为信号编号,比特位的内容为是否收到信号,假如收到6号信号就会把第六个比特位置。
1.2 信号发送
-
所谓的发送信号,本质其实写入信号,直接改特定进程的信号位图中的特定的比特位,0->1
-
task_struct是内核数据结构,只能由OS来修改 -- 无论什么样产生信号的方法,最终都是让OS来完成最后的发送过程!
1.3 信号捕捉
signal函数
原型:signal(int sig, void (*func)(int))
功能:用来自定义信号处理方式
参数:
- sig:信号编号
- func为函数指针,传入的函数为信号的处理方式
2 信号产生 方式
2.1 通过终端按键产生信号
通过终端按键也就是通过键盘产生信号,比如我们常用的ctrl+c。ctrl+\。
注意:
- Ctrl-C 产生的信号只能发给前台进程。Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
- 一个命令后面加个&可以放到后台运行,bash默认是前台进程,这样bash(Shell)不必等待进程结束就可以接受新的命令,启动新的进程。
- 前台进程在运行的任何时候都可能收到ctrl+c产生的信号发生终止,所以说信号对于进程的控制流程来说是异步。
问题:我们平时在输入的时候,计算机怎么知道我从键盘输入了数据呢?
答:键盘是通过硬件中断的方式,通知系统我们的按键已经按下了。
【按键信号分析】
CPU硬件有着多个针脚,键盘的按键按下的时候,通过转化对应的针脚会产生中断号,OS此时就会感知到,向中断向量表中发送信号,从该表中,OS就可以分析出按下的对应的是哪一个按键,并将按键解析成对应的信号(整形),这时候OS找到对应的前台的进程并发送相应的信号。
2.2 通过系统调用向进程发送信号
首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。
- 4568是test进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在4568进程终止掉 之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用 户的输入交错在一起,所以等用户输入命令之后才显示。
- 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 4568 或 kill -114568 , 11是信号SIGSEGV的编号。以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
kill函数 -- 接受别人发送的信号
signo为信号编号。成功返回0,失败返回-1。
【模拟实现kill命令】
#include <iostream>
using namespace std;
#include <cstring>
#include <sys/types.h>
#include <signal.h>
void Usage(char *str)
{
printf("%s 信号编码 进程pid\n", str);
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
}
else
{
int pid = atoi(argv[2]);
int sig = atoi(argv[1] + 1);
cout << "pid:" << pid << "; sig:" << sig << endl;
int ret = kill(pid, sig);
if (ret != 0)
{
perror("kill:");
}
}
return 0;
}
raise函数
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
int raise(int signo);
成功返回0,失败返回-1。
abort函数
void abort(void);
功能:向自己发送6号信号SIGABRT
注意:SIGABRT可以被捕捉,但是捕捉之后依然会让进程终止,这就是SIGABRT的特点
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
2.3 由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。(当读端关闭的时候,OS会向写端发送SIGPIPE信号从而终止写端的进程)
本节主要介绍alarm函数 和SIGALRM信号。
alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
alarm的返回值是0或者是剩余秒数。如果闹钟被提前唤醒,返回值为剩余秒数,否则是0。
【使用】写一个计数程序,看看cou一秒钟可以计多少次数。
int count=0;
void myhandler(int signo)
{
cout<<"count:"<<count<<endl;
cout << "get a signo:" << signo << endl;
exit(0);
}
int main()
{
signal(SIGALRM, myhandler);
alarm(1);
while (true)
{
count++;
}
}
这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。
2.4 硬件异常产生信号
在代码中有野指针,除0等操作时程序会出现异常,这时候会产生信号,然后程序崩溃。程序崩溃的本质就是收到了信号。
下面对这个过程进行具体解释:以除0操作为例,我们知道计算都是在cpu中的,cpu中有一个状态寄存器,当进行除0操作后,状态寄存器会异常。os是软硬件的管理者,当检测到cpu状态异常后,会定位到相应的进程,更改进程的信号位图,进程通过位图识别到信号发生崩溃。信号宏名字:SIGFPE
再比如当前进程访问了非法内存地址,负责虚拟地址与物理内存的一个硬件MMU会产生异常,之后和上述过程一样。信号:SIGSEGV
空指针和野指针问题分析:
多种信号分析
问题:普通信号有有31种,而默认行为只有终止进程,停止进程,继续进程等较少的功能,而为什么操作系统会提供这么多信号机制呢?
答:因为我们并不关心进程的终止,而是要知道进程为什么会终止,多种信号,可以提供给我们更加丰富的信息,从而让我们方便的解决问题!
问题:终止进程的信号有两种action:Term&Core,这有什么用?
再验证之前我们先来学习一个概念:Core Dump
在程序正常结束后,我们可以通过错误码判断程序是否运行正确。但代码在运行过程中出错,我们也要有办法判断,其中一个方法就是调试,除此之外,linux为我们提供了核心转储功能。当一个进程要异常终止时,可以选择把进程的核心内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump
核心转储功能一般在云服务器,线上生产时是关闭的。
ulimit -a 查看系统资源
可以发现core文件大小为0。想要使用此功能,我们首先要改一下core文件大小。
可以发现在发送信号后进行了(core dumped),并且多出了一个core.6229文件。我们使用gdb调试myproc,再查看core文件,可以得到进程异常终止信息。这种方法也适用于代码中内存越界,除0等错误,可以快速定位到第几行。
问题:核心转储功能调试的时候这么好用,为什么云服务器默认会关闭它?
答:写代码一共有三种环境:开发环境、测试环境、生产环境;云服务器是生产环境。在生产环境里面,为了维护软件的稳定,当某个进程出了问题,会被其他进程立马重启,从而保证目前的软件能跑!而核心转储功能会在每次程序异常退出的时候生产一个core.pid二进制文件,而且该文件通常都比较大,当一个程序出了问题且在没人维护的时候被重启多次,就会产生多个core文件,没人管,就会占满磁盘空间,导致整个服务崩溃。所以核心转储功能只适合用来事后处理,当需要的时候在开启这个功能!
status
core dump表示是否有核心转储功能!
获取进程退出的所有信息:
int status=0;
waitpid(id,&status,0);
cout<<"exit code: "<<((status>>8)&0xFF)<<endl;
cout<<"eixt signal: "<<(status&0x7F)<<endl;
cout<<"exit dump flag: "<<((status>>7)&0x1)<<endl;