Linux知识点 – 进程信号(一)
文章目录
- Linux知识点 -- 进程信号(一)
- 一、理解信号
- 1.理解Linux信号
- 2.信号的产生与处理
- 3.常见的信号
- 4.如何理解组合键变成信号
- 5.如何理解信号被进程保存
- 二、信号的产生
- 1.键盘产生
- 2.核心转储
- 3.系统调用接口产生信号
- 4.由软件条件产生信号
- 5.硬件异常产生信号
一、理解信号
1.理解Linux信号
-
Linux信号:
本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理; -
信号结论:
(1)进程要处理信号,必须要具备识别的能力(看到 + 处理动作);
(2)进程通过程序员编写的代码逻辑,能够识别信号;
(3)信号的产生是随机的,进程可能在忙自己的事情,所以,信号的后续处理,可能不是立即处理的;
(4)进程会临时记录下对应的信号,方便后续进行处理;
(5)进程会再合适的时候处理信号;
(6)一般而言,信号的产生相对于进程而言是异步的;
2.信号的产生与处理
-
信号的产生:
当一个进程正在运行时,我们通过键盘上的ctrl + c可以杀掉进程,这本质就是通过键盘的组合键向目标进程发送2号信号; -
信号处理常见的方式:
(1)默认(进程自带的,程序员写好的逻辑)
(2)忽略
(3)自定义动作(捕捉信号)
3.常见的信号
在Linux下输入kill -l指令,可以查看系统的信号:
其中,前31个是常用的信号;
4.如何理解组合键变成信号
键盘的工作方式是通过中断方式进行的,当然能够识别组合键;
- 操作系统处理信号的流程
OS解释组合键 -> 查找进程列表 -> 前台运行的进程 -> OS写人对应的信号到进程内部的位图结构中
5.如何理解信号被进程保存
信号的数据主要有两类:信号种类以及是否产生;
在进程PCB中的task_struct中有保存信号的相关数据结构 – 位图(unsigned int),每一位都表示不同信号,0和1代表有没有接收该信号;
信号发送的本质:OS向目标进程写信号,OS直接修改目标进程PCB中的指定的位图的结构,完成发送信号的过程;
二、信号的产生
1.键盘产生
我们知道信号可以从键盘产生,当我们产生信号后,信号的处理方式有三种,接下来我们使用自定义捕捉来处理键盘产生的信号;
- signal函数:
参数:
sighandler:函数指针,作为回调函数的参数使用;
signum:信号名称;
handler:处理信号的函数的指针;
这个函数的功能是:我们一旦收到了指定的信号,signal函数会将信号的编号传回给回调函数的参数;
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号,正在处理中:" << signum << "pid: " << getpid() << endl;
}
int main()
{
//signal(2, catchSig);//signal第一个参数可以传信号名,也可以传对应的序号
signal(SIGINT, catchSig);
while(true)
{
cout << "我是一个进程,我正在运行,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
上面的代码在捕捉2号信号,当捕捉到后,会调用自己的处理函数catchSig;
运行结果:
(1)进程不断在运行,当我们按下键盘组合键ctrl + c时,触发2号信号,进程就会执行我们自定义捕捉的处理方式,但是进程并没有结束;
(2)这是因为特定信号的处理方式,一般只有一个,我们将2号信号的处理方式改为自定义的处理方式,系统默认的处理方式就失效了;
(3)signal函数仅仅是修改了进程对特定信号的后续处理动作,不是直接调用对应的处理动作;只有当对应信号触发的时候,signal函数会回调catchSig函数,对信号进行处理;
(4)如果后续没有任何SIGINT信号产生,那么catchSig永远不会被调用;
2.核心转储
在上面的代码增加对3号信号的捕捉(3号信号快捷键是ctrl + \ ,也是让进程退出):
int main()
{
//signal(2, catchSig);//signal第一个参数可以传信号名,也可以传对应的序号
signal(SIGINT, catchSig);
signal(SIGQUIT, catchSig);//3号信号
while(true)
{
cout << "我是一个进程,我正在运行,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
man 7 signal指令打开手册,查看信号的处理动作:
发现2号指令后面的动作是term,而3号的是core,这是进程的核心转储功能;
我们在学习进程等待时,子进程的status中,被信号所杀的子进程的status会有一个core dump位:
这个位就是标志该进程是否发生了核心转储;
-
核心转储:
当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中;
核心转储主要为了调试,这是它的主要应用场景;
一般而言,云服务器(生产环境)的核心转储功能都是关闭的; -
打开核心转储:
ulimit -a:查看,可以看到core file size是0;
ulimit -c 大小:设置core file的大小;
再次查看可以发现,core file size变为了10240;
这仅仅是在自己的终端中打开了核心转储,如果重启云服务器,又会恢复回去;
只要是信号动作是core的,都会触发核心转储,在进程运行时触发核心转储,会报错core dumped,并产生一个core文件,后缀是进程的pid;
触发3号信号终止进程,并产生相应的core文件:
8号信号也是core信号,在进程中触发除0错误就会触发8号信号:
int main()
{
while(true)
{
sleep(1);
int a = 10;
a /= 0;
cout << "我是一个进程,我正在运行,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
core文件是能够用来调试的,用gdb加载core文件:
core文件会告诉我们在哪里发生了错误,触发的是什么信号;
3.系统调用接口产生信号
- kill函数:是向进程发送信号的系统调用接口
功能:向pid进程发送sig信号;
能够发送系统信号,与kill指令的功能一致;
模拟kill指令代码:
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}
// ./system_signal 2 pid
int main(int argc, char* argv[])
{
if(argc != 3)//不满足格式要求
{
Usage(argv[0]);
exit(1);
}
int signumber = atoi(argv[1]);
int procid = atoi(argv[2]);
kill(procid, signumber);
return 0;
}
运行结果:
将sleep 10000这个进程,通过我们自己模拟的kill指令,向进程发送信号,终止该进程;
- raise函数:给自己发送sig信号
int main(int argc, char* argv[])
{
cout << "运行中" << endl;
sleep(1);
raise(8);
return 0;
}
运行结果:
进程向自己发送了8号信号;
-
abort函数:给自己发送和abort信号,就是自己终止自己
-
系统调用接口的流程:
用户调用系统接口 -> 执行OS对应的系统调用代码 -> OS提取参数或者设置特定的数值 -> OS向目标进程写信号 -> 修改对应的信号标记位 -> 进程会处理信号 -> 执行对应的处理动作
4.由软件条件产生信号
- alarm:设定闹钟
代码验证1s内CPU能执行多少次++:
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int count = 0;
void catchSig(int signum)
{
cout << "count: " << count << endl;
}
int main(int argc, char* argv[])
{
//验证1s之内,CPU能进行多少次++
alarm(1);
signal(SIGALRM, catchSig);
while(true)
{
count ++;
}
return 0;
}
运行结果:
这段代码是使用了alarm作为时钟,当这个函数运行之后,就会定时一段时间,当这段时间到了之后,就会向进程发送信号,这就是软件发送信号;
当我们设定一个闹钟之后,一旦触发闹钟,就自动移除了,如果想要定期做一件事情,可以在捕捉完信号后,再设一个闹钟;
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int count = 0;
void catchSig(int signum)
{
cout << "count: " << count << endl;
alarm(1);//捕捉完闹钟信号后,再设一个闹钟
}
int main(int argc, char* argv[])
{
//验证1s之内,CPU能进行多少次++
alarm(1);
signal(SIGALRM, catchSig);
while(true)
{
count ++;
}
return 0;
}
运行结果:
设置一个定时器
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<functional>
#include<vector>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
typedef function<void ()> func;
vector<func> callbacks;
int count = 0;
void showLog()
{
cout << "日志功能" << endl;
}
void logUser()
{
if(fork() == 0)
{
execl("/usr/bin/who", "who", nullptr);
exit(1);
}
wait(nullptr);
}
void showCount()
{
cout << "count : " << count << endl;
}
void catchSig(int signum)
{
for(auto &f : callbacks)
{
f();
}
alarm(1);
}
int main(int argc, char* argv[])
{
signal(SIGALRM, catchSig);
alarm(1);
callbacks.push_back(showCount);
callbacks.push_back(showLog);
callbacks.push_back(logUser);
while(true)
{
count ++;
}
return 0;
}
运行结果:
5.硬件异常产生信号
捕捉8号信号:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号:" << signum << endl;
sleep(1);
}
int main()
{
signal(SIGFPE, catchSig);
int a = 10;
a /= 0;
while (true)
{
sleep(1);
}
return 0;
}
运行结果:
-
系统一直在捕捉8号信号,这是因为8号信号即除0信号是硬件触发的;
(1)进行计算的CPU是硬件;
(2)CPU内部是有寄存器的,状态寄存器(位图)有对应的状态标记位,溢出标记位,O3S会自动进行计算完毕之后的检测,如果溢出标记位是1,OS会识别到有溢出问题,立即找到当前谁在运行,并提取pid,OS完成发送信号的过程,进程会在合适的时候,进行处理;
(3)一旦出现硬件异常,进程不一定会退出;
(4)一直捕捉到8号信号的原因是:寄存器标志位异常一直没有被解决; -
遇到野指针问题,称之为段错误,11号信号就是野指针;
野指针越界是硬件信号的原因:
(1)进程必须通过地址,找到目标位置;
(2)语言上面的地址,全都是虚拟地址;
(3)将虚拟地址转换为物理地址 ,需要页表 + MMU(Memory Manager Unit,硬件);
(4)野指针越界,即访问非法地址,MMU转化的时候,一定会报错;
所有的信号,无论来源,最终都是被OS识别,解释并发送的;
- 问题