👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】
文章目录
- 一,关于信号
- 二,深剖信号的产生
- 1. 键盘组合建产生信号
- 2.核心转储
- 3. 系统调用接口产生信号
- 4. 由软件条件产生信号
- 5. 硬件异常产生信号
一,关于信号
1. 什么是信号
在我们的生活中存在许多的信号:红绿灯,闹钟等都是信号,这些信号的本质都是告诉我们对应的信号发送后,我们要有相应的动作应对。
Linux信号的本质也是如此:是一种通知机制,用户或者OS发送给进程信号,通知进程某件事已经发生,你要进行后续的处理。
2. 信号和进程
就像红绿灯一样,我们要能够处理信号,必须具备能识别信号的能力。那么进程也是一样的,在处理信号前要能够识别相应信号是什么意思。
进程怎么能够识别信号呢?------通过程序员。
信号的产生是随机的,进程可能在忙 自己的事情,暂时没办法处理信号,那么就会在后续才进行处理,并不是要立刻进行处理。并且进程会临时记录下信号,方便后续的处理。
信号的产生一般而言对于进程是异步的。
3. 信号是怎么产生的
我们使用xshell时,经常会利用键盘组合键 ctrl+c进行终止程序的操作。
这本质就是向相应的进程发送信号。或者我们可以通过命令直接向对应的进程发送信号也是可以的。
如图就是我们Linux中常用的信号
其中1-31时我们的普通信号 34-64是我们的实时信号。
对于信号的处理我们有三种处理方式:
默认(进程自带的,已经写好的逻辑 )
忽略
自定义动作(捕捉信号)
组合键是如何变为信号的呢?
键盘的工作原理是:中断。
组合键在程序中已经有相应的解释。因此OS能够对其做出解释,接着OS会查找进程列表,找到前台运行的进程,最后将对应的信号写入该进程的位图结构中,等待该进程去处理。
4. 管理信号
信号是作用于进程的,OS又作为进程的管理者,因此信号最终都是要通过进程发送给进程的。而信号又不止一个,是不是要对信号进行管理?怎么管理?先描述,后组织。因此在每个进程的PCB中都必须要有保存信号的相关数据结构。Linux中有62种信号,我们在使用信号时,在乎的是信号有无。因此我们可以用位图来保存信号。
因此我们也就知道了发送信号的本质是:在OS直接将目标进程PCB中的信号位图中指定位置置为1 。
二,深剖信号的产生
1. 键盘组合建产生信号
我们前面说过ctrl +c 可以终止一个进程,并且其发送的是二号信号,那么该如何证明呢?
我们上面说过,信号 处理有三种方式,该函数可以修改我们信号对应的默认处理方式。
第一个参数:是int类型,刚刚查看的信号我们除了可以看见信号名称外还能看到编号,在系统中,信号编号就是整数,每一个信号都是被#define定义出来的,我们既可以使用信号名又可以使用数字。这里的参数就是信号编号。
第二个参数:是返回值为void,参数为int的函数指针
signal的返回值是指向之前的信号处理程序的指针。
下面我们来看代码:
#include<iostream>
#include<unistd.h>
#include<signal.h>
void func(int signnum)
{
std::cout<<"我是 "<<getpid()<<"我正在处理信号: "<<signnum<<std::endl;
}
int main()
{
signal(2,func);
while(true)
{
std::cout<<"hellow world"<<std::endl;
sleep(1);
}
return 0;
}
当我们按下组合键ctrl c 时我们发现其对应的确实是2号信号。
ctrl + \ 也可以终止进程
Ctrl + \ 对应3号
Ctrl + z (暂停)对应20号。
下面我们再来谈谈singal这个函数。
当我们调用这个函数时,系统并不会立刻执行func这个回调函数,只有接收到信号的时候才会调用,也就是说singal只修改了对信号的处理方法。
若我们通过./mytest + &使得进程在后台运行
此时我们发现通过组合键就无法发送信号了
此时我们通过命令来直接杀死该进程
键盘产生的信号,只能用来终止前台进程,也就是说只能用来终止那些阻塞你执行命令的程序。
ps: 9号信号不可以被捕捉(自定义)!
2.核心转储
我们通过man 7 signal就可以查看信号的默认行为
我们在前面说过:status我们只研究其低十六位:次低八位表示退出状态;最低七位表示退出信号,中间那一位叫做core-dump我们现在只需知道它是用于调试的。
core dump就是我们所说的核心转储.
一般而言云服务器,其默认核心转储功能是关闭的。
core dump标志位代表了是否发生了核心转储
我们来看下面这段代码:
int main()
{
pid_t pid=fork();
while(pid==0)
{
sleep(1);
std::cout<<"我是子进程:"<<getpid()<<std::endl;
int a=10;
a/=0;
}
int status=0;
waitpid(-1,&status,0);
std::cout<<"退出信号: "<<(status & 0x7f)<<"是否发生了核心转储:"<<((status>7)&1)<<std::endl;
}
我们可以看到其标志位显示其发生了核心转储,并且生成了core文件。那么这个文件有什么用呢?
我们可以用waitpid()中的status的次低7位获取到进程退出的信号,知道信号我们就知道了崩溃的原因。除此之外,我们还想知道进程是在哪一行崩溃的。因此通过核心转储生成的该文件中就会有记录。
我们可以使用ulimit -a 进行查看core dump。
通过ulimit -c 来进行设置。
在linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常情况)。
当一个进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因。
如果必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘当中,方便我们后期调试。
看下面这段代码:
int main()
{
int a=0;
a/=0;
return 0;
}
我们的云服务器中的core dump默认是关掉的
打开后其也确实生成了core文件。
我们加-g选项后生成可执行程序。gbd ./test 进行调式,再输入core-file core.pid 就可以看到奔溃原因和第几行奔溃的了。
我们先让程序出异常,然后在用gdb调试,直接用core-file 命令得到了错误的原因和错误的行数。这种方案我们称为事后调试。
3. 系统调用接口产生信号
我们可以使用系统调用接口kill来发送命令
参数pid: 进程pid
sig: 发送几号信号
返回值:成功返回0,失败返回-1
下面我们通过系统调用接口来模拟实现一个kill命令。
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
using namespace std;
int main(int argv,char* argc[])
{
if(argv!=3)
{
cout<<"argv!=3 "<<"proc "<<"sign "<<"who"<<endl;
}
int sign=atoi(argc[1]);
int who=atoi(argc[2]);
kill(who,sign);
cout<<sign<<" "<<who<<endl;
return 0;
}
我们用自己模拟实现的kill去杀掉sleep进程。
除了上述这样的系统调用,我们还有raise , abort等这样的系统调用接口。
raise:自己给自己发信号
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
using namespace std;
int main()
{
int count=5;
while(count)
{
cout<<"等待:"<<count--<<endl;
sleep(1);
}
cout<<"发送信号"<<endl;
raise(8);
}
abort:给自己发送六号信号,若未捕获,则终止当前进程
int main()
{
int count=5;
while(count)
{
cout<<"等待:"<<count--<<endl;
sleep(1);
}
cout<<"发送信号"<<endl;
abort();
}
如何理解系统调用接口发送信号:用户调用系统接口—》执行OS对应的系统调用代码—》OS提取参数,或者设置特定值—》OS向对应进程写信号—》修改对应进程的信号标记为—》等待进程后续处理----》处理成功。
4. 由软件条件产生信号
我们在前面提到过匿名管道,当读写的任意一端关闭后,都会造成对立的进程退出,下面我们进行测试。
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<cstring>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
int pipefd[2];
int n=pipe(pipefd);
assert(n!=-1);
pid_t pid=fork();
if(pid==0)
{
close(pipefd[0]);
const char* buffer="hellow world";
while(true)
{
int s=write(pipefd[1],buffer,strlen(buffer));
assert(s!=0);
sleep(1);
}
exit(1);
}
char buffer[1024];
memset(buffer,'\0',1024);
close(pipefd[1]);
//close(pipefd[1]);
while(true)
{
int n=read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
printf("%s\n",buffer);
}
else
{
cout<<"退出"<<endl;
break;
}
sleep(1);
close(pipefd[0]);
}
int status;
waitpid(-1,&status,0);
close(pipefd[0]);
cout<<"退出信号"<<(status&(0x7f))<<" "<<"是否核心转储"<<((status>>7)&1)<<endl;
return 0;
}
我们发现当关闭父进程中的读端时,OS会向子进程发送13号信号(SIGPIPE),终止子进程。
设置一个计时器,会延迟的向我们发送一个信号。比如second设置成10,就代表着过10秒后给我们发送一个sigalrm信号也就是14号信号。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 ” 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
void handler(int signum)
{
cout<<"闹钟响了"<<endl;
}
int main()
{
signal(14,handler);
alarm(5);
while(true)
{
cout<<"I am process"<<endl;
sleep(1);
}
return 0;
}
若我们取消进程对alarm信号的自定义处理方式,那么其默认处理方式为:
让进程退出。
我们还可以在对信号的自定义处理方式中再定闹钟进行循环。
void handler(int signum)
{
cout<<"闹钟响了"<<endl;
alarm(5);
}
int main()
{
signal(14,handler);
alarm(5);
while(true)
{
cout<<"I am process"<<endl;
sleep(1);
}
return 0;
}
我们知道每个进程都可能通过alarm接口设置闹钟,所以可能会存在很多闹钟,那么操作系统一定要管理起来它们。
先用一个结构体描述每个闹钟,其中包含各种属性:闹钟还有多久结束(时间戳)、闹钟是一次性的还是周期性的、闹钟跟哪个进程相关、链接下一个闹钟的指针…… 然后我们可以用数据结构把这些数据连接起来。
接下来操作系统会周期性的检查这些闹钟,当前时间戳和结构体中的时间戳进行比较,如果超过了,说明超时了,操作系统就会发送SIGALRM给该进程。
为了方便检查是否超时,可以利用堆结构来管理。
如何理解由软件条件发信号
OS先检测到某种软件条件出发或者不满足,然后构建相关信号发送给对应的进程。
5. 硬件异常产生信号
如何理解除0错误
根据冯诺依曼原理,我们现代计算机在计算时都得要通过cpu来进行运算。
cpu内部有寄存器,寄存器中也有位图,有对应的状态标记位。OS会在计算完成之后对位图进行检测。若发生除0错误,对应的标记位会被置为1,OS进行检测发现后便会找到当前运行的进程,发送相应的信号。
出现硬件异常,进程不一定会退出,一般默认时退出,但我们可以自定义处理行为,但不退出,我们也做不了什么。
我们来看下面这个例子。
oid handler(int signum)
{
cout<<"除0错误"<<endl;
sleep(1);
//alarm(5);
}
int main()
{
signal(8,handler);
int a=1;
a/=0;
return 0;
}
我们发现若是,处理处理错误0信号时,我们自定义处理不退出,便会一直进行处理。
这是因为寄存器中的对应的标记位仍然是1,显示异常。OS会不断的去检测,所以才会一直在处理。
如何理解野指针或指针越界问题?
出现这样的错误,其本质都是访问量非法的地址。
所以何时报错,就是何时能够检查出他们访问的地址是非法的。
首先我们在语言层面上的地址都是虚拟地址,其在通过地址找目标位置的时候,虚拟地址会经过页表+MMU映射到物理内存上,而MMU(硬件)就能够判断其地址是否合法,在遇到非法地址,MMU在转化时,便会报错。
OS检测到后便会发送信号给对应的进程,该进程进行后续的处理。
所有信号都有来源,都是通过OS识别,解释,并发送的。