文章目录
- 什么是进程信号?
- 用户层产生信号的方式有哪些?
- 信号在内核的存在形式
- 认识信号的一些接口
- 信号处理的执行流程
- 理解用户态和内核态
- 信号处理流程
什么是进程信号?
进程信号是一种事件异步通知机制,属于软件中断(因为信号产生时是异步的,当信号产生时,对应的进程可能正在做更重要的事情,我们的进程可以暂时不处理这个信号)
在linux中使用kill -l
可以查看所有信号,如下(共有62种):
inux 内核支持 62 种不同的信号,这些信号都有一个名字,这些名字都以三个字符 SIG 开头。在头文件siganl.h中你能够,这些信号都被定义为正整数,称为信息编号。其中,编号 1 到 31 的信号称为普通信号,编号 34 到 64 的信号称为实时信号,实时信号对处理的要求比较高。
普通信号和实时信号的关系就像分时操作系统和实时操作系统的关系类似,分时操作系统是基于时间片轮转调度的,而实时操作系统要求要有严格的时序,可以认为是一个队列。将一个任务放入该队列中,那么操作系统就尽量快地将该任务处理完。日常生活中使用最多的就是分时操作系统,而实时操作系统常见于特殊的行业,如军工领域和自动驾驶领域等等。
分时系统
所谓分时系统,即一台计算机与多个终端设备连接,每个用户通过终端向系统发出命令,请求系统为其完成某项工作。系统根据用户的请求完成指定的任务,并把执行结果返回。
分时系统思想
- 采用时间片轮转的方法,同时为多终端用户服务,对每个用户能保证足够快的响应时间,并提供交互会话的功能
- 时间片:将CPU的时间划分成若干个片段,称为时间片,操作系统以时间片为单位,轮流为每个终端用户服务
- 设计目标:对用户的请求及时响应,并在可能条件下尽量提高系统资源的利用率
实时系统
所谓“实时”,是指能够及时响应随机发生的外部事件并对事件做出快速处理的一种能力。
“外部事件”,是指与计算机相连接的设备向计算机发出的各种服务请求
实时操作系统是能对来自外部的请求和信号在限定的时间范围内做出及时响应的操作系统
信号的内核数据结构(Linux2.6环境下)
前面说当信号产生时,对应的进程可能正在做更重要的事情,我们的进程可以暂时不处理这个信号。
但是我们的进程必须记住这个信号,那么这个信号保存在哪里呢?
保存在我们进程的PCB里,也就是task_struct里
在Linux中,task_struct结构体中表示信号的字段是signal。该字段是一个指向signal_struct结构体的指针,它包含了当前进程所设置的所有信号的信息,包括信号的处理方式、挂起的信号等。这些信息被用来决定何时向进程发送信号以及如何处理这些信号。signal_struct结构体中还有其他的字段,例如sigaction、sigmask等,它们用于存储信号的处理方式和信号掩码等信息。
用户层产生信号的方式有哪些?
- 用户可以通过键盘输入kill命令对指定的进程进行发送信号(但是键盘只是产生信号,OS才是写入(发送信号)
- 可以通过系统接口完成对进程发送信号的(比如signal,kill等等,后面对这些接口再详细说明)
- 可以通过软件条件发送信号(alarm函数, 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程)
- 通过硬件异常产生信号(比如除0错误)(硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除 以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。)
信号在内核的存在形式
信号的一些概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
这里解释一下上面的示意图,每个进程PCB也就是task_struct,会记录该进程的信号情况,类似上图;我们知道信号有62种,在图中可以看出每个信号都有两个标志位分别表示阻塞(block)和未决(pending)还有一个函数指针表示当该信号发送后,就会执行这个指向的函数。
block和pending都是一个位图,在上图中SIGINT信号产生过,但是正在被阻塞,所以暂时不能递达,所以pending是1。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
认识信号的一些接口
signal函数
sighandler_t signal(int signum, sighandler_t handler);
功能:为signum信号,注册一个回调方法,如果有信号发送给当前进程,就会执行handler方法
什么是sigset_t
sigset_t我们称为信号集,其实就是一个位图,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
信号集操作接口
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,然后把所有的信号加入到此信号集里。
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
- 如果oset是非空指针,set是空指针,则读取进程的当前信号屏蔽字通过oset参数传出
- 如果set是非空指针,oset是空指针,则更改进程的信号屏蔽字。
- 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽。
- how参数有几个取值
SIG_BLOCK :参数set包含了我们希望添加到当前信号的屏蔽字的信号
SIG_UNBLOCK:参数set包含了我们希望从当前信号屏蔽字中解除阻塞的信号。
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值
sigpending
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程
序如下:
代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,获取到一个信号:" << signo << endl;
exit(0);
}
static void showPending(sigset_t *pendings)
{
for(int sig = 1; sig <= 31;sig++)
{
if(sigismember(pendings,sig))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
//给2号信号注册一个方法
signal(2,handler);
//添加2号信号到信号屏蔽字中
sigset_t bsig,obsig;
sigemptyset(&bsig);
sigemptyset(&obsig);
sigaddset(&bsig,2);
sigprocmask(SIG_SETMASK,&bsig,&obsig);
//获取当前进程pending信号集
sigset_t pendings;
int cnt = 0;
while(true)
{
sigemptyset(&pendings);
if(sigpending(&pendings) == 0)
{
//打印当前信号集
showPending(&pendings);
}
sleep(1);
cnt++;
if(cnt == 10)
{
cout << "解除对所有信号的block" << endl;
sigprocmask(SIG_SETMASK,&obsig,nullptr);
}
}
return 0;
}
信号处理的执行流程
理解用户态和内核态
在介绍信号处理的执行流程之前,我们先来了解什么是用户态和内核态
在图中我们可以看出,进程地址空间分为两部分,一部分是用户空间,一部分是内核空间;页表也分为两种一种是用户级页表,一种是内核级页表。
用户级页表:每一个进程,都有一份,而且大家的用户级页表都是不一样的!
内核级页表:所有进程共享的只有一份,前提是你有权利访问
那到底什么是内核态,什么是用户态呢?
用户态是指应用程序运行时所处的状态,而内核态是指操作系统内核运行时所处的状态。
当应用程序需要访问操作系统提供的资源或执行一些特权操作时,需要切换到内核态,由操作系统内核来完成相应的操作。在内核态下,应用程序无法直接访问系统资源和硬件设备,需要通过操作系统提供的接口来进行操作。
相比之下,用户态下的应用程序只能访问自己的内存空间和一些受限的资源,不能直接访问操作系统的资源和硬件设备,也不能执行特权操作。用户态和内核态之间的切换需要一定的时间和资源,因此应该尽量减少切换的次数,以提高系统的性能和稳定性。
简单来说,内核态可以访问所有的代码和数据,具有更高的权限;而用户态只能访问自己的
进程如果是用户态——只能访问用户级页表(只能访问用户的代码和数据)
进程如果是内核态——访问内核级和用户级页表(可以访问所有代码和数据)
那我们的进程什么时候会进入内核态呢?
- 调用系统接口的时候(此时就会切换内核级页表,通过页表映射到物理内存存放内核代码和数据的地方,执行系统接口的代码)
- 时间片到了,进程间的切换(要执行调度代码等等)
怎么知道当前进程是用户态的还是内核态的呢?
CPU内部有对应的状态寄存器CR3,有比特位标识当前进程的状态;0表示内核态,3表示用户态
信号处理流程
理解了什么是内核态,什么是用户态,我们就可以来理解信号的执行流程了