一.什么是信号
生活中,有哪些信号相关的场景呢,比如:红绿灯,闹钟,转向灯等等
1.这里我们要知道,你为什么认识这些信号呢,记住了对应场景下的信号+后续是有”动作“要你执行的
2.我们在我们的大脑中,能够识别这个信号的
3.如果特定信号没有产生,但是我们依旧知道应该如何处理这个信号
4.我在收到这个信号的时候,可能不会立即处理这个信号
5.信号本身,在我们无法立即被处理的时候,也一定要先被临时的记住
结论:什么是Linux信号,本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理。
二.信号如何使用,结合进程,信号结论
1.进程要处理信号,必须具备信号“识别”的能力(看到+处理动作)
2.凭什么进程能够“识别”信号呢,代码是程序员编写的,就比如说你是如何认识红绿灯的,这都是有人告诉你的
3.信号产生是随机的,进程可能正在忙自己的事情,所以,信号的后续处理,可能不是立即处理的
4.进程会临时的记录下对应的信号,方便后续进行处理
5. 在什么时候处理呢,合适的时候(这个后面会说)
6.一般而言,信号的产生相对于进程而言是异步的(什么是异步呢,异步双方不需要共同的时间,也就是接收方不知道发送方什么时候发送,所以在发送的信息中就要有提示接收方开始接收的信息,如开始时有开始位,同时在结束时有停止位。)
三.信号常见的处理方式
1.默认(进程自带的,程序员写好的逻辑)
2.忽略(信号的一种处理方式)
3.自定义动作(捕捉信号)
四.常见信号
1-31普通信号,34-64实时信号。
如何理解组合键变信号:键盘的工作方式是通过:中断方式进行的,当然也能够识别组合键,ctrl+c,OS解释组合键->查找进程列表->前台运行的进程->OS写入对应的信号到进程内部的位图结构中。
如何理解信号被进程保存:进程必须具有保存信号的相关数据结构(位图,unisgned int)PCB内部保存了信号位图字段。
如何理解信号发送的本质:信号位图是在task_struct -> task_struct内核数据结构->OS。
信号发送的本质:OS向目标进程写信号,OS直接修改pcb中的指定的位图结构完成“发送”信号的过程。
五.信号的产生以及核心转储
键盘产生信号
sighandler_t handler回调函数,通过回调的方式,修改对应信号的捕捉方法,signum,要捕捉信号的名称或编号。
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
void cating(int signum)
{
cout<<"捕捉到信号:"<<signum<<endl;
return;
}
int main()
{
signal(2,cating);
while(1)
sleep(1);
return 0;
}
signal(SIGINT,catchsig),特定信号的处理动作,一般只有一个,signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作,如果后续没有任何SIGINT信号产生,catchsig永远也不会被调用。
核心转储
man 7 signal 查看信号的默认处理行为。这里不同信号的Action不同,有Term、Core、Ign、Cont、Stop等状态行为。
接下来就是了解一下Core动作——核心转储(一般而言云服务器的核心转储功能是被关闭的)。关于进程等待中,status 中如果是正常终止就保存返回值、错误码。
如果被信号所杀,第7位上保存的这个就叫做core dump,如果是0表示没有发生核心转储,为1则是发生了核心转储。我们可以打印code_dump位的信息 (左移7位然后与上1即可)。
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
void cating(int signum)
{
cout<<"捕捉到信号:"<<signum<<endl;
return;
}
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(1);
int a = 100;
a /= 0;
exit(0);
}
int status = 0;
waitpid(id, &status, 0);
cout << "父进程:" << getpid() << "子进程:" << getppid() << endl;
//退出信号:
cout << "exit sig" << (status & 0x7f) << endl;
// 打印core dump位
cout << "core dump" << (status > 7 & 1) << endl;
}
如果核心转储是被关闭的,可以使用ulimit -a查看,ulimit -c10240打开,这里就可以使用core.11077就可以定位错误。
系统调用发送信号
第一个参数为指定的进程pid,第二个参数为对应的信号编码。
kill 是给指定进程发送信号,而如果想让自己给自己发信号,可以使用 raise 命令
给自己发送abort信号,也就是6号信号。相当于代码:raise(6) 或 kill(getpid(),6)
软件条件产生信号
这里可以举一个例子:当管道,读端不进行读取,还关闭了文件描述符,而写端一直写入,会发生什么问题?操作系统会自动终止对应写端进程,通过发送信号的方式,发送SIGPIPE信号。
验证:
1.创建匿名管道
2.让父进程进行读取,子进程进行写入
3.让父进程关闭读端 && waitpid(),子进程一直进行写入
4.子进程退出,父进程waitpid拿到子进程的退出status。
5.提取退出信号。
SIGPIPE便是一种软件条件产生的信号,除了管道中会发出SIGPIPE信号,接下来我们学习其它软件产生的信号,alarm 函数与SIGALRM 信号,系统调用中的 alarm 函数会产生 SIGALRM 信号。接下来让我们了解一下 alarm 接口。
调用 alarm 函数可以设定一个闹钟,也就是告诉内核再 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程。
利用上面这个函数,我们可以做一个定时器。
int count = 0;
void catchSig(int signum)
{
cout << "count: " << count << endl;
}
int main()
{
// 1秒后发送消息
alarm(1);
signal(SIGALRM, catchSig);
while (1)
{
++count;
}
return 0;
}
如何理解软件条件给进程发送信号:OS先识别到某种软件条件触发或不满足。OS构建信号,发送给指定的进程。
硬件异常产生信号
首先我们要知道硬件是如何产生信号的,我们先写一段整数除以0的代码看一下。
void handler(int signum)
{
sleep(1);
cout << "signal is : " << signum << endl;
}
int main()
{
signal(SIGFPE, handler);
int a;
a/=0;
while (1)
sleep(1);
return 0;
}
这段代码会不断的产生信号8,但是我们把信号8捕捉了,他就会不停的发送。
一.那如何理解整数除以0这个操作
1.因为计算的是CPU,如果CPU计算出现错误,会将错误信息放入到状态寄存器中,状态寄存器中有对应的状态标记位(类比成 位图),其中会存在溢出标记位,OS会自动进行计算完毕之后的检查。
2.如果OS识别到有溢出问题,根据 current指针(指向当前正在运行的进程) 找到进程,然后提取出 PID,O S再进行信号发送到该进程,进程则会再合适的时候,进行信号的处理。
3.立即找到当前 task_struct中有一个current指针,当程序进行执行时,current内的内容也会被加载到CPU的寄存器中。
4.所以,整数除以零是一个硬件异常的问题。二·.那一旦出现硬件异常,进程一定会退出吗?
不一定,一般默认是退出,但是如果我们不进行退出,我们也不能进行任何操作,因为无权访问CPU中的寄存器数据。
三.为什么会发生死循环?
因为寄存器中的异常一直没有被解决,所以一般我们出现除0等错误,一般就直接exit()退出了。
指针越界、野指针一般被称为段错误 (11号信号SIGSEGV)
那如何理解野指针或越界问题?
1.都必须通过地址,找到目标位置,
2.语言上的地址,全部都是虚拟地址
3.将虚拟地址转化为物理地址
4.页表+MMU(Memmory Manager Unit——硬件)
5.野指针,越界->非法地址->MMU转化的时候,一定会报错。因为MMU这个硬件其中也有寄存器,注意,外设也有寄存器的,不只是CPU有寄存器。
结论:硬件也能产生信号。所有的信号,都有其来源,但最终全部都是被OS被识别、解释、发送的。
信号的常见问题
为什么所有的信号产生,最终都要由OS来执行?
因为OS是进程的管理者。
信号的处理是否是立即处理的?
由OS在合适的时机进行处理。
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里?
需要被记录下来,记录在进程PCB中对应的信号记录位图。
如何理解OS向进程发送信号?
本质是OS直接修改PCB中的信号位图,根据信号编号修改特定的比特位(由0置1)。