【Linux】信号
目录
信号的概念
信号的产生
键盘组合键
指令
系统调用
kill
raise
abort
硬件异常
软件条件
core dump
信号的发送
信号的保存
概念
三张表
接口
sigset_t
信号集操作函数
sigprocmask
sigpending
代码示例
信号的捕捉处理
内核态与用户态
内核空间
捕捉信号
signal
sigaction
可重入函数
volatile
SIGCHLD信号
信号的概念
前台进程
ctrl c 是2号信号
给信号设置自定义动作
键盘输入数据完成后,会通过硬件中断提醒CPU,操作系统提取对应中断号去中断向量表执行对应方法。
异步
信号的产生
键盘组合键
键盘输入完成后,给CPU对应针脚发送硬件中断,操作系统会根据中断号在中断向量表中找到对应中断方法并执行,把键盘上的数据读到内核里,识别到是组合键,就把组合键转成对应信号发送给目标进程。
指令
系统调用
kill
第一个参数是进程,第二个参数是信号
raise
谁调用你,你就对谁发送信号
abort
给自己发送6号信号。
正常来说,如果你对6号信号进行了自定义行为,那么当你接收到6号信号时,你就会做自定义行为。
但这个函数不是,它会先执行自定义行为然后中止进程。
硬件异常
除0错误会收到8号信号
野指针异常会收到11号信号
当出现除0,野指针这些错误时操作系统会给进程发送信号,这些信号的默认动作是中止进程。
操作系统怎么知道有除0错误的?
因为CPU有状态寄存器会识别并告诉操作系统
操作系统怎么知道野指针错误的?
MMU是内存管理单位,集成在CPU中。
CPU读到的虚拟地址结合页表和MMU去转化物理地址,野指针就是地址转化失败,CPU有个寄存器会保存转化失败的地址,转化失败会出现硬件报错被操作系统识别。
操作系统怎么知道你是除0错误还是野指针错误?
因为是不同的寄存器,不同的硬件报错。
上述的异常在对信号进行捕捉时会出现死循环,是因为硬件方便一直报错,无论被调度多少次,每当你带着上下文回来,操作系统就会检测到你的硬件报错。
软件条件
管道写端一直在写,但读端不读了,系统就会给写端进程发送信号关闭进程。
闹钟
返回值是上一个闹钟剩余时间。
core dump
core dump标志保存你的中止方式,是Core还是Term,Term是0,Core是1。
当前目录下生成了core文件。
打开core dump功能并且是Core行为的信号就会触发核心转储。
为什么要进行核心转储?
调试可以直接把core文件导进来,可以直接定位到出错原因。
云服务器的Core功能默认关闭,因为自动重启功能可能会导致磁盘被core文件打满。
设置大小开启
信号的发送
本质是写信号,在PCB中的位图管理。
为什么必须是操作系统发?
信号的保存
概念
三张表
pending表保存着接收的信号,这些信号还没执行对应方法。
handler表是函数指针数组,保存着信号默认方法,如果用户要自定义方法,就是用signal函数把用户提供的方法替换进来。
block表记录对应信号是否被屏蔽,1是屏蔽。
默认处理动作其实是整数,只不过被强转成函数指针类型。
接口
sigset_t
这是信号集类型,本质是位图,接下来进行信号位图交流时都要用这种类型。
信号集操作函数
sigprocmask
第一个参数
第二个参数是我们设置一个信号集去和系统的信号集交互。
第三个参数是输出型参数,我们找个地方存放老的信号集。
sigpending
输出型参数,把pending表带出来
代码示例
//测试:练习使用操作信号集的接口,以及操作block表和pending表的接口
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
//捕捉后的自定义动作
void MyHandler(int signo)
{
cout << "get signal: " << signo << endl;
}
void PrintPending(const sigset_t& pset)
{
for(int i=31; i>=1; i--)
{
if(sigismember(&pset, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
//对测试信号进行自定义捕捉
signal(2, MyHandler);
//构建信号集
sigset_t set, oset;
sigemptyset(&set); //全0
sigemptyset(&oset);
sigaddset(&set, 2); //对2号信号置1
//通过sigprocmask把信号集设置进内核
sigprocmask(SIG_BLOCK, &set, &oset);
//通过sigpending观察pending表
int cnt = 0;
while(1)
{
sigset_t pset;
sigemptyset(&pset);
sigpending(&pset); //获取pending表
PrintPending(pset); //打印pending表
sleep(1);
if(cnt == 10) //解除阻塞
{
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
cnt++;
}
return 0;
}
信号的捕捉处理
内核态与用户态
信号是什么时候被处理的?
内核态与用户态切换
操作系统怎么判断当前是内核态还是用户态?
CPU有一个ecs寄存器,其中有两个比特位来记录,00(0)是内核态,11(3)是用户态,使用int80陷入内核就可以把3变成0,该进程就变成内核态可以访问内核空间了。
当进程遇到系统调用等原因,会从用户态变成内核态完成对应任务。
完成任务后会变回用户态,在变回之前会检测信号。
先看pending表,再看block表,如果有信号且未阻塞再看处理方法,默认就直接中止进程,忽略就直接返回用户态。
如果是用户自定义方法,就暂时变成用户态完成对应任务,然后变回内核态,然后按照流程处理完信号后就变回用户态。
进程是会被调度的,因为时间片,所以会周期性的态变换,尽管没有系统调用之类的。
内核空间
时钟中断
操作系统是一个死循环,有一个硬件是时钟芯片,每隔一段时间就会给系统发一个中断,系统就会检查当前进程的时间片使用情况,均匀的持续的推着操作系统运作。
捕捉信号
signal
sigaction
第一个参数是信号编号
结构体第一个字段是处理方法
sa_mask字段是设置你额外还想屏蔽的信号,除了系统自动屏蔽当前正在处理的信号。
可重入函数
函数进行到一半,进程切换了,由于态转换又触发了信号的处理。
volatile
main函数中,CPU对flag变量并没有修改行为只有检测行为,可能会导致flag变量的值直接存到寄存器中,以后CPU就不访问内存了直接访问寄存器。
-O是选择优化模式
volatile防止被优化
SIGCHLD信号
如果多个同时退出就用循环等待,因为有部分退出的情况,所以采用非阻塞等待,防止卡在那里。
父进程中,把SIGCHLD信号的处理动作设置成忽略,可以让子进程退出时不会产生僵尸进程,父进程也不关心子进程退出情况。
手册的忽略和显式忽略不一样。