Linux信号_信号的保存
我们知道向进程发送信号,进程并不是立即处理,而是等合适的时机进行处理。那么就需要保存信号。在信号的产生中说过信号保存在进程PCB里面的信号位图里,那信号位图到底是什么?
一.信号保存
我们先补充一些概念
1.阻塞 忽略概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。(只把信号保存,但还没有进行处理)
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态(保存信号,但不让处理),直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略(处理信号,但行为就是忽略)是在递达之后可选的一种处理动作。
2.block pending信号集 handler信号处理器表
我们之前说的信号位图就是下面三个表中的pending表,那这三个表有什么用呢?
1.pending位图,当前进程收到的信号列表。bit位的位置表示信号编号,1/0表示是否收到信号。
2.block位图,表示哪些信号正在被阻塞。bit位的位置表示信号编号,1/0表示该信号是否被阻塞(如果被阻塞,在pending表中对应信号即使为1,也不会对信号进行处理,等到阻塞消失,才会完成消息递达)。
3.handler信号处理表(函数指针数组),表示对应信号要进行的行为(可以是系统默认的,也可以是signal()函数自定义行为)。信号编号-1就是要执行动作函数指针的下标
sigset_t 信号集
我们知道block pending表是位图,但他们的类型是什么?是int吗?
其实它们的类型是一个结构体sigset_t
#include <signal.h> typedef struct { unsigned long __val[2]; // 通常是一个长度为 2 的 unsigned long 数组 } sigset_t;
未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态.
阻塞信号集(block)也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
3.信号集操作函数
我们知道block pending表是位图,但不建议直接用位操作来更改bit位。而是用信号集操作函数来实现。
增删查改
#include <signal.h>
int sigemptyset(sigset_t *set); //将set位图bit位全置为0
int sigfillset(sigset_t *set); //将set位图bit位全置为1
int sigaddset (sigset_t *set, int signo); //向信号集中添加一个信号,下标signo-1置1
int sigdelset(sigset_t *set, int signo); //从信号集中删除一个信号,下标signo-1置0
//这四个函数都是成功返回0,出错返回-1。
int sigismember(const sigset_t *set, int signo); //查找signo信号是否属于给定的信号集。
//如果信号在集合中,返回 1;如果不在集合中,返回 0;如果出错,返回 -1。
sigprocmask 更改block
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
1.int how 如何更改
2.set 指向需要操作的信号集。
3.oset 用于存储原来的信号屏蔽字(可选)。
成功时返回 0,失败时返回 -1。
eg.
sigset_t set, oldset;
sigemptyset(&set);//初始化信号集 bit位全置0
sigaddset(&set, SIGINT);
// 在内核PCB中block中 阻塞 SIGINT 信号
sigprocmask(SIG_BLOCK, &set, &oldset);
// 恢复屏蔽字
sigprocmask(SIG_SETMASK, &oldset, NULL);
sigpending 读取未决信号集(pending)
#include <signal.h>
int sigpending(sigset_t *set);
set: 指向
sigset_t
类型的变量,用于存储当前进程的未决信号集。调用成功后,该变量将包含当前进程未决信号的集合。如果调用成功,返回 0。出错,返回 -1,并将
errno
设置为具体的错误值。
补充:操作系统是如何运行的
1.硬件中断
当我们用键盘输入信息,操作系统怎么知道键盘要输入信息的?又是怎么知道其它外设有资源要处理呢?
1.中断触发。当外设准备好时,就会发起中断,每一个外设都对应一个中断号。(eg.键盘输入时会触发中断号1)
2.保存上下文。收到中断请求时,CPU会保护现场,暂停当前的程序执行,保存当前的执行状态(即程序计数器、寄存器等)。
3.查找中断向量。根据中断号,操作系统查找中断向量表,获取对应的中断处理程序地址,并执行对应方法。
4.恢复现场:中断处理完成后,恢复先前的执行状态(程序计数器、寄存器等),并继续执行被中断的程序。
2.时钟中断
现在我们知道了每当外设有资源要处理时,会通过中断的方式让CPU进行处理。但这和操作系统运行有什么关系呢?
其实有一个硬件时钟源,它会每隔很短的时间向操作系统发送中断,所以操作系统就会根据它的中断号来查找中断向量表,执行它对应的方法。但时钟源对应的中断服务就是进程调度。这样操作系统,就可以在硬件时钟的推动下,自动调度了。
因为时钟源会频繁向系统发送中断,这样会占用大量中断控制器资源,降低响应速度。所以一般把时钟源集成到CPU内部,减少中断传播延迟。
时钟源发送中断,引起的中断服务:进程调度 并不意味着要进行进程切换。
比如说执行一个进程的时间片1s int count=1000,时钟源每隔1微秒中断一次,count--。当count==0时就意味着时间片耗尽,要切换下一个进程。
3.软件中断
上面都是因为硬件触发的中断,有没有因为软件来触发中断的?
eg.1.系统调用 为了让操作系统支持系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),让CPU在内部触发中断逻辑。
2.缺页中断 /0 野指针操作
1.系统调用
int 0x80 是一种在 x86 架构(尤其是 32 位系统)中触发软件中断的指令,常用于执行 系统调用(system call)。
int 触发一个中断,后面加中断号
0x80 作为中断号,在 32 位 x86 系统中约定为触发 系统调用的入口。
1.int 0x80,触发软件中断。系统根据后面0x80中断号,在中断向量表中找到对应的处理程序。
2.再根据系统调用号作为下标查找系统调用表中的对应函数指针。
3.返回函数执行结果。
系统调用号哪来的?寄存器EAX中
在系统调用的过程中,把要调用的系统调用号写入寄存器EAX中
(系统调用参数一般也是通过寄存器传的 返回值通常存放在寄存器中,如 EAX(32位架构)或 RAX(64位架构))
所以系统调用也是通过中断完成的
由此看来,Linux内核提供的系统调用接口,不是C语言,而是系统调用号+传递参数 返回值寄存器 +int 0x80 / syscall实现的。
我们平常用的都是C语言封装的调用
2.缺页中断 /0 野指针操作
除了系统调用会触发软中断,像缺页中断 /0 野指针等异常操作也会触发软中断。
为什么说/0 访问野指针,系统能知道。就是因为触发了软中断,让操作系统找中断向量表,找到对应的执行程序。
1.CPU内部触发的软中断,int 0x80 syscall ,我们叫做陷阱。
2./0 野指针等 我们叫做异常