Linux——信号概念与信号产生方式

目录

一、概念

二、前台进程与后台进程

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)。

下一章:信号的保存与处理 

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值