Linux 进程信号的产生
序言:
信号的生命活动被分为了信号的产生,信号的保存,信号的处理。
这篇博客主要介绍的是信号的产生。
一、什么叫做信号?
在我们日常的生活中信号是十分常见的比如 上课铃声,电话铃声等。当上课铃声响起的时候我们知道要上课了,我们就要回到教室,换一句话来说:信号是某件事件发生的通知, 在计算机中:信号是一种给进程发送的,用来进行事件异步处理的通知机制。
这里补充一下同步和异步的概念:
举个栗子:一天你在寝室和室友打着游戏,突然你室友说要下楼去买饭,你说帮你也带一份。
这时你如果等你的室友回来了以后再开一把游戏那么这种叫做同步。
那么如果这时你不等你的室友回来就自己开了一把游戏那么这种就叫做异步。
同步:两件事情分前后顺序进行,一件执行完了以后下一件再去执行,如果没有执行完就一直等待。
异步:两件事情同时进行没有先后顺序,这种就叫做异步。
如何去查看信号的类型呢?
命令 kill -l
上面这副图我们看见了信号编号(比如1,2等)和信号名称(SIGINT,SIGHUP等),其实信号就是这些整数数字,而这些信号名称是 宏 。
在Linux中一共有62种信号(1~64,没有32,33号信号),我们又把信号分为了两种一种是普通信号(1~31号信号)另一种是实时信号(34~64号信号),普通信号被用在了分时操作系统中进程收到可以不立即处理可以选择在合适的时候处理,实时信号被用在了实时操作系统进程收到之后要立即处理。
什么是分时操作系统?什么又是实时操作系统?
分时操作系统:将 CPU 时间切分成多个 “时间片”,轮流分配给多个用户 / 任务,让每个使用者都感觉自己在 “独占” 电脑,核心是公平共享(如 Windows、Linux 桌面版)。
实时操作系统(RTOS):对任务响应时间有严格要求,必须在规定时间内完成操作,否则可能导致严重后果,核心是确定性和及时性(如汽车控制系统、医疗设备)。
二、信号的处理
我们先举一个栗子:
当我们听到闹钟铃声响起的时候,我们可以起床这是我们的默认动作,我们也可以去忽略这个闹钟继续睡,也可以听到闹钟开始去把闹钟拿到一个听不到的地方这是自定义动作。
1、信号捕捉类型
进程处理信号的动作也分为这几种 默认动作 ,自定义动作 ,忽略动作。这三种统称为捕捉,自定义动作被称为自定义捕捉。
大部分的默认动作是终止进程,那么如何去查看?
man -7 signal:
core/term:都是使进程进程终止(core和term的区别我们在后面说)
ign:忽略信号
stop:将进程暂停
cont:将暂停的进程恢复
我们从上面的图可以看见我们大部分的信号的处理动作都是终止进程。
2、自定义捕捉系统调用
下面我们先来看一个系统调用:
signal:用来对于信号的自定义捕捉。
sighandler_t:是对返回值为void*参数为int类型的函数指针的 typedef 出来的类型,将来我们想要捕捉的信号的编号将会被传进去。
handler:是我们传进去的自定义方法。
signum:我们想要对哪个信号进行自定义捕捉。
#include <signal.h>
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{std::cout <<"收到了" << sig << "号信号" << std::endl;
}
int main()
{signal(SIGINT , handler);while(true){std::cout << "我是一个进程 pid:" << getpid() << std::endl;sleep(1);}return 0;
}
那么有的同学就在想如果我们把全部的信号种类全部自定义捕捉,那么不就创建出一个杀不死的进程了?
Linux的程序员也想到了这个,所以一些信号是无法被自定义捕捉的比如 9号和19号信号。
三、信号的产生
1、键盘产生
在以前的博客中我们说过ctrl + c可以终止进程,其实这是因为ctrl + c向目标进程发送了信号,执行信号的默认处理动作终止了进程。
下面我们来验证一下现象:
1、ctrl + c之后我看见了进程收到了哪个信号
#include <signal.h>
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{std::cout <<"收到了" << sig << "号信号" << std::endl;
}
int main()
{signal(SIGINT , handler);while(true){std::cout << "我是一个进程 pid:" << getpid() << std::endl;sleep(2);}return 0;
}
我们看见了进程确实收到了2号信号,那2号信号是干什么的?
从键盘上获取终端信号。
可以有的同学问操作系统是怎么知道信号给哪个进程的,换一句话来说我们上面说的ctrl + c 会给目标进程发信号目标进程是谁?
下面我们在看一个现象:
看上面这幅图为什么ctrl + c 进程没有收到对应的2号信号?
这是因为./xxx &会把进程在后台运行,换一句话来说进程是后台进程。
1.1 前台进程和后台进程:
./xxx 进程在前台运行
./xxx &进程在后台运行
前台进程:可以从标准输入流(键盘)里读取数据,也可以向标准输出流里写入数据。
后台进程:不可以从标准输入流(键盘)里读取数据,但和前台进程一样可以下向标准输出流里写入数据。
前台进程只能有一个而后台进程可以有很多个。
为什么只有前台进程可以从标准输入流里获得数据?
因为标准输入流只有一个,我们从标准输入流里传入数据进程需要对数据进行处理,如果标准输入流里的数据给很多进程那么数据的处理是混乱的,所以前台进程是唯一的。
换一句话来说:前台进程的本质就是从标准输入流中拿取数据进行处理加工。
如果进程的父进程是shell,那么shell进程会到后台运行,如果我们在进程内部创建子进程,那么父进程在前台运行,子进程在后台运行,如果父进程收到信号,父子进程都会被杀掉不会导致僵尸进程。
1.2 前后台进程切换的系统指令
jobs:查看所以的后台进程。
fg(foremost ground)+ 任务号:将后台进程提到前台运行。
ctrl + z:将变成后台进程并且前台进程暂停。
为什么前台进程暂停了就变成了后台进程?
因为前台进程如果被暂停了,那就说明我们输入任何数据都无效了系统相当于卡死了,所以前台进程不能被暂停,如果暂停必须变成后台进程。
bg(background)+ 任务号:将后台暂停的进程重新运行起来。
1.3 如何给进程发信号?
这个问题在这篇博客中只能先初步理解,我们将在下一篇博客在说信号的保存的时候会详细介绍。
我们在上面介绍普通信号的时候说过普通信号可以不被立即处理而是可以选择在合适的时候处理,那么进程就要去记录这个信号防止当要处理的时候忘记处理,那么如何去保存信号呢?在task_struct结构体里有一个位图,位图的比特位的位置代表的是信号编号,比特位的数字代表的是是否收到信号,换一句话来说向进程发信号就是更改进程的PCB里的位图。
那么我们再来想一个问题:PCB是不是内核中的数据结构,对于内核数据结构来说能修改它的只能是操作系统,那么我们可不可以理解成发送信号必须要经过操作系统,所以我们想要对一个进程发送信号必须由操作系统之手才可以实现。
1.4 信号 VS 通信
下面我们将从两个角度(狭义和广义)来理解这个问题:
狭义:
信号是我们通过操作系统向进程发送信号而通信是进程和进程之间,从这个角度来说信号和通信是不一样的。
广义:
信号是通知某种时间发生的异步通知机制,那么通信也是传输数据和信息的,从这个角度来说信号和通信又是相同的。
2、系统命令产生
2.1 kill -signum pid
kill -signum pid:可以向任何进程发送任何信号。
3、系统函数产生
3.1 kill
kill:可以向任何进程发送任何信号。
pid:想要向哪个进程发送的pid。
sig:想要发送的信号编号。
我们上面说的系统命令kill底层也是kill实现的,下面我们来写一个我们自己的kill。
#include <signal.h>
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
#include <string>int main(int argc , char* argv[])
{if(argc != 3){std::cout << "./mykill -signum -pid" << std::endl;return 1;}int signum = std::stoi(argv[1]+1);int pid = std::stoi(argv[2]);int ret = kill(pid , signum);return ret;
}
3.2 raise
raise:向自己发送任何信号。
sig:想要发送的信号编号。
#include <signal.h>
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{std::cout <<"收到了" << sig << "号信号" << std::endl;
}
int main()
{signal(SIGINT , handler);int cnt = 0;while(true){std::cout << "我是一个进程 pid:" << getpid() << std::endl;sleep(2);if(cnt == 5){raise(SIGINT);}cnt++;}return 0;
}
3.3 abort
abort:向自己发送SIGABRT信号。
这个函数会向进程发送6号信号,而且无法自定义捕捉强制执行默认捕捉。
4、软件条件产生
在我们之前的 Linux进程间通信之命名管道 中我们在说管道通信的特点的时候,说过当读端关闭写端也会关闭,这其实就是一个由于软件条件引起从而发送了SIGPIPE信号。
4.1 alarm闹钟
alarm 是 Unix/Linux 系统下的进程级定时器函数,核心功能是让内核在指定秒数后,向当前进程发送一个 SIGALRM 信号,终止进程。
second:是想要设定在多少秒后闹钟响起。
返回值:第一次调用会返回0,如果后面再次调用会返回会上次调用剩余的秒数。
4.2 体验IO的效率
大量IO的情况:
#include <unistd.h>
#include <iostream>
int main()
{int cnt = 0;alarm (1);while(true){std::cout <<"cnt = " << cnt << std::endl;cnt++;}return 0;
}
少量IO的情况:
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <signal.h>int cnt = 0;
void handler(int sig)
{std::cout <<"cnt = " << cnt << std::endl;
}int main()
{signal(SIGALRM , handler);alarm (1);while(true){cnt++;}return 0;
}
我们可以看见有大量IO的情况cnt比少量IO的情况cnt小很多,它们两的差距是指数级的,说明IO对效率的影响是很大的。
4.4 alarm是基于什么实现的?
alarm 函数的实现依赖于 Unix/Linux 内核的定时器机制和信号系统,在现代Linux中定时器是被管理的,如何管理呢?老办法:先描述 ,再组织。
下面我们来看一下描述定时器的数据结构:
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
}
下面我们将详细说明上面结构体两个变量(expires和function):
expires:它用来实现定时功能,比如现在的时间戳是1000,我现在alarm(5),那么操作系统会创建一个timer_list内核数据结构expires就为1005,当时间戳 > 1005时将会调用function。
function:它是一个函数指针(返回值为void ,参数为unsigned int),当定时器响了以后会调用它,由它去实现对事件的处理(比如向进程发送SIGALRM信号)。
4.5 什么是软件条件?
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进⾏相应的处理。简⽽⾔之,软件条件是因操作系统内部或外部软件操作⽽触发的信号产⽣。
5、硬件异常产生
5.1 什么是硬件异常产生信号?
硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。
5.2 除0错误
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <signal.h>void handler(int sig)
{std::cout << "收到了" << sig << "号信号" << std::endl;exit(1);
}int main()
{signal(SIGFPE , handler);int a=10;a /= 0;return 0;
}
再CPU的众多寄存器中有一个EFLAGS寄存器,如果计算出错会记录在这个寄存器,操作系统会发现(因为操作系统是软硬件资源的管理者)然后向进程发信号。
这个问题更加本质的解释将在下一个博客中详细介绍,现在可以先这样理解。
四、Core和Trem的区别
6.1 区别
Core Dump:当进程因为异常终止的时候,可以选择把进程的用户空间内存和数据保存到磁盘上,形成一个文件叫core,这个就是Core Dump。
Term:就是直接杀死进程不会保留任何数据和内存到磁盘上。
Core和Term都会杀死进程。因为我用的是云服务器,一般云服务器的Core Dump功能是关闭的。
当我们调试代码的时候可以使用core文件。
6.2 如何打开Core Dump功能
ulimit -a:可以查看Core Dump是否被打开
可以看见我的core file的大小是0个块大小是关闭的。
ulimit -c 大小:打开Core Dump功能,并设置最大的core file大小。
ulimit -c 0:可以关闭。
总结:
本篇博客详细介绍了信号的产生,有一些问题无法在当先解释清楚我将在下面的博客在解释清楚。
在本博客中我们说了很多衍生知识比如前台和后台进程,core和term区别,alarm的底层等
=========================================================================
本篇关于命名管道的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持和修正!!!