【Linux之拿捏信号1】什么是信号以及信号的产生


生活角度的信号

在我们的生活中,什么可以被称为信号呢?

那可太多啦,有红绿灯,闹钟,下课铃,倒计时,狼烟,冲锋号,肚子叫,眼色脸色,手势,外卖电话。。。。。。

联系实际,理解信号

大致从三个角度来讲,如图所示:
在这里插入图片描述

  1. 当我们看到红绿灯(信号)的时候,会有匹配的动作,红灯停,绿灯行。但是我们收到这个信号时,为什么会知道怎么做呢?那是因为曾经有人“教育”过我们,信号还没有产生,我们也知道如果收到信号该怎么做,也就是说我们具有“识别”信号的能力。推导得出——>进程就是我,信号是一个数字,进程在没有收到信号的时候,其实它早就知道一个信号该怎么被处理了!也就是说,程序员设计进程的时候,早就已经设计了对信号的识别能力
  2. 同时因为信号随时都有可能产生,所以在信号产生前,我可能正在做优先级更高的事情,我可能不能立马去处理这个信号!我们需要在后续合适的时间才能处理这个信号。比如大家可能都有这个经历,当你在房间打王者的时候,正推塔呢,突然外卖员给你打电话,说你的外卖到了,但是你要打完这把游戏再去拿外卖,在这个时间窗口内你脑子里面是记得等会游戏结束要去拿外卖,也就是说你保存了“拿外卖”这个信号。同理,信号产生——>时间窗口——>信号处理,进程收到信号的时候,如果没有立马处理这个信号(处在时间窗口),需要进程具有记录信号的能力
  3. 信号的产生对于进程来讲是异步的。(即外卖的到来,对你来讲是异步的,你不能确定外卖小哥具体什么时间点给你打电话)
  4. 当你时间合适时,顺利拿到你的外卖后,就要开始处理外卖了(处理信号),而处理外卖的一般方式有三种:(1)执行默认动作(开始幸福的吃起外卖);(2)忽略外卖(拿到外卖后,先晾在一遍,再打一局王者);(3)执行自定义动作(你突然想高歌一曲,跳支舞)。同理,进程在收到信号之后,处理信号有三种方式:(1)执行默认动作(处理信号);(2)忽略信号;(3)执行自定义操作。

技术应用角度的信号

系统中的信号

认知了上述形象生动的例子之后,我们知道若进程不及时处理信号,就需要记录保存对应产生的信号,那么到底将信号记录保存在哪里呢?

由于系统内部可能有多个进程,同时也有多个信号,对应的进程处理对应的信号,所以我们依然要采取先描述,再组织的方法,管理信号。那么怎么描述一个信号,用什么数据结构管理这个信号呢?

在linux中,我们可以通过命令kill -l可以察看系统定义的信号列表,如图所示。

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
  • 编号34以下的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

在这里插入图片描述

那么其实马上就可以想到之前学过的数据结构——位图。没错,这31个信号就是采用位图结构就记录的,32个比特位,0,1表示信号是否被记录。所以task_struct内部必定要存在一个位图结构,用int表示:uint32_t signals;假设该进程收到了9号kill信号,那么该位图的第九个比特位就会由0置1,9号信号就被记录了,待该进程处理。

综上,所谓的发送信号,本质其实是向进程PCB写入信号,直接修改特定进程的信号位图中的特定比特位,0->1。task_struct 数据内核结构,只能由OS操作系统进行修改,所以无论后面我们有多少种信号产生的方式,最终都必须OS完成最后的发送过程!

信号函数signal

我们通过理解Ctrl+c,来学习一下signal信号函数

  1. 功能
    设置某一信号的对应动作
  2. 函数声明
#include <signal.h>
typedef void (*sighandler_t)(int);//函数指针
sighandler_t signal(int signum, sighandler_t handler);
  1. 参数说明 
    第一个参数signum指明了所要处理的信号类型(信号编号),它可以取除了SIGKILL和SIGSTOP外的任何一种信号。  
    第二个参数handler描述了与信号关联的动作,它可以取以下三种值:
    (1)SIG_IGN  :这个符号表示忽略该信号
    (2)SIG_DFL  :这个符号表示恢复对信号的系统默认处理。不写此处理函数默认也是执行系统默认操作。
    (3)sighandler_t类型的函数指针,当接收到一个类型为sig的信号时,就执行handler 所指定的函数。

Ctrl+c?

  • 用户输入命令,在Shell下启动一个前台进程。用户按Ctrl+c,这个键盘产生一个硬件中断,被OS获取,解释成信号,发送给目标进程,前台进程因为收到信号,进而引起进程退出。
  • 所以Ctrl+c的本质是是向前台进程发送对应的信号。那么如何证明这一点呢?我们来看如下代码:

代码:

  1 #include<iostream>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 void handler(int signo)//自定义方法
  5 {
  6     std::cout<<"get a signal:"<< signo <<std::endl;
  7 }
  8 int main()
  9 {
 10     signal(2,handler);
 11     while(true)
 12     {
 13         std::cout<<"我是一个进程,我正在运行......,我的pid是:"<<getpid()<<std::endl;                                                                                             
 14         sleep(1);
 15     }
 16     return 0;
 17 }

我们在键盘按下 Ctrl+c会终止进程,实际是因为键盘产生硬件中断(一个硬件对应一个中断)后,OS获取并解析成了2号信号,在OS中2号信号对应的动作就是结束进程。而上述代码,我们用signal函数改变了2号信号对应的动作,所以2号信号的动作由结束进程——>打印get a signal:2。所以我们再将程序跑起来,当我们在键盘上桥下Ctrl+c,这个进程就不会被终止,而是打印。。。看图吧
在这里插入图片描述
综上:

  1. 2号信号:进程的默认处理动作是终止进程
  2. signal方法:可以进行对指定的信号设定自定义处理动作

注意:signal(2,handler)调用完这个函数的时候,handler方法并没有被调用,只是更改了2号信号和对应handler方法的映射关系,并没有调动handler函数,例如以下的代码show函数并没有调用Print函数,只是接收了这个函数的参数,但没有进行回调handler函数。当2号信号产生的时候,handler函数才会被调用,执行自定义捕捉

void Print()
{
	printf("hello world\n");
}
void show(int a,func_t f)
{
	printf("hello show\n");
}

int main()
{
	show(10,Print);
	return 0;
}

PS:前台进程与后台进程

  • ./进程名——运行起来的是前台进程,./进程名 &——运行起来的是后台进程,只能kill-9干掉后台进程。
  • Ctrl+c产生的信号只能发送给前台进程。如果是后台进程,则程序可以放到后台运行,即不能干掉后台进程,这样shell就不必等待进程结束,就可以接受新的命令,启动新的进程。
  • 前台进程在运行过程中用户随时可能按下Ctrl+c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的

调用系统函数向进程发信号

当我们要使用kill命令向一个进程发送信号时,我们可以用kill -信号名(信号编号) 进程ID的形式进行发送。

kill函数

实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号,kill函数的函数原型如下:

int kill(pid_t pid, int sig);

kill函数用于向进程ID为pid的进程发送sig号信号,如果信号发送成功,则返回0,否则返回-1。
我们可以用kill函数模拟实现一个kill命令,实现逻辑如下:

//测试代码loop.cc
                                                    
  1 #include<iostream>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6     while(true)
  7     {
  8         std::cout<<"我是一个进程。。。pid:"<<getpid()<<std::endl;
  9         sleep(2);                                                                    
 10     }
 11 }
  1 #include<iostream>
  2 #include<cstdlib>
  3 #include<string>
  4 #include<unistd.h>
  5 #include<signal.h>
  6 #include<sys/types.h>
  7 #include<cstring>
  8 #include<cerrno>
  9 #include<cassert>
 10 
 11 void Usage(std::string proc )
 12 {
 13     printf("\tUsage\n\t");
 14     std::cout << proc << "信号编号 目标进程" << std::endl;
 15 }
 16 int main(int argc,char* argv[])
 17 {
 18     if(argc!=3)
 19     {
 20         Usage(argv[0]);
 21         exit(1);
 22     }
 23     int signo = atoi(argv[1]);
 24     int target_id = atoi(argv[2]);                                                   
 25     int n = kill(target_id,signo);
 26     if(n!=0)
 27     {
 28         std::cerr << errno <<":"<<strerror(errno) << std::endl;
 29     }
 30 
 31 }
//makefile文件
  1 .PHONY:all
  2 all:mykill loop
  3 
  4 loop:loop.cc
  5     g++ -o $@ $^ -std=c++11
  6 
  7 mykill:mykill.cc
  8     g++ -o $@ $^ -std=c++11
  9 .PHONY:clean
 10 clean:
 11     rm -f mykill loop                                                                

在这里插入图片描述

raise函数

raise函数可以给当前进程发送指定信号,即自己给自己发送信号,raise函数的函数原型如下:

int raise(int sig);

raise函数用于给当前进程发送sig号信号,如果信号发送成功,则返回0,否则返回一个非零值。

例如,下列代码当中用raise函数每隔一秒向自己发送一个2号信号。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
	printf("get a signal:%d\n", signo);
}
int main()
{
	signal(2, handler);
	while (1){
		sleep(1);
		raise(2);
	}
	return 0;
}

在这里插入图片描述

abort函数

raise函数可以给当前进程发送SIGABRT信号,使得当前进程异常终止,abort函数的函数原型如下:

void abort(void);

abort函数是一个无参数无返回值的函数。

例如,下列代码当中每隔一秒向当前进程发送一个SIGABRT信号。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
	printf("get a signal:%d\n", signo);
}
int main()
{
	std::cout << "begin " << std::endl;
	sleep(1);
	abort();//给自己发送指定信号
	std::cout << "end " << std::endl;
	
	return 0;
}

运行结果:只打印出来begin,没打印end,因为执行到abort函数时直接终止了进程。
在这里插入图片描述
也可以这样写:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
	printf("get a signal:%d\n", signo);
}
int main()
{
	signal(SIGABRT,handler);

	while(true)
	{
		std::cout << "begin " << std::endl;
		sleep(1);
		abort();//给自己发送指定信号
		std::cout << "end " << std::endl;
	}
	
	
	return 0;
}

但是上述代码并未产生循环,打印完get a signal之后,就aborted退出了,总之,abort可以被自定义捕捉,但是“我”依然要终止进程。

由软件条件产生信号

alarm函数 和SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

我们先来看一段代码:
在这里插入图片描述
上面这段代码是用以验证算力的,算力结果如下:
在这里插入图片描述

但是为什么算力这么小呢?原因是因为有IO(需要打印到显示器),网络的约数限制,所以说IO的效率其实非常低。

那我们让算力安心计算,1s到了,就调用自定义函数对其捕捉。
在这里插入图片描述

此次运行结果:
在这里插入图片描述

alarm函数的自举:自己调用自己

在这里插入图片描述
运行结果:
在这里插入图片描述

由硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元(硬件)会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

除0错误的本质,就是就是触发CPU硬件异常,如将CPU的某个状态寄存器置1,代表本次计算有溢出问题,然后OS找到发生错误的进程,向该进程的PCB内写入对应的信号,使该进程异常退出。

在这里插入图片描述

运行结果:我们的进程确实是收到了:8信号导致崩溃的

对空指针的解引用问题:导致MMU内存管理单元出现硬件异常问题。

在这里插入图片描述
在这里插入图片描述

总结

信号的产生:

  • 键盘
  • 系统调用
  • 指令
  • 软件条件
  • 硬件异常

都是借助OS之手向目标进程发送信号,即向目标进程pcb写信号位图。

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_麦子熟了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值