linux 信号
信号
信号是Linux系统提供的让用户(进程)给其他进程发送
异步信息
的一种方式
异步:信号的产生,是由别的进程产生的,进程收到之前,一直在做自己的事情,两个进程并发在跑
那为什么要有信号呢?
系统要求进程有随时响应外部信号的能力,随后做出反应
- 在没有发生时,进程已经知道发生时怎么处理了
- 进程能够认识信号,是设置了设别特定信号的方式
- 信号到来时,进程在处理更重要的事情,暂时不能处理到来的信号,所以进程必须暂时将到来的信号保存起来
- 信号到来,可以不立即处理,在合适的时候处理
- 信号的产生是随时产生的,无法准确预料,所以信号是异步发送的
信号概念
kill -l :查看所有的信号
其中没有0,32,33号信号,总共62个信号
,其中34-62是实时信号,我们不做学习,1-31是非实时信号,总共31个 。
其中每个信号都有一个编号和一个宏定义
,通过编号或宏定义都可以表示一个信号。
之前说过,信号到来时进程可能有更重要的事情,所以需要对信号进行保存,在Linux中,信号保存在进程的PCB中
,同时Linux使用位图
来存储信号,所以我们经常说的发送信号本质是操作系统往进程PCB中写信号,位图中的比特位的位置表示是哪一个信号,比特位的内容为1表示收到该信号,为0表示没有收到该信号
之前说过,ctrl + c
可以终止当前进程,其实ctrl + c
本质是向进程发送了2号信号
信号的处理方式:
1. 默认动作, 进程收到信号的默认动作大多都是终止进程
使用 man 7 signal 可以查看每个信号的默认动作
2. 忽略信号
3. 自定义处理信号(信号捕捉)
signal函数可以自定义信号处理方式,当收到指定信号时,执行用户所定义的信号处理函数
硬件中断
- 我们按下了
ctrl + c
,被解释为信号,进程收到了2号信号,默认终止进程。 - OS怎么知道键盘输入数据呢?
硬件中断
OS在开始时会注册一张表,表中有各种软硬件操作(函数指针
),表的名称为中断向量表(函数指针数组)
同时CPU有很多针脚,和外设物理连接,每个针脚有自己的编号,键盘按下按键会给特定的针脚发送高电平,触发硬件中断,CPU会识别到针脚的编号(中端号
),并保存在指定寄存器中,当中断发生时,CPU会暂停当前正在执行的程序,并保存当前进程的上下文数据,然后查找中断向量表,找到与中断号对应的处理函数的地址,回调方法。
通过上述操作,就可以读取键盘数据了,如果判定为字符,就写到键盘的文件缓冲区,如果判定为控制命令,比如ctrl + c
,就解释为信号,发送给进程,
信号的产生
-
kill命令
-
键盘产生
ctrl + c -> OS解释为SIGINT信号 ->发送给进程
ctrl + \ -> OS解释为SIGQUIT信号 -> 发送给进程
SIGINT的默认动作是终止进程,SIGQUIT的默认动作也是终止进程,那么Core和Trem有什么区别吗?
首先,云服务器默认将进程的Core退出进行了特殊处理,默认Core是关闭的
如何打开?使用ulimit命令允许Core文件最大为1024k
ulimit -c 1024
core dump:将进程再内存中的核心数据(与调试相关)转储到磁盘中形成core文件
为什么要有呢?一个进程异常退出,想通过core定位到进程为什么退出以及执行到哪里推出的
core文件可以帮助我们事后用调试器检查core文件以查清错误原因事后调试
-
系统调用
kill:给指定进程发送指定信号
raise:自己给自己发送信号
abort:给自己发送6号信号,引起进程终止
-
软件条件
在匿名管道中,如果读端不读并且关闭了读端,这时写端再往管道里写,就会收到13 GIGPIPE
信号,终止进程。
alarm函数可以设置一个闹钟,也就是告诉OSseconds之后向当前进程发送14 SIGARRM
信号,该信号默认终止当前进程。函数返回值为0或以前设定闹钟剩下的秒数。
设定闹钟是再OS内部,OS内可能有很多进程使用闹钟,所以OS要管理所有的闹钟,先描述,再组织,所以内核中一定要有描述闹钟属性的结构体,并用特定的数据结构组织起来,这里可以使用小根堆来对闹钟进行管理。这里的结构体,数据结构都属于软件条件哦
-
异常
段错误
:
除0错误
:
硬件异常被检测到并通知OS,然后OS向当前进程发送合适的信号
除0错误:当前进程执行了除0的指令,CPU的运算单元会发生异常,内核将这个异常解释为SIGFPE
信号发送给进程
段错误:当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV
信号发送给进程
综上,无论信号产生的方式有多少种,最终都是OS向进程写入信号
信号的保存
实际执行信号的处理动作称为信号的抵达
。
信号产生到抵达之间的状态称为信号未决(pending)
。
进程可以选择阻塞(block)
某个信号。
阻塞和屏蔽:
阻塞:一个信号阻塞,则该信号永远无法抵达处理
,除非解除阻塞
忽略:忽略是信号抵达的一种方式
block表:位图结构,比特位位置表示信号编号,比特位内容表示该信号是否被阻塞
pending表:位图结构,比特位的位置表示信号编号,比特位内容表示该信号是否未决
handler表:函数指针数组,每个函数指针的类型为void(*)(int),数组下标表示信号编号,数组内容表示该信号的抵达处理动作
因为pending表是位图结构,所以信号在抵达之前最多只计一次
pending表和block表可以使用相同的数据类型sigset_t存储,sigset_t称为信号集,阻塞信号集也叫做信号屏蔽字
信号集操作函数
sigprocmask:读取或更改进程的信号屏蔽字
how参数选项:
sigpending:读取当前进程的信号集
信号抵达时,一定会把对应的位图清0,先清0,再抵达
9,19号信号无法被阻塞,18号信号会被做特殊处理
信号的处理
之前说过:信号到来,可以不立即处理,在合适的时候处理,什么是合适的时候呢?
合适的时候:进程从内核态,切换会用户态时,信号会被检测并处理
内核态:OS的运行状态,权限级别高
用户态:普通程序的运行状态
32位平台下,进程地址空间中[0,3]GB属于用户空间,[3,4]GB属于内核空间
电脑开机时,第一个加载的软件是OS
OS只有一份,所以所有进程虚拟地址空间中的内核空间相同,用户空间不同,进程无论怎么切换,总能找到OS,本质就是通过进程的地址空间来访问即可
我们使用系统调用或者访问系统数据,其实还是在进程的地址空间中进行跳转的
用户可以随意访问用户空间,但用户不能直接访问内核空间,这时候,就必须能区分当前用户的运行模式:内核态和用户态
,设置CPU内寄存器指定标志位,更改CPU执行权限
sigaction结构体:
sa_handler:自定义动作
sa_sigaction:和实时信号相关,不做处理
sa_mask:执行signum的handler方法时,可再屏蔽其他信号
sa_flags:默认传0即可
sa_restorer:和实时信号相关,不做处理
当某个信号处理函数被调用时,内核会自动将当前信号加入进程的信号屏蔽字中,当信号处理函数返回时自动恢复原来的信号屏蔽字。如果设置了sa_mask,处理函数被调用时,内核会自动将sa_mask信号集中的信号加入进程的信号屏蔽字中,当信号处理函数返回时自动恢复原来的信号屏蔽字。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <assert.h>
using namespace std;
void handler(int signo)
{
cout << "get a signal, signo : " << signo << endl;
sigset_t s;
sigemptyset(&s);
while (true)
{
cout << "pending set : ";
sigpending(&s);
for (int i = 31; i >= 1; i--)
{
if(sigismember(&s, i))
cout << '1';
else
cout << '0';
}
cout << endl;
sleep(1);
}
}
int main()
{
cout << "pid : " << getpid() << endl;
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 5);
//信号抵达处理时,阻塞该信号
int n = sigaction(2, &act, &oact);
assert(n == 0);
while (true)
sleep(1);
return 0;
}
原文地址:https://blog.csdn.net/l2894/article/details/138244406
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!