目录
一、概念
二、前台进程与后台进程
1.ctrl+c
2.ctrl+z
三、信号的产生方式
1.键盘输入产生信号
2.系统调用发送信号
2.1 kill()函数
2.2 raise()函数
2.3 abort()函数
3.异常导致信号产生
3.1 除0异常
3.2 段错误异常
4.软件条件产生信号
4.1 管道
4.2 闹钟
四、核心转储
总结
一、概念
生活中,我们也经常遇到信号,比如早八的闹钟、红绿灯、鲁大师在电脑里面抽烟、周杰伦的到底有谁知道~是几点的方向~你~才~会~收~到~暗~号~。在信号还没有产生的时候,我们已经知道什么样的信号需要如何去处理,比如早八的闹钟,在闹钟响之前,我们就知道早上要么起床去上课,要么把闹钟一关继续睡觉。
信号产生了,我们不一定要立即处理他,而是在合适的时候处理。比如你肚子疼,需要拉肚子,但是游戏已经开了,对抗路已经在操作了,今天不管,你有本事就拉裤兜,必须先把这把打完再去。因此我们要对信号进行暂时保存。
同时,我们并不清楚具体什么时候肚子痛,因此信号到来相对于我现在正在打的游戏,是异步(多执行流同时进行操作)的。
小总结:
我们能识别信号,同时知道该如何处理
信号是异步的
可以对信号进行暂时保存
在合适的时候处理信号
信号是向目标进程发送消息通知的一种机制。
二、前台进程与后台进程
1.ctrl+c
进程在运行时,一般都是前台进程,如下代码,./mytest运行起来,我们输入命令没用反应,但我们运行时输入ctrl+c可以终止掉进程。
如果我们执行的时候在后面加上 & ,就让进程在后台运行,此时输入命令可以反应,但是进程不能被ctrl+c终止。
这是因为前台进程只能有一个,后台进程可以有多个,由于shell也是一个进程,因此当进程在前台运行时,shell需要退入到后台进程中去,因此无法接受指令。而你现在输入ctrl+c终止进程,就是让前台进程终止,自然进程就结束了。
而当你进程以后台方式启动时,前台进程是shell,因此输入指令可以有结果,如果你输入ctrl+c,shell自己会保护自己,无法被ctrl+c终止,后台进程并没有收到ctrl+c,因此无法终止后台进程。
之前我们提到,后台进程可以有多个,比如现在我们重定向到log1.txt与log2.txt,输入指令jobs可以看到后台进程的运行情况。
他们在后台运行,并不影响前台shell接受指令,同时ctrl+c也无法终止。
我们输入fg + number,就可以将后台进程提到前台来,输入ctrl+c就可以终止掉该进程了。
小总结:判断是前台进程还是后台进程,看shell有没有能力接受用户输入,有就是后台进程,没有就是前台进程。
2.ctrl+z
输入ctrl+z可以暂停前台进程,但是被暂停的进程会自动放到后台,也就是我输入指令命令行能接受了。
现在我们输入 bg+number ,就可以将暂停的后台进程继续运行。
小总结:
- ./mytest & 让进程在后台运行
- jobs 查看后台进程
- fg + number 将后台进程放到前台
- ctrl+z 暂停前台进程并放到后台
- bg + number 将暂停的后台进程继续执行
三、信号的产生方式
1.键盘输入产生信号
在上面,我们学习ctrl+c终止前台进程,其实ctrl+c的本质上就是信号,他是信号表中的2号信号。
我们可以通过写代码的方式来查看该现象。
signal函数:能将收到的信号做自定义处理
- 参数1:信号num
- 参数2:返回值为void,参数为int的函数指针
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void sighandler(int signo)
{
cout<<"收到了"<<signo<<"号信号"<<endl;
exit(1);
}
int main()
{
signal(2,sighandler);
while(1)
{
cout<<"pid: "<<getpid()<<endl;
sleep(1);
}
}
运行后发现,输入ctrl+c,会打印出内容,同时发送2号指令也会打印出相同的内容,由此可见,ctrl+c 等价于发送2号信号 。也就是键盘输入可以产生信号。
- 我们知其然,还要知其所以然。
- 我们在键盘中输入ctrl+c,会被操作系统接受到你的输入,因为操作系统是进程的管理者,他判断出你输入的内容是终止信号,同时是输入给当前运行的前台进程的。那么操作系统肯定需要告诉进程,信号到来了,你赶紧处理。
- 在进程的角度来看,进程一定要通过存储来表示自己是否收到信号,收到了哪种信号。这完全可以通过位图来实现,比特位的位置代表收到的哪种信号,比特为的内容0/1代表是否收到信号。由于普通信号只有1-31个,因此用32位的整形变量就可以判断了。
- 我们之前提到,在信号没有到来时,进程就已经知道该如何处理相关信号,因此进程还得有一张自己的函数指针数组,数组的下标与信号的编号相关,于是,当发现信号位图中有信号的到来,知道是哪一张信号,然后会调用信号相关的函数进行执行。
- 操作系统向目标进程发送信号,其实本质上是写信号,将该进程信号位图的相关位置设置为1
那么既然ctrl+c是一个信号,之前我们学过的ctrl+z,也应该是信号咯。当然没错,ctrl+z是20号信号SIGTSTP。还有ctrl+\,是3号信号
跟之前一样,使用signal捕获一下2号信号,3号信号,和20号信号。
9号信号不可被自定义捕捉,因为操作系统得有能力杀死任意进程,如果某个进程是恶意进程,一直在读取你的数据,或者对操作系统做破坏,那么该进程捕捉了所有信号,岂不是杀不掉了,因此操作系统设置9号信号不可被捕捉。
2.系统调用发送信号
2.1 kill()函数
有一个kill的系统调用函数,他可以往指定的进程发送指定信号。我们可以通过该函数来发送信号。
kill:给指定进程发送信号
- 参数1:进程pid
- 参数2:信号编号
代码如下
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<string>
using namespace std;
void Usage(const string& proc)
{
cout<<"\n Usage: "<< proc <<" -signumber process"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
int signumber = stoi(argv[1]+1);
int processpid = stoi(argv[2]);
kill(processpid,signumber);
}
我们就可以利用kill函数,给他传信号与进程pid,即可将信号发送给此进程。
2.2 raise()函数
kill是给任意进程发送任意信号,而raise是给自己发送任意信号
kill:给指定进程发送信号
- 参数1:信号编号
使用很简单,如下,raise(2)代表给自己发送2号信号
因为我们自定义捕捉了,因此会一直收到2号信号。
2.3 abort()函数
给自己发送abort信号,是6号信号,参数返回值都不管,直接调用即可。
我们发现,进程确实收到了6号信号,我们sighandler里面并没有让进程退出,但是进程却被Aborted终止,这是abort函数的特点,就是进程收到6号信号,你可以自定义捕捉,但捕捉完毕我仍然会终止。
3.异常导致信号产生
C++我们学过异常,发生异常了,代码并不会再往后面执行,而是直接终止,除非你捕获了异常。
3.1 除0异常
如下代码,你发生了除0错误,编译的时候就会警告,当你执行的时候,告诉你浮点数异常。这是8号信号 SIGFPE。
我们将信号捕捉,运行,我们并没有写循环,按道理只会打印一次,这里发现进程在一直打印消息。
这是因为进程在运行的时候,会将进程中的数据放到CPU里面的寄存器去运行,现在CPU运算发现了除0错误,会给相应的标志位写上1,记录你的错误,操作系统就知道进程的错误了(因为操作系统是软硬件资源的管理者),那么操作系统需要发送对应信号干掉目标进程。
但是你自己的操作,将信号自定义捕捉了,杀死不了,而后进程就切换了,当进程又切换回来时,操作系统又发现你对应的标志位为1,因此又给你发送信号,从此往复,会一直发送信号,却又杀不掉你。
3.2 段错误异常
我们写C/C++代码的时候,经常会遇到段错误。比如野指针,越阶访问等等。段错误异常是11号信号SIGSEGV,因此我们捕捉11号信号看看结果。
跟之前一样,没有写循环,依然一直报错。这是在访问虚拟内存页表时,发现页表中没有对应内容虚拟到物理的映射。发生段错误,标志位设置,操作系统发送信号给进程,进程收到信号并自定义捕捉,进程切换,又回到该进程,发现标志位为1,继续发送信号,继续套娃。
4.软件条件产生信号
4.1 管道
我们之前在学习管道的时候,当我们把读端关闭的时候,写端会直接终止,因为操作系统给写端发送SIGPIPE信号,告诉写端,兄弟别舔了,她已经把你拉黑了。此时我们并没有涉及到寄存器或者其他硬件设备,虽说有内存,但是内存空间都是我自己开辟的呀,我自己的东西我爱咋用咋用,操作系统不会无缘无故给你发信号,这就叫软件条件。
4.2 闹钟
alarm:可以设置闹钟->给当前进程发送14号信号
参数:设置几秒中后的闹钟
返回值:正常返回0,闹钟提前返回剩余时间。
在5次打印后收到了消息,返回值为0。
综上,产生信号的方式有很多,但是发送信号只能由操作系统来完成!!!也就是向进程PCB中的信号位图写入。
四、核心转储
首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁 盘上,文件名通常是core,这叫做Core Dump。
命令行输入指令 man 7 signal ,往下翻可以查看信号的详细信息,这里可以看到信号的动作有Term、Core、Ign(ignore)、Cont(continue)、Stop。
其他的都比较好理解,比如Ign忽略,Cont继续,Stop暂停。但是还有Term和Core这两种,我们学过的信号这两种类型都有,他们都是终止进程,有什么区别呢?
我们发现Core中有SIGABRT、SIGFPE、SIGSEGV等等,他们都比较像异常情况。报错的具体代码尚不清楚。需要用户进一步排查
而Term中的SIGINT(键盘中断)、SIGKILL(杀死进程)等等,其实他们都没犯错, 只是被杀掉了。结果很清楚
你的代码如果出现了Core类型的错误,其实会发生core dump(核心转储),也就是在当前目录下多出一个以进程pid命名的 core.pid 文件。我们可以通过查看gbd查看代码具体报错位置信息。但是core dump功能是被默认关闭的。
输入指令ulimit -a可以查看系统的配置项,其中第一项core file size就是core dump,大小为0,我们可以将他大小设置一下
输入指令ulimit -c + size,就可以更改core file size 的大小,这是内存级别的,也就是重启xshell会自动恢复。
现在再更改代码并运行就会发现多了core dumped,发生了核心转储。
我们现在gdb调试mytest,同时在gdb中输入core-file + core.pid ,此时就可以看到该文件是在具体哪个地方出错。
总结
- 信号的产生,最终都要由操作系统来进行执行,这是因为操作系统是进程的管理者。
- 进程在信号到来不会立即处理,而是在合适的时候处理。
- 由于信号不会被立即处理,因此信号需要暂时被进程记录下来,记录在信号位图中。
- 信号在没有收到信号的时候,已经知道自己该如何处理信号。
- 操作系统向进程发送信号,本质是向目标进程的信号位图中写信号(置1)。