当前位置:主页 > 查看内容

从信号的生命周期了解信号(Linux进程信号)

发布时间:2021-09-17 00:00| 位朋友查看

简介:目录 1. 生活中的信号 2. 系统当中的信号 3. 信号的生命周期 3.1 信号产生 3.1.1 键盘产生 3.1.2 系统调用函数产生 3.1.2.1 kill系统调用 3.1.2.2 raise函数 3.1.2.3 abort函数 3.1.3 软件条件产生 3.1.3.1 alarm 3.1.4 硬件条件产生 3.1.5 总结 3.2 信号保……

1. 生活中的信号

红绿灯,现在眼前没有红绿灯,但我们知道红灯停,绿灯行。幼儿园老师在我们的头脑里注册了这一个方法。
狼烟,狼烟虽然没有点燃,但一经点燃,官兵知道该怎么做。
闹钟,闹钟没响,但早上响起,我们就要起床。
下课,下课铃没响,但是铃声一响,我们可以出教室。

信号就是事件发生的一种通知机制。注意,进程通信是传输数据,而信号是通知事件给进程,但是可以以看做通信的一种方式。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5    while(1)                                                                                                                                                          
  6    {
  7      printf("running\n");
  8      sleep(1);
  9    }
 10    return 0;
 11 }

当这个前台进程在运行的时候,输入什么命令都没反应
在这里插入图片描述
在一个终端,只允许有一个前台进程。当myfile在前台运行的时候,bash作为后台进程接收不到其他命令。
那么将它变成后台进程。./myfile &,bash此时在前台
在这期间,其他命令可以运行,但是ctrl + c关不了myfile这个进程了
在这里插入图片描述

用上节学到的进程间通信是可以解释的,无论是后台进程myfile,还是bash输入的命令。都在显示器显示,显示器是文件,它也是一种临界资源,没有他加锁所以显示到显示器就乱打。

注意:

  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的
  4. fg,将一个进程变成前台进程。bg将一个进程变为后台进程

2. 系统当中的信号

kill-l查看信号
在这里插入图片描述
一共62个信号,前31个叫做普通信号,后31个叫做实时信号。

SIGINT:就是ctrl+c,
SIGQUIT:就是ctrl+\ ,会生成一个core文件
SIGPIPE:读端关闭读,写端还在写就会发送sigpipe信号

3. 信号的生命周期

生活中,当你接到快递的电话并不是立刻去取快递,而是把手头的事情做完,再去取,而中间这个过程,取快递这个信号被我们保存在脑海里,所以得出结论,信号产生之后并不是被立即处理的。
在这里插入图片描述
那么就可以从这三个方面去研究信号。
信号产生的方式。
信号保存的方式。
信号处理的方式。

3.1 信号产生

3.1.1 键盘产生

ctrl+c,ctrl+/

3.1.2 系统调用,函数产生

3.1.2.1 kill系统调用

kill是系统调用,向进程发送信号
在这里插入图片描述
形参是进程的pid,要发几号信号

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/types.h>
  5 #include<signal.h>
  6 int main(int argc,char* argv[])
  7 {
  8    if(argc==3)
  9    {
 10   //字符串转换成整形,不是强转,不是强转,强转是不改变底层结构的
 11      kill(atoi(argv[1]),atoi(argv[2]));                                                                                                                                                                
 12    }
 13    return 0;
 14 }

让sleep 100变成后台进程,然后调用我们写的程序杀掉他。

在这里插入图片描述

3.1.2.2 raise函数

raise是一个向当前进程发送信号的库函数
在这里插入图片描述

运行起来会杀掉当前进程。
在这里插入图片描述
但是值得注意的是,他不能被捕捉。这个代码利用了signal系统调用,捕捉信号。而面对9号信号,无法捕捉。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/types.h>
  5 #include<signal.h>
  6 
  7 void handle(int singno)
  8 {
  9   printf("catch singno:%d\n",singno);
 10 }
 11 int main()                                                                                                                                                           
 12 {
 13    signal(9,handle);
 14    raise(9);
 15    return 0;
 16 }

依旧是被killed
在这里插入图片描述
其实仔细想想也能理解,万一这个进程是病毒呢,假如全部信号被捕捉,运行起来没人管得了他了。
捕捉2,是可以的
在这里插入图片描述

3.1.2.3 abort函数

在这里插入图片描述
他是一个执行6号信号的函数,需要注意的是即使我们捕捉他,他还是会在之后运行成功
在这里插入图片描述

这三个是递进关系,kill是任意进程,任意信号。raise是任意信号。abort是6号信号

3.1.3 软件条件产生

向管道,假如读端关闭掉文件描述符,写端是可能会被直接杀掉,用的信号就是sigpipe

3.1.3.1 alarm

在这里插入图片描述

当某种条件满足,产生的信号

3.1.4 硬件条件产生

模拟一个野指针,会报出段错误

int *p=NULL;
*p=2;

在这里插入图片描述
经查看是11号信号。野指针是怎么被发现呢?
进程里有指针操作,指针里面存放着虚拟地址,解引用访问数据,虚拟地址转换为物理地址,页表内不存在,或者标志位不一致,而mmu硬件转换出现错误,操作系统识别到错误。
野指针(11号信号)
数组越界
除零(8号信号)
溢出等:cpu状态寄存器,溢出会存储溢出状态

3.1.5 总结

硬件产生,操作系统把组合键解释成信号发给进程。
系统调用,操作系统提供的
软件条件,时机成熟,提醒操作系统发出。
硬件条件,操作系统识别。

所有信号都经过操作系统之手完成。因为操作系统是进程的管理者。这四种都是信号产生的触发条件。

3.2 信号保存

3.2.1 coredump

当进程异常终止的时候,操作系统将你的一些重要信息转储到硬盘当中生成一个coredump文件,可以通过查看coredump文件来进行查看错误信息。
但是默认不生成
在这里插入图片描述
需要我们修改core file size,
在这里插入图片描述
通过ulimit -s 修改生成coredump文件的大小
在这里插入图片描述
以debug方式生成程序,为了方便调试
在这里插入图片描述

通过3号信号来终止这个进程
在这里插入图片描述
通过gdb调试可以看出,调试信息给出通过3号信号终止了它
在这里插入图片描述

所以实际上,这是一种事后调试,进程崩溃之后才进行定位,那为什么需要默认关闭呢,因为默认生成一个core文件,这个临时文件还是比较大的,例如当一个公司的服务器挂掉,首要的问题不是找出原因为什么挂掉,而是先恢复启动,再找出错误所在。假如不断地挂掉,那么就会不断生成core文件。
在之前进程等待时,waitpid方法的第二个参数,status是一个输出性参数,调用此方法,等待的进程正常退出,错误退出,检测这个整数st16位中的高8位,进程异常退出,通过低7位来检测进程异常退出时,操作系统所发出的信号。而第8位就保存着当前进程是否需要coredump生成core文件。(因为有些信号是不需要要你生成core文件的例如kill -9)。

3.2.2 位图

操作系统发出信号,进程不是立即执行信号所对应操作,那么就需要将这个信号保存起来,而位图正是保存信号的方式。也就是说一个进程结构体里一定有一个位图来存储信号,默认全0。

task_struct
{
unsigned int map=0;
}

对于普通信号,比如当操作系统发2号信号,就对应把这个位图中第2个比特位改成1。所以操作系统发信号,不如把他说成写信号更加合适。操作系统的任务结束,接下来是进程该如何处理。所以操作系统写信号,进程不是立即执行的,由进程自己决定什么时候操作。

看几组概念

  • 实际执行信号的处理动作称为信号递达(Delivery)3种方式(默认,自定义,忽略)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。不是立即执行
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
    注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
    之前创建一个进程需要以下结构体,
    pcb,地址空间,页表,file_struct,现在又要加一个信号结构体。

前两个表是位图结构,第几个比特位代表第几个信号,比特位为1代表存在,为0代表不存在
Pending表:是否收到信号,收到几号信号
Block表:几号信号,无论是否收到,我就要阻塞他(阻塞可以被取消)
handler表是数组结构,指针数组。
handler表:里面存放着一个个函数指针,DFL默认操作,IGN忽略
在这里插入图片描述
所以之前用的signal(信号,函数指针)。下标代表几号信号,执行什么动作就把对应的函数地址写进handler表这个指针数组当中。

注意:如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次
或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

3.2.2.1 sigset

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,相当于我们自己定义的变量,用作输出型参数,与后面的函数相互配合,这个类型可以表示每个信号
的“有效(1)”或“无效(0)”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

3.2.2.2 信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量。

int sigemptyset(sigset_t *set);

把比特位全部清空

int sigfillset(sigset_t *set);

把比特位设置成全1

int sigaddset (sigset_t *set, int signo);

把对应信号集中信号的比特位由0变1

int sigdelset(sigset_t *set, int signo);

由1变成0

int sigismember(const sigset_t *set, int signo);

查看信号,在信号集中对应的比特位是否为1
先定义一个信号集变量,这几个都是对我们定义的那个信号集进行操作(无论是block或pending),pcb中对应的信号集,是在调用了sigprocmask或sigpending之后才开始改变
sigprocmask
通俗点来说那张block位图就叫做信号屏蔽字。
参数解释:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
mask|set 按位与 ,00 | 01 = 0 1,就把第二个信号添加了
mask&~set 希望删除第二个,0111 & ~(0100)= 0011,就删除了
覆盖
在这里插入图片描述

int sigpending(sigset_t *set);

sigset_t pending;
sigemptyset(&pending);


读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

下面来练习一下这几个函数。
block表中阻塞2号信号,打印pending表(没有传入全0),键盘传入2号信号(到pending表),但由于被阻塞,所以就一直在pending表中不会被递达,打印pending表(2号信号比特位为1)。10次之后,解除屏蔽。由于2号信号递达,进程终止
所以看到的结果是,先是全0,前四秒没动,当我发送2号信号,pending表中2号比特位变成1,后6秒打印出来,第10s时解除屏蔽,递达,默认动作是终止进程。
在这里插入图片描述
递达完之后,pending表2号比特位肯定又变成0了,但是默认动作终止了进程,我们也看不到。
不过我们可以修改抵达行为,自定义捕捉一下,让他不要终止。
也就是说现象是,与之前大致一样,不过当键盘输入2号信号时,立马被捕捉,由于进程没有终止,循环继续,打印出来是全0.
在这里插入图片描述

 1 #include<stdio.h>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 void show_pending(sigset_t *pending)
  5 {
  6   int i=1;
  7   //只阻塞了2号信号
  8   for(;i<=31;i++)
  9   {
 10     //i=2时,判断2号信号是否在pending表中
 11      if(sigismember(pending,i))
 12      {
 13        printf("1 ");
 14      }
 15      else{
 16        printf("0 ");
 17      }
 18   }
 19   printf("\n");
 20 }
 21 void handler(int sig)                                                                                                                                                
 22 {
 23   printf("%d signal was catched\n",sig);
 24 }
 25 int main()
 26 {
 27 
 28     signal(2,handler);
 29     sigset_t block,o_block;
 30     //初始化
 31     sigemptyset(&block);
 32     sigemptyset(&o_block);
 33     //向我们的变量添加2号信号被阻塞
 34     sigaddset(&block,2);
 35     //把我们设置好的传给pcb中,pcb为改变之前给到o_block
 36     sigprocmask(SIG_SETMASK,&block,&o_block);
 37     int count=0;
 38     //不断获取pending表,键盘输入2号信号,过10秒之后解除屏蔽,进程就结束了
 39     while(1)
 40     {
 41     //初始化
 42     sigset_t pending;
 43     sigemptyset(&pending);
 44     //pcb中的pending,传给我们的变量
 45     sigpending(&pending);
 46     //先输出全0,当在这10s内通过键盘输入2号信号,2号信号比特位输出1,10s到达进程停止
 47     show_pending(&pending);
 48     sleep(1);
 49     count++;
 50     if(count==10)
 51     {
 52       printf("the process will destory\n");
 53       //把他以前的经过初始化全0的block表给他,代表10s过后此时没有阻塞
 54       sigprocmask(SIG_SETMASK,&o_block,NULL);
 55     }
 56     }
 57   return 0;
 58 }                              

阅读源码可以看到,block表与pending表,handler表(sighand)

在这里插入图片描述

由于有普通信号和实时信号,所以他们两个要区分开
在这里插入图片描述
action就是一个指针数组,里面存储这函数指针
在这里插入图片描述

3.3 信号处理

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 自定义,提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

之间讲过,信号不是被立即处理的,而是在合适的时候,这个合适的时候就是内核态切换到用户态的时候。我们的程序是有可能在用户态在内核态之间互相切换的。
这句话怎么理解呢?假如在程序中定义一个或者修改一个变量,是单纯的用户态。一旦当你printf()这个数据,就必定要调用系统调用接口write,传入文件描述符1,来打印到屏幕,这个write是系统调用,是操作系统帮我们做的,所以这就叫内核态。我们大多数写的代码都是由用户态和内核态组成,所以进程在运行的时候也是内核态,用户态互相切换。

在这里插入图片描述
由于大多数进程的代码和数据都不一样,所以每个进程的用户级页表和物理内存映射关系也不一样,但是操作系统的代码在物理内存只有一份。cpu是怎么区分你现在是内核还是用户呢
当你进行系统调用时,每个进程都有自己的内核空间,进程瞬间陷入内核(相当于顶了一个进程壳子的操作系统),寄存器cr会识别到你现在是用户还是内核。

在这里插入图片描述

在这里插入图片描述
这下就可以理解,信号是内核态转到用户态的时候处理的。

3.3.1 sigaction

在这里插入图片描述
signum代表几号信号。
const struct sigaction *act代表要执行的动作
struct sigaction *oldact代表之前的执行动作

第二个和第三个参数的类型,struct sigaction这个结构体里面包含
在这里插入图片描述
第二个和第五个是处理实施信号。
第一个是我们处理的动作(函数指针)
第三个是信号集,当你在进行处理对应信号时,把这个信号阻塞掉,防止处理的时候被再次调用产生干扰
练习一下。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<signal.h>
  4 void handler(int sal)
  5 {
  6   printf("handling %d\n",sal);
  7 }
  8 int main()
  9 {
 10   struct sigaction act,o_cat;
 11   act.sa_handler=handler;
 12   act.sa_flags=0;
 13   sigemptyset(&act.sa_mask);
 14 
 15   sigaction(2,&act,&o_cat);
 16   while(1);                                                                                                                                                          
 17   return 0;
 18 }

在这里插入图片描述

;原文链接:https://blog.csdn.net/qq_45928272/article/details/115642551
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:Recent Plan(线上交流课——(一)) 下一篇:没有了

推荐图文


随机推荐