进程信号(上)【Linux操作系统】

发布于:2025-05-22 ⋅ 阅读:(19) ⋅ 点赞:(0)

进程信号

信号引入

生活中的信号:
狼烟,闹钟,上下课铃声等都是信号
狼烟四起便知敌人来犯,闹钟响起,就知道要做对应的事情了

信号是操作系统/用户/其他进程向目标进程发送异步事件的一种方式
[异步事件即:出乎意料,突然发生的事件]
异步事件指的是
在没有预先定义的时间点发生的事件,这些事件与程序执行的正常流程无关,它们可以在任何时候发生,通常是由外部条件或系统内部的其他进程触发的。


进程要如何识别信号?

类比我们人识别信号,我们是通过学习知识,就知道红绿灯红灯亮了要停
进程也是如此,但是它不需要学习,因为程序员通过代码把"知识"直接塞进了进程的脑子里
本质就是:操作系统的进程模块中编写了信号的种类和对应信号的默认处理函数
也就是
进程能识别的大部分信号都是内置的,是进程出生就有的
要识别新的信号,就要编写对应代码,进程就能识别对应信号

进程在信号产生之前,就已经知道要怎么处理信号了
因为可能收到的信号的种类和其对应的处理方式是写进操作系统源代码里面的,都运行起来了自然知道
就比如
我们就算面前没有红绿灯,我们也知道如果遇到红灯应该停下
我们定了一个明天的闹钟,我们定闹钟是时候,就知道它明天会响,响了要干嘛


进程接收到信号的时候,不一定马上处理信号

因为一般信号都是突然出现的,进程在信号出现的时候可能正在做一些任务
所以进程一般会做完任务再处理信号,或者等到合适的时机再处理信号

所以为了能在合适的时机处理信号,进程要把接收到的信号保存起来
不然的话,进程就会"记不得"有信号要处理,要处理什么信号了


进程处理信号的情况

  1. 默认行为:
    直接按照内置的处理方式对信号进行处理
    例如:红灯停绿灯行

  2. 忽略信号:
    直接不管该信号了,不对该信号进行处理

  3. 自定义行为:
    即有些进程可能比较特殊,它接收到某一信号时,会调用用户自定义的函数



信号相关概念

  • 进程对信号执行信号处理方法时,称为信号递达

  • 进程接收到信号到处理信号之间的状态,称为信号未决

  • 进程可以阻塞(屏蔽)某个信号,屏蔽那个信号之后,即使进程接收到了这个信号,这个信号将一直处于信号未决状态,也永远不会处理它
    除非解除对这个信号的阻塞(屏蔽)

  • 阻塞信号和忽略信号的区别:
    1.阻塞信号a,a就会一直处于信号未决状态
    2.忽略信号a,表示a已经处理(递达)了,因为忽略就是一种信号处理的方式



信号产生

键盘产生

即用户从键盘输入字符给一个终端的前台进程,其中有一些特定字符就会被操作系统解释成信号

例如:
用户键盘输入ctrl c,前台进程就会接收到ctrl c,操作系统识别到之后,就会把ctrl c解释成2号信号并发送给前台进程


通过指令向进程发送信号

例如
kill 信号编号 指令,就可以给指定的进程发送信号指定信号


系统调用向进程发送信号

比如kill系统调用raise系统调用


软件条件

某个软件没有准备好,或者具备/不具备某个条件,或者软件已经不能使用了等
如果此时进程还要使用软件,那么操作系统就会发送信号,直接杀掉进程

比如
管道读端关闭,进程却还要向管道写入,此时操作系统就会给进程发送13号信号,杀掉这个进程

定时器(闹钟)也是软件
它的软件条件即:闹钟响了,操作系统就给目标进程发送信号


异常错误

进程因为野指针,除0等异常错误,而导致的进程崩溃的本质其实是:
操作系统识别到进程的异常,向进程发送了信号,进程接收信号之后才退出


操作系统如何知道进程出现了异常错误?

除0等数据运算出现错误

CPU执行代码时,遇到数据运算的时候
会把参与运算的数据,放进对应寄存器中,然后再进行计算

但是数据运算的结果可能出问题,比如遇到了除0
所以
CPU中有一个寄存器,叫做状态寄存器(Eflags),它用来衡量CPU的数据运算是否有问题
①如果它里面的比特位都为0,那就没问题
②有一个比特位为1就说明有问题

所以一旦CPU数据计算出现问题,状态寄存器里面对应的标志位就会被0→1
比如
如果除0了,状态寄存器里面的溢出标志位就0→1

此时CPU就会通过内部中断向操作系统报告错误
所以操作系统就知道有进程出现了数据运算类的异常错误
操作系统就要处理这个错误:即用信号杀掉进程
这个进程是谁呢?
当然就是当时正在CPU上跑的进程


如果此时这个出现数据运算异常的进程把对应的信号捕捉了,把它的默认处理方法改了,进程没有被杀掉,此时会怎样?
操作系统会死循环地使用信号杀它
因为出现数据运算异常的进程再操作系统看来是必死无疑的,所以没必要对清除它的上下文数据,等下一个进程上来直接覆盖就行
所以这个进程再CPU中对应的上下文数据不会改变
也就是状态寄存器中的标志位一直都在
所以这个进程就算没有退出,CPU只要执行它的代码,就会发现状态寄存器有标志位为1,就会告诉操作系统,操作系统又会发信号杀它
所以数据运算异常的进程就算捕捉信号了,不会退出,它后面的代码也跑不了了
因为CPU看见它就内部中断,就只会上报操作系统,根本不跑代码了。



野指针等段错误

进程产生野指针等段错误,多半就是虚拟地址在页表中没有对应的物理地址,无法完成虚拟到物理的转换

虚拟到物理的转换是由硬件MMU实现的,MMU也是在CPU内部的
当MMU发现无法做到虚拟地址→物理地址时,就会报错
此时就和上面的一样,CPU内部中断,把错误告知操作系统,操作系统通过信号杀掉进程

综上:
进程内部产生的各种异常错误,都会在对应的硬件上有所表现
操作系统作为硬件的管理者,当硬件出现问题时,就会向操作系统发送硬件中断,操作系统就自然会知道了



信号相关函数,系统调用,指令

系统调用:signal

  • 头文件:signal.h

  • 参数表:

    • 1.signum:信号编号
      以下是Linux支持的常见信号以及其对应编号
      其中前31个普通信号我们需要了解,之后的信号是实时信号不需要了解
      使用man 7 signal即可查看Linux常见信号的作用和基本信息
      在这里插入图片描述

[上图中的信号名称(SIGINT等)其实就是宏,宏值就是它们自己的编号]

  • 2.void(*p)(int)类型的函数指针:表示要替换上来的方法
    1.第2个参数如果是SIG_IGN,就表示把默认处理方法改成忽略
    2.第2个参数如果是SIG_DFL,就表示执行默认处理方法
    SIG_IGN和SIG_DFL本质是一个函数指针类型的宏
    int类型的参数的作用是:
    信号被触发(接收到信号)的时候,操作系统会把接收到的信号的编号,自动传给这个int类型的形参

  • 作用:把进程对捕捉到的信号的默认处理方法,修改成我们传入的指定方法

  • 注意:

    • ①系统调用signal的作用就只是修改进程对信号的默认处理方法
      所以只需要设置一次即可
      其他的,信号识别之类的工作不需要它做
      信号识别/信号接收等动作进程自己就内置了
    • 9号信号永远无法被捕捉,默认处理方式也无法被修改
      因为使用signal捕捉信号之后,可以修改它们的默认处理方法
      那么如果一个进程把所有的信号都捕捉了,并且把所有信号的默认处理方法全部都修改掉
      那岂不是没有任何方法能够杀死这个进程的了?[因为进程异常退出的方法只有通过信号实现]这个进程一旦启动就可以肆无忌惮地搞破坏了?
      操作系统怎么可能允许这样的事情发生!

系统调用:sigaction

  • 头文件:signal.h

  • 参数:

    • int signum:信号编号

    • struct sigaction*act:输入型参数,作为修改源
      struct sigaction中我们要在意的成员变量:- 1.viod(*)(int)sa_handler:要修改成的函数方法
      2.sigset_t sa_mask:执行该信号的信号处理方法时,除了默认会屏蔽的信号以外要新增的屏蔽的信号列表
      例:
      如果sa_mask为00001101
      进程a执行2号信号的处理方法时,1,2,3,4信号都会被block表屏蔽
      其中2号信号默认一定会被屏蔽,其他的则是因为sa_mask而屏蔽的

    • struct sigaction*old:输出型参数,把被该函数修改前结构体,带出来
      (防止用户修改了之后后悔,可以保存修改前的结构体)

  • 作用:
    修改一个进程对于某个信号的处理方法


指令:kill -信号编号 pid

给对应进程发送对应编号的信号


系统调用:kill

  • 头文件:sys/types.hsignal.h

  • 返回值:

    • 成功,返回0
    • 失败,返回-1
  • 参数:

    • pid_t pid:对应要发送给的进程的pid
    • int sig:信号编号
  • 作用:把指定信号发送给指定进程


系统调用:raise

  • 头文件:signal.h

  • 返回值:

    • 成功,返回0
    • 失败,返回-1
  • 参数:
    int sig:信号编号

  • 作用:把指定信号发送给,调用raise的进程自己


系统调用:alarm

  • 头文件:unistd.h

  • 返回值:unsigned int
    闹钟距离响起还剩多少时间
    如果闹钟正常响了,那么就返回0
    ②如果设置了闹钟之后,过了一会儿又取消或者重新设置了闹钟(alarm(0)是取消闹钟,alarm(n)就是重置闹钟)
    那么alarm就会返回上一次的闹钟距离响起还剩多少时间
    例如:
    alarm(3)→过了2秒→alarm(0)→返回值为3-2=1
    alarm(3)→过了2秒→alarm(5)→返回值为3-2=1
    所以一个进程只能有一个闹钟

  • 参数:
    unsigned int seconds:秒数

  • 作用:给进程定一个闹钟[定时器],时间到了就会给自己这个进程发送14号信号

  • 注意:
    alarm设置的闹钟是一次性闹钟
    即设置了之后,只会响一次不是每隔n秒响一次的永久闹钟。
    响一次之后就会取消闹钟

除非再调用alarm设置闹钟,才有可能继续响


系统调用:pause

  • 头文件:unistd.h

  • 参数:没有参数

  • 作用:阻塞等待信号的到来,如果没有信号来就一直阻塞着