进程信号详解

目录

背景知识

信号产生的各种方式

信号其他相关常见概念

信号发送后

sigaction 

可重入函数

volatile

SIGCHLD信号 


背景知识

生活中有信号的场景有很多,比如闹钟,红绿灯,信号枪,鸡叫声等等,这些都是给人看的!所以

当我们听到这些场景触发的时候,我们立马就知道了我们接下来该做什么,但并不是只有这些场景

真正的放在我面前,我才知道该怎么做,其实和场景是否被触发,没有直接关联!比如我定闹钟

下午三点学习,并不是等闹钟响了我才知道要学习,而是我早就知道了,只是提醒我而已!

进程具有识别信号并处理信号的能力,远远早于信号的产生!

对于信号的处理动作,我们早就知道了,甚至远早于信号的产生!这是我们对特定事件的反应,是

被教育的结果!本质:你记住了!而对于进程,在没有收到信号的时候,它应该如何识别是哪一个

信号,以及处理它?它知道是因为曾经编写操作系统的工程师在写进程源代码的时候,就

好了!

进程收到某种信号的时候,并不是立即处理的,而是在"合适"的时候

在生活中,我们收到某种"信号"的时候,并不一定是立即处理的,信号随时可能产生(异步),但是

我当前可能做着更重要的事情!比如,你在做引体向上时,突然来了个电话,但距离你的目标还差

几个没有完成,所以你会先做完这最后几个,再去接电话

进程收到信号之后,需要先将信号保存起来,以供在"合适"的时候处理! 

既然信号不能被立即处理,已经到来的信号,会保存起来,保存至struct task_struct中

信号的本质也是数据,信号的发生,也就是往进程task_struct内写入信号数据!

无论我们的信号如何发送,本质都是在底层通过os发送的!

task_struct是一个内核数据结构,定义进程对象,内核不相信任何人,只相信它自己!所以只能

是os向task_struct内写入信号数据!

信号产生的各种方式

如下图,1-31号被称为普通信号,34-64被称为实时信号(不做讲解)

如下图,是一个修改进程对信号的默认处理动作的接口!

键盘Ctrl+c的时候,本质是我们向指定进程发送2号信号!证明如下图,通过signal注册对2号信号

的处理动作,改成我们的自定义动作

信号的产生方式其中一种就是通过键盘产生,键盘产生的信号,只能用来终止前台进程

如下图,将所有信号都进行捕捉,执行同一种动作

总结

一般而言,进程收到信号的处理方案有3种情况

默认动作——一部分是终止自己,暂停等

忽略动作——是一种信号处理的方式,只不过什么也不干

自定义动作(信号的捕捉)——我们刚刚用signal方法,就是在修改信号的处理动作,让其由默认

-> 自定义动作

如下图,让不同的信号执行不同的动作,但是发送9号信号后,却没有执行修改后的9号动作,还是

原来的杀掉进程,这是因为9号信号不可以被捕捉(自定义)!!!

 

信号产生的方式,程序中存在异常问题,导致我们收到信号退出

如下图,发生了进程的崩溃,因为收到了信号,所以进程会崩溃,在Windows或Linux下,进程崩

溃的本质,是进程收到了对应的信号,然后进程执行信号的默认处理动作(杀死进程)

为什么会发送信号

软件上面的错误,通常会体现在硬件或者其他软件上,所以当CPU处理a /= 0后,会将处理结果保

存至状态寄存器中,而os管理硬件,就要对硬件的健康(不是指硬件损坏,而是运算问题等等)

负责,然后os就会找那个拥有此代码的进程,然后发送信号,终止进程!

当进程崩溃的时候,我们除了想知道崩溃的原因外,还需知道在哪一行崩溃了!

在Linux中,当一个进程退出的时候,它的退出码和退出信号会被设置(正常情况)

当一个进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因

如果必要,os会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘中,

便我们后期调试

如下图,在云服务器上,默认情况下,core dump是被关掉的,core file size的值为0,我们可以

使用ulimit -c 10240来将其打开,可以看到一个core.5843的文件

如下图,可以在用gdb调试时,找到进程崩溃的具体行,就可以事后调试

进程如果异常的时候,被core dump,该位置会被设置为1,但不是所有的信号都会被core dump

通过系统调用产生信号

kill

给一个指定的进程发送一个指定的信号

raise

给自己发送一个指定的信号

abort

给自己发送一个明确的信号—6号信号

软件条件,也能产生信号

通过某种软件(os),来触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪等

这样的场景下,触发的信号发送。例如:在进程间通信中,当读端不仅不读,而且还关闭了读fd,

写端一直在写,最终写进程会收到sigpipe(13),就是一种典型的软件条件触发的信号发送

alarm

延时信号的发送,返回值是0或者是以前设定的闹钟时间还余下的秒数,alarm(0):取消闹钟

统计一下,在一秒内,我们的server能够对count递增到多少,从图中可看出两者差距非常大,前

者比较慢,是因为有IO

总结

信号产生的方式虽然非常多,但是无论产生信号的方式千差万别,但是最终一定都是通过os

目标进程发送的信号!即产生信号的方式,本质都是os发送的!

信号的编号是有规律的,从1到31,而进程中,就可以采用位图来标识该进程是否收到信号!

比特位的位置(第几个),表示是信号的编号,比特位的内容(0,1),表示是否收到了信号!

如何理解os给进程发送信号->os发送信号数据到task_struct->本质是os向指定进程的task_struct

的信号位图写入比特位1,即完成信号的发送(信号的写入)

信号其他相关常见概念

实际执行信号的处理动作称为信号递达(Delivery)
分为三种:自定义捕捉,默认,忽略
信号从产生到递达之间的状态,称为信号未决(Pending),本质是这个信号被暂存在task_struct的
号pending位图中,
进程可以选择阻塞 (Block )某个信号,本质是os,允许进程暂时屏蔽指定的信号,结果有两种
该信号依旧是未决的
该信号不会被递达,直到解除阻塞,方可递达
递达中的忽略与阻塞的区别
忽略是递达的一种方式,而阻塞是没有被递达,独立状态
进程内置了"识别"信号的方式
如下图,这三张表得横着看,没有收到信号,也是可以阻塞的,但被阻塞的信号,不能被递达
block表,本质上,也是位图结构,uint32_t block:比特位的位置, 代表信号的编号,比特位的
容,代表信号是否被阻塞,阻塞位图也被叫做信号屏蔽字 

sigset_t

不要认为只有接口才算是system call,也要意识到:os也会给用户提供数据类型,配合系统调用

完成

接口sigprocmask

修改的是进程的block位图,参数oldset,是返回老的信号屏蔽字——block位图

如下图,系统给用户提供的数据类型,创建的变量,不能直接进行运算,而是要调用对应的接口!

如下图,屏蔽2号信号和9号信号,但从结果可知,9号信号不能被屏蔽

  

 如下图,此接口不对pending位图做修改(os来修改),而只是单纯的获取进程的pending位图

如下图,预先屏蔽2号信号,然后不断的获取pending位图,再手动发送2号信号,再获取pending

位图,并打印显示!

如下图,10秒过后,恢复2号信号,但是却看不到现象,因为2号信号的默认动作是终止进程,所

以看不到现象

捕捉2号信号后,就可以看到现象了!

信号发送后

信号发送之后,不是被立即处理,而是在"合适"的时候,因为信号的产生是异步的,当前进程可能

正在做更重要的事情,所以需要将信号延时处理(取决于os和进程)

"合适"的时候:从内核态切换回用户态的时候,进行信号检测与信号处理!

内核态:进程执行os的代码和数据时,所处的状态就叫做内核态,os的代码的执行全部都是在

核态!

用户态:用户代码和数据被访问或者执行的时候,所处的状态,我们自己所写的代码全部都是在用

户态执行的!

两种状态的主要区别在于权限,内核态的权限更高一些

感性理解

实际上,我们在写代码时,可能在不断的从用户态切入内核态,内核态切入用户态!最典型的就是

调用系统调用!

如下图,用户调用系统函数的时候后,除了进入函数,身份也会发生变化,用户身份变成内核身份

较为理性的认识

用户的身份是以进程为代表的

如下图,用户的数据和代码一定要被加载到内存,而os的数据和代码也是一定要加载到内存中的

而只有一个CPU,os启动之后,os的数据和代码会通过内核页表映射到物理内存被执而内

核页表,整个系统只有一份,被所有进程共享!

CPU内有名字为CR3的寄存器保存了当前进程的状态

用户态使用的是用户态页表,只能访问用户数据和代码

内核状态使用的是内核态页表,只能访问内核的数据和代码

进程具有了地址空间是能够看到用户和内核的所有内容的,但不一定能访问

进程之间无论如何切换,我们能够保证我们一定能找到同一个os,因为我们每个进程都有3~4G

的地址空间,使用的是同一张内核页表

所谓的系统调用,就是进程的身份转化为内核,然后根据内核页表找到系统函数,执行就行了

在大部分情况下,实际上我们os都是可以在进程的上下文中直接运行的

信号的捕捉过程

如果检测没有信号会直接返回,去执行代码的下一行,默认就会执行默认的信号操作,比如终止进

程,直接将进程的相关资源释放就完成了,而忽略,则会将pending位图中那个信号置为0,然后

返回去执行代码的下一行,而自定义则是如下图所示

sigaction 

修改的是handler函数指针数组

捕捉2号信号

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返

时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么

它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,

还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函

数返回时自动恢复原来的信号屏蔽字,如下图

  

可重入函数

如下图,以链表头插节点为例,当插入node1节点时,node1指向之前的第一个节点后,头节点还

未指向node1节点,信号到来,就去处理信号,也是执行链表头插节点,处理完后,就返回main

执行流,本来头节点应该指向node2节点,而此时又让头节点指向node1节点,就会造成内泄漏

问题,这种现象也就可以被称为insert函数被重复进入了!

insert函数一旦重入,有可能出现问题——该函数:不可重入函数

insert函数一旦重入,不会出现问题——该函数:可重入函数

我们所学到的大部分函数,STL,boost库中的函数,大部分都是不可重入的!

volatile

如下图,编译器优化后的两种不同结果,gcc编译器有大O(0-4)的几种优化级别

而在flag变量前加了volatile关键字后,也就让编译器不再优化了!

 

解释如下图,优化前,CPU每次都要从内存加载数据,然后运算,但在发现这个值没有做修改时,

就不再从内存去加载数据,而是将值保存至寄存器,每次都去寄存器加载数据,提高效率,所以如

上图,当flag被修改为1后,实际上改的只是内存中的值,而没有改变寄存器保存的值,volatile也

就是告诉编译器不做这个优化,每次都从内存加载数据

SIGCHLD信号 

子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略

如下图,显示设置忽略17号信号,当进程退出后,自动释放僵尸进程

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
进程间通信中,信号是一种非常重要的机制。它可以让一个进程向另一个进程发送一个异步通知,让接收进程执行特定的操作。 信号的基本概念是:当一个进程需要通知另一个进程发生了某个事件时,它可以向另一个进程发送一个信号。接收进程在接收到信号后,可以选择忽略该信号或者执行相应的处理程序。 信号可以由如下几种方式发送: 1. kill命令:可以使用kill命令向指定进程发送信号。 2. 信号函数:可以使用信号函数向指定进程发送信号。 3. 软件异常:当一个进程执行某些非法操作时,操作系统会向该进程发送一个信号。 常见的信号有: 1. SIGINT:中断进程。通常由Ctrl+C键触发。 2. SIGTERM:请求进程终止。通常由kill命令发送。 3. SIGKILL:强制进程终止。通常由kill命令发送。 4. SIGSTOP:暂停进程。通常由kill命令发送。 5. SIGCONT:恢复进程。通常由kill命令发送。 6. SIGUSR1和SIGUSR2:用户自定义信号。 在Linux系统中,可以使用kill命令向指定进程发送信号。例如: ``` kill -SIGINT pid ``` 这个命令会向指定pid的进程发送SIGINT信号,让该进程接收到信号后执行相应的处理程序。 在编写程序时,可以使用信号函数向指定进程发送信号。例如,下面的代码向指定进程发送SIGUSR1信号: ``` #include <signal.h> #include <unistd.h> int main() { int pid = 12345; // 指定进程的pid kill(pid, SIGUSR1); // 向指定进程发送SIGUSR1信号 return 0; } ``` 当接收进程接收到信号后,可以执行相应的处理程序。例如,下面的代码定义了一个信号处理程序,当接收到SIGUSR1信号时会执行该处理程序: ``` #include <signal.h> void handle_signal(int signum) { // 处理SIGUSR1信号 } int main() { signal(SIGUSR1, handle_signal); // 注册信号处理程序 while(1) {} // 循环等待信号 return 0; } ``` 上面的代码中,signal函数用于注册信号处理程序,当接收到SIGUSR1信号时会执行handle_signal函数。循环部分是为了等待信号的到来。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值