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

目录

  • 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 信号保存
      • 3.2.1 coredump
      • 3.2.2 位图
        • 3.2.2.1 sigset
        • 3.2.2.2 信号集操作函数
    • 3.3 信号处理
      • 3.3.1 sigaction

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 }

当这个前台进程在运行的时候,输入什么命令都没反应
从信号的生命周期了解信号(Linux进程信号)_第1张图片
在一个终端,只允许有一个前台进程。当myfile在前台运行的时候,bash作为后台进程接收不到其他命令。
那么将它变成后台进程。./myfile &,bash此时在前台
在这期间,其他命令可以运行,但是ctrl + c关不了myfile这个进程了
从信号的生命周期了解信号(Linux进程信号)_第2张图片

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

注意:

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

2. 系统当中的信号

kill-l查看信号
从信号的生命周期了解信号(Linux进程信号)_第3张图片
一共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是一个向当前进程发送信号的库函数
从信号的生命周期了解信号(Linux进程信号)_第4张图片

运行起来会杀掉当前进程。
在这里插入图片描述
但是值得注意的是,他不能被捕捉。这个代码利用了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函数

从信号的生命周期了解信号(Linux进程信号)_第5张图片
他是一个执行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,
从信号的生命周期了解信号(Linux进程信号)_第6张图片
通过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忽略
从信号的生命周期了解信号(Linux进程信号)_第7张图片
所以之前用的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,就删除了
覆盖
从信号的生命周期了解信号(Linux进程信号)_第8张图片

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时解除屏蔽,递达,默认动作是终止进程。
从信号的生命周期了解信号(Linux进程信号)_第9张图片
递达完之后,pending表2号比特位肯定又变成0了,但是默认动作终止了进程,我们也看不到。
不过我们可以修改抵达行为,自定义捕捉一下,让他不要终止。
也就是说现象是,与之前大致一样,不过当键盘输入2号信号时,立马被捕捉,由于进程没有终止,循环继续,打印出来是全0.
从信号的生命周期了解信号(Linux进程信号)_第10张图片

 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就是一个指针数组,里面存储这函数指针
从信号的生命周期了解信号(Linux进程信号)_第11张图片

3.3 信号处理

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

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

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

从信号的生命周期了解信号(Linux进程信号)_第13张图片

从信号的生命周期了解信号(Linux进程信号)_第14张图片
这下就可以理解,信号是内核态转到用户态的时候处理的。

3.3.1 sigaction

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

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

  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 }

从信号的生命周期了解信号(Linux进程信号)_第16张图片

你可能感兴趣的:(Linux操作系统,linux)