目录
产生信号
引入 -- 信号怎样被管理
我们先不去想是信号如何产生,而是来思考一个问题
- 我们可以通过多种方式来产生信号,但是进程不一定会立即处理他们
- 所以为了不遗漏这些产生的信号,就需要对应的进程去保存他们收到的信号
- 一旦这些需要被保存的信号数量增加,os势必要去管理他们
- 那么就是 -- 先描述,再组织
数据结构 -- 位图
- 再想想信号的特点,每个信号都有自己的编号,且常用的就31个
- 而且信号对应的结果只有两种 -- 要么收到了该信号,要么没收到(可以对应1 / 0)
- 是不是和位图的原理很像?
- 所以我们可以使用位图来作为信号的结构
- 又因为发送信号就是发送给某个特定的进程的
- 所以,这个结构就在进程的pcb里
产生信号的本质
- 从上面的分析我们已经知道,信号结构是存储在内核数据结构中的(pcb由os管理)
- 也就代表 -- 只有os才能改变信号的状态
- 因此,我们无论通过什么手段来产生信号,实际上最终都是由os将信号的改变写入到信号位图结构中
接下来,就开始介绍产生信号的多种方法噜
键盘输入
介绍
- 有些信号可以由用户在终端上输入特定的键序列来产生
- 例如Ctrl+C(SIGINT)表示终止进程,Ctrl+Z(SIGTSTP)表示暂停进程
- 像这样的信号只能发送给前台进程
原理
单个键
- 当我们在键盘上敲击一个键时,该键的电路将产生一个按键事件,通常为电气信号或电平变化
- 键盘控制器会将按键事件转换为对应的扫描码
- 每个按键都有一个唯一的扫描码
- 该扫描码标识了按下的键是哪个键,并且还包括信息是否是按下或释放动作
- 然后通过中断机制,将扫描码发送给cpu
- 当cpu收到键盘中断后,它会执行预定义的中断处理程序,读取和解释键盘的扫描码,并将其转换为对应的字符或功能
组合键
- 既然cpu可以处理一个键的输入,自然也就可以处理组合键
- 当我们输入特定的组合键,由于其自身的设置,某个组合键 就代表 会触发操作系统产生预定义的信号
- 所以,信号就可以通过键盘输入来产生
- os再通过查找当前进程列表,找到前台正在运行的进程,向其写入刚刚产生的信号(在该进程pcb内部对应的位图结构中)
中断
是一种硬件机制,用于处理外部设备的事件或异常情况
它会打断cpu当前的执行,使其立即处理与中断相关的处理程序
分类(按来源)
硬中断
由硬件设备发起的中断,例如计时器、磁盘控制器、网络接口等
时钟中断
- 时钟中断是由系统时钟产生的一种硬件中断
- 这种中断周期性地发生,以固定的时间间隔触发,通常以毫秒为单位,由系统时钟频率决定的,常见的频率是1000 Hz,即每秒触发1000次时钟中断
- 用于维护系统时间、进程调度以及执行与时间有关的操作
软中断
由软件生成的中断,通常通过系统调用或异常触发,例如系统调用、陷阱指令等
eg:信号就属于软中断
系统事件
操作系统在发生某些特定事件时,会向进程发送相应的信号
软件条件
由软件条件不满足 / 触发条件时,os会向当前使用该软件的进程发送信号
管道文件问题
前面有说到,管道文件的一端关闭后,另一端也会随之关闭:
- 其中,当读端关闭时,写端会收到SIGPIPE信号,从而退出
- 这是因为当读端关闭时,写入操作已经没有意义了,那么os自然需要直接结束io过程,也就是给写端发送信号
alarm函数
函数原型
- 调用alarm函数可以设定一个闹钟
- 也就是告诉os在seconds秒之后,给当前进程发SIGALRM信号
- 该信号的默认处理动作是终止当前进程
- 闹钟触发后,会自动移除
应用 -- 定时器
思路
- 闹钟触发后,会让os给当前进程发送信号,也就会调用SIGALRM信号对应的信号处理方法
- 那么我们可以通过自定义函数,让他每次触发闹钟后,继续设定一个闹钟
- 这样就可以实现每过一定时间,都会自动调用函数,我们可以在该函数中,调用我们所需的功能函数
- 也就是定时处理了
代码
vector<function<void()>> callbacks; void log(){ cout<<"loging"<<endl; } void show_user(){ if(fork()==0){ execl("/usr/bin/who","who",nullptr); exit(1); } wait(nullptr); } void func_test(int signum){ signum=0; for(auto& it:callbacks){ it(); } alarm(1); } void test5(){ callbacks.push_back(log); callbacks.push_back(show_user); alarm(1); signal(SIGALRM,func_test); while(1){ ; } }
硬件异常
异常以某种方式被硬件检测到并通知os,然后os向当前进程发送信号
但进程不一定会退出!!!
除0
引入
之前我们只知道,/0后会使程序异常退出,但不知道为什么:
void test2(){ int a=1; a/=0; while(1){ sleep(1); } }
但现在我们可以知道,程序终止和信号息息相关,而程序异常终止后打印出的这句话,正是8号信号对应的错误信息:
思考 -- 信号产生原理
但是,os怎么知道哪个进程出现异常的?
- 自然是某个元件计算的过程中,发现异常,然后报告给了os
- os定位当前正在运行的进程,然后给该进程发送了信号
谁计算?
- 当然是专门计算的元件 -- cpu的运算器(硬件嗷,也就对应了是硬件发现了异常)
如何报告?
- 可以参考os发送信号的原理 -- pcb的信号位图中,哪一位bit是1,就发送对应的信号给该进程
- 报告也是一样的道理,会有专门的状态位图,硬件修改里面的bit位,来表示当前运算出现异常
- 而状态位图是由硬件的状态寄存器实现(几乎所有外设都有寄存器),其中一个bit位就代表当前运算是否溢出
- os通过读取寄存器中的数据,来获取状态
- 一旦某位为1,就会让os知道当前运行的进程出现了某个异常
如何定位当前运行的进程?
- os中会有该进程的上下文数据,其中有一个指针current指向当前运行进程的pcb
然后就可以发送信号了,还是一样的,os向pcb中的信号位图写入
死循环问题
引入
当我们修改上面的代码,执行自定义的处理方法:
void func(int signum) { cout << "im " << getpid() << "i got a signal : " << signum << endl; } void test2(){ signal(SIGFPE,func); int a=1; a/=0; while(1){ sleep(1); } }
会发现我们的进程在不断收到8号信号,为什么呢?
原理
- 是因为我们自定义的处理函数中并没有终止进程
- 也就是说,进程的上下文依然存在,会被os不断进行时间片切换
- 硬件中的状态寄存器也是进程的上下文
- 随着每一次该进程的上下文被加载,os就会检测到溢出状态位是1,就会给进程发送8号信号
野指针
思考
还是和前面一样,出现野指针问题后,进程有时会退出
- 原因肯定和上面差不多,都是进程收到了os的信号
- 那信号的源头是谁呢?
联系我们已经学过的知识:
野指针是什么?
- 野指针也就是我们使用了某个地址访问了不该访问的空间
- 而这个地址是虚拟地址,访问物理内存是需要经过页表映射的
如何映射?
- 映射的工作是由硬件和软件共同来完成的
- 承担这个任务的硬件部分叫做存储管理单元MMU,软件部分是操作系统的内存管理模块
- 并且为了能够快速完成虚拟->物理的转换,页表的格式直接灌给了硬件
- 所以查询页表等操作,都是以二进制形式进行的
信号的源头?
- 当我们进行非法访问时,mmu在进行地址转换时发现了非法操作,它就会报错
- 和进行除0操作时,cpu会报错的原理一样,mmu内部也有寄存器
- 所以!对应的状态寄存器中的某个bit位就被赋为1
- 那么os通过读取这个寄存器,就可以知道发生了异常,并且也能拿到当前进程的pcb
- 也就可以发送信号了
函数调用
kill()系统调用
函数原型
pid
sig
要发送的信号编号
它可以是预定义的信号宏,也可以是用户自定义的信号编号
模拟实现
#include <iostream> #include <unistd.h> #include <signal.h> #include <string> #include <stdlib.h> using namespace std; void usage(string arr) { cout << arr << " " << "illegal"; } int main(int argc, char *argv[]) // 模拟kill命令 -- ./mykill 2 pid { if (argc != 3) { usage(argv[0]); cout << endl; exit(0); } int signum = atoi(argv[1]); int pid = atoi(argv[2]); kill(pid, signum); return 0; }
示例
raise()
它允许进程在运行时发送信号给自己,触发信号处理操作
abort()
用于引发程序异常终止(和exit()类似)
它在程序执行时发送一个信号SIGABRT:
并且会发生核心转储(6号信号):
命令行 -- kill指令
(实际上底层还是调用了上面的系统接口)
它不仅可以终止(杀死)进程,还可以向进程发送其他信号,以便进行不同的处理
默认
- 如果没有指定信号,默认情况下会向目标进程发送SIGTERM信号,这会请求目标进程正常退出
指定信号
- 指定发送的信号 ( -s选项 ) (信号可以使用名称 / 编号):
- 通常,只有root用户或进程的所有者(或者有特定权限的用户)可以向其他进程发送信号
- 不是所有的信号都可以通过kill指令发送给进程,有些信号可能只能由内核或其他特定进程发送