Linux 信号产生方式
目录
信号的概念
信号的本质
名词解释
异步
前台进程
后台进程
前后台进程区别
中断
发送信号
前后台进程相关命令
信号产生的方式
查看有多少信号
进程怎么保存信号
方法1:键盘产生
signal
过程
注意
方法2:使用系统调用发送信号
kill
raise
abort
方法3:通过异常
解释:除0为什么会终止进程
解释:空指针的解引用
小结
方法4:软件条件
alarm
小结
信号的概念
- 向目标进程发送通知消息的一种机制
- 信号产生时,进程不一定立即处理它,而是在合适的时候处理,所以要能够保存信号
- 信号是异步产生的
- 进程通过不同的数字识别信号
- 有64种情况(信号)让你中断程序
信号的本质
- 用软件来模拟中断的行为
- 信号的本质是一种进程间通信机制,用于通知进程发生了异步事件,并请求其对该事件进行处理
- 信号本质上是一种软中断,允许操作系统以非阻塞的方式向进程发送通知,以便进程可以响应某些重要的系统或用户事件
名词解释
异步
- 你做你的我做我的,不会立即影响我
- 是任务之间的执行不需要彼此等待,而是可以独立地进行
前台进程
- 系统层面上只能有一个,且必须有一个
- 能接受用户输入
- 运行前台进程时(shell除外),命令不能运行
- 当没有前台进程时,OS会将shell提到前台
- 可以理解为shell这个前台进程可以启动别的前台进程
- 一定是正在运行的
后台进程
- 方式:启动时 最后加一个&
- 可以运行命令,因为这时shell变成前台进程
- 可以运行命令是因为shell是前台进程
前后台进程区别
- 能不能接收用户输入
中断
发送信号
-
其实是写信号,操作系统写到对应task_struct的位图里
前后台进程相关命令
bg number
- 将后台暂停进程变成后台运行进程
fg number
- 将后台进程切到前台,启动
jobs
- 查看后台任务
信号产生的方式
查看有多少信号
- 向特定进程发送信号
- 可以使用名字也可以使用数字
- 没有0号信号,为了标识进程正常结束,不然不知道是否要读取退出码
- 1-31:普通信号;;;34-64:实时信号
- 实时信号:一般要求OS立即处理,一般不会出现信号丢失的情况(底层用队列,链表)
- 每个进程都有一张自己的函数指针数组,数组的下标和信号编号强相关(底层指向的函数都是同一份)
进程怎么保存信号
进程是否收到信号---通过位图
- 位置决定编号
- 内容决定是否收到信号
- 进程的PCB中维护这张位图
- 用位图来管理的信号称为普通信号
方法1:键盘产生
外设间接向CPU特定的针脚发送就绪信息的
- 数据层面上CPU不和外设打交道,但是在控制信息层面上打交道(通过CPU针脚)
- 硬件给CPU发送中断信息
- CPU和外设是间接连接的
- 90以上的设备要有中断
- 通过8259将外设信息转换为光电信号
- CPU中特定的针脚所对应的编号叫做中断号
- OS内部提供一个函数指针数组(中断向量表,OS启动时候启动的第一张表),下标就是中断号对应硬件的读取方法,数据由外设拷贝到对应的内存区域里
- 按键盘的时候,键盘是基于中断来进行驱动的、
- 这个过程像信号,都有编号
- 输入的数据分两类:1.正常输入,2.控制数据
ctrl c
- 一般情况下终止前台进程,但shell不会被终止
- 本质就是给前台进程发送 2 号信号
ctrl z
- 前台进程变成后台进程且暂停进程,这时候,shell会变成前台进程
ctrl \ :3号 默认终止进程
系统调用
signal
- 通过第一个参数找到地址,并将后面函数地址覆盖原来的函数(这也证明了确实每个进程都要一个函数指针数组,因为你要修改它啊)
- 信号可以被handler自定义捕捉
- 找到一张信号的函数指针表(PCB好像指向这个表),通过信号编号缩影到对应的函数
函数原型
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
参数
sigum
- 表示要捕捉的信号编号
handler
- 指向信号处理函数的指针,定义了在收到指定信号后应执行的处理方式
- 实际上就是在
task_struct
的信号处理表(action
数组)中替换相应信号编号对应的处理函数指针
返回值
- 成功时,返回信号的先前处理函数的指针
- 失败时,返回
SIG_ERR
并设置errno
以指出错误原因
实际作用就是替换掉原理的方法
#include <iostream>
#include <signal.h>
#include <unistd.h>void handler(int x)
{std::cout << x << "信号被写入" << std::endl;exit(0);
}
int main()
{signal(2, handler);while(true){std::cout << "正在运行" << std::endl;sleep(1);}
}
实验结果
- 在按ctrl c 时会打印 2信号被写入
- 说明ctrl c会触发2号信号
过程
- 键盘按下,通过硬件中断向CPU发送IRQ1信号,挂起正在处理的进程,先去处理键盘
- cpu将会执行中断号对应的中断向量表里的方法---到这处理器准备好了,数据下面这样来
- OS去读 键盘控制器 提供的 扫描码,给CPU处理,得到字符,放入输入缓冲区中(专门处理键盘的)
- OS读取输入缓冲中的数据,发现如果是组合键,OS会生成对应的信号
- OS将这个信号放入,前台进程的task_struct的pending对象中
- OS会定期将pending中的信号,给队友的进程
注意
- 并不是所有的信号函数都可以被替换,9号就不行
man 7 signal
方法2:使用系统调用发送信号
kill
int kill(pid_t pid, int sig);
- 可以向任何进程发送
系统调用原型
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
参数
pid
- 指的进程的pid
sig
- 发送的信号类型
返回值
- 成功返回0
- 失败返回 -1, 设置errno
raise
- 向调用它的进程发送一个信号
函数原型
#include <signal.h>int raise(int sig);
sig
- 发送的信号类型
返回值
- 成功时:返回
0
- 失败时:返回非零值
abort
- 是一个用于立即终止程序执行的 C 标准库函数
函数原型
#include <stdlib.h>void abort(void);
特点
- 给自己发送信号SIGABRT,允许被捕捉(这个信号)
- 就算这个信号被捕捉了,依旧可以终止进程
- 因为用户捕捉了和我函数没关系,它不通过acitve数组里的下标去找,就是封装好的
方法3:通过异常
解释:除0为什么会终止进程
- CPU内部状态寄存器的溢出标志位中计算结果出现异常,即溢出标志位被设置为1(只占一个bit位)
- 因为OS要管理软件和硬件
- 硬件出问题了,OS向引发该错误的进程发送特定的信号,来终止这个进程
- 这个过程和语言没关系,所有语言都会,因为这是进程出现异常了
- 其实就是进程的上下文有问题,CPU不允许加载
- CPU报告给OS,OS去解决对应的进程
注意
- CPU的寄存器属于CPU,但是寄存器里的内容不属于CPU,属于当前进程,即硬件上下文,所以状态标记寄存器的标志位为进程的上下文,所以只要将这个进程干掉,status标志位间接置0了(让其他进程覆盖)
- 把进程杀掉就是处理异常的方式之一,也是恢复了CPU信息的健康状态方法之一
将8号的处理方法改成一句打印,为什么会一直执行打印?
过程解释
- 进程运行,它的上下文加载到CPU内,状态标记寄存器表示错误
- 进程不继续执行
- 通知OS,这个进程有问题,给他发送8号信号
- 执行8号信号对应的方法,打一句话
- 但是这个进程没有被退出,过段时间上下文又被加载到CPU上
解释:空指针的解引用
MMU
- CPU内部有个MMU(集成的小的硬件电路)和页表一起找到物理内存地址,所以进来的是虚拟地址,出去的就是直接访问物理内存了
过程详解
- 页表中没有映射到0地址处
- MMU里面也有标志位,标志本次转化是否成功
- 一旦MMU出问题OS就知道了,又识别到硬件问题了
- 所以OS将引起这个错误的进程干掉
- 本质是虚拟到物理转换出现的硬件问题
小结
- 所以说vs中出现这些异常和vs没有关系,根本原因是被windows检查到了错误信息,进程被windows杀掉了,所以进程被杀,上下文就没了,错误信息就没了
-
语言的异常和系统的有区别,抛出异常的根本目的不是为了修正异常,而是为了在固定的地点打印出现的是什么异常,大部分异常是没法修正的;
-
抛出异常,处理异常的机制,更多是为了让执行流正常结束的
方法4:软件条件
概念
- 当程序执行中达到某种特定状态或满足某种条件时,程序内部逻辑通过调用系统的信号接口来触发信号
alarm
函数本质上是一个软件定时器,它依赖的是程序自身的计时机制,而不是外部硬件alarm
设置的倒计时结束后,程序会触发一个SIGALRM
信号,通知程序某个时间点或条件达成;这种基于时间的触发机制也是一种软件条件
alarm
- 函数是一个在 POSIX 标准中定义的系统调用,用于设置一个定时器,在指定的时间(秒)后触发 SIGALRM 信号(14)
- 在一个进程中,alarm 计时器实际上只能设置一个;因此,一个进程内只有一个 alarm 计时器是有效的
- 每次调用 alarm() 会返回上一次计时器的剩余时间(如果有的话),然后开始新的倒计时
函数原型
#include <unistd.h>unsigned int alarm(unsigned int seconds);
参数
seconds
- 设置一个倒计时,若为0,则取消当前进程的计时器
返回值
- 返回上一次调用
alarm()
的剩余秒数。如果没有设置过计时器或者上一个计时器已经完成,则返回0
测试代码
#include <iostream>
#include <unistd.h>
#include <signal.h>int n = 0;
void handler(int sig)
{n = alarm(0);std::cout << n << std::endl;}
int main()
{signal(14, handler);std::cout<<getpid()<<std::endl;alarm(100);n = alarm(8);while(1){std::cout << n << std::endl;sleep(1);}std::cout<<getpid()<<std::endl;std::cout<<getpid()<<std::endl;std::cout<<getpid()<<std::endl;}
结果
- 8个10,后序都是0