🪐🪐🪐欢迎来到程序员餐厅💫💫💫
主厨的主页:Chef‘s blog
所属专栏:青果大战linux
信号的保存
下面的概念务必记住
-
实际执行信号的处理动作称为信号递达(Delivery)
-
信号从产生到递达之间的状态,称为信号未决(Pending)。
-
进程可以选择阻塞 (Block )某个信号。
-
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
-
阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号的内核描述
如图所示,我们的task_struct中保存了三张表,block表,pend表,handler表
pending表
它本质就是一个位图
handler表
它本质就是一个函数指针数组
typedef void (*sighandler_t)(int);
我们之前通过signal函数给指定信号设置自定义方法,本质就是把signal函数的sighandler参数写入handler表,所以我们signal调用一次,就可以永久修改一个进程的信号处理方法
block表
它本质也是一个位图
被阻塞的信号,即是被写入pending表,也不会被处理,直到阻塞被解除才会去处理它
有了这三张表,我们就可以直到一个信号有没有被传进来,他有没有被阻塞,如果没被阻塞那他又该如何处理,如此一来我们的进程就可以识别信号了,而三张表的实现,当然是当初写OS的程序员通过代码实现的,因此我们说进程认识信号,是程序员内置的结果。
内核数据结构
我们需要先了解一些类型,如下所示,不懂的看注释
struct task_struct {
struct sighand_struct *sighand;//hand表
sigset_t blocked//block表
struct sigpending pending;//pend表
...
}
先看hand表的结构体类型
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG]; // _NSIG是个宏,为64
spinlock_t siglock;
};
//上面的结构体中包含了K_sigaction这个结构体,它的定义就在下面
struct k_sigaction {
struct __new_sigaction sa;
void __user *ka_restorer;
};
//上面的结构体中包含了__new_sigaction这个结构体,它的定义就在下面
struct __new_sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void); /* Not used by Linux/SPARC */
__new_sigset_t sa_mask;
};
//上面的结构体中包含了__sighandler_t这个类型,它的定义就在下面
typedef void (*__sighandler_t)(int);//即函数指针
再看pend表
struct sigpending {
struct list_head list;//表示这是个链表结构
sigset_t signal;//它的重点是这个sigset_t类型
};
显然,pend表的内容是sigset_t类型的变量存储的,block表也是,sigset_t被称为信号集,这个类型定义如下
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;//注意这前面有两个下划线
typedef __sigset_t sigset_t;//这次typedef后,前面没有下划线了
这个sigset_t类型本质就是个unsigned long类型的数组,这个数组就是用来当位图用的
#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中的所有比特位变为0
-
sigfillset:使信号集set中的所有比特位变为1
-
sigaddset:使信号集set的第signum位变为1
-
sigdelset:使信号集set的第signum位变为0
-
sigismember:检测信号集set的第signum位是0还是1
- 在使用sigset_ t类型的变量前,⼀定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
- 这四个函数都是成功返回0,出错返回-1。
- sigismember是⼀个布尔函数,用于判断⼀个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
how
参数- 这个参数决定了如何修改信号屏蔽字,它有以下几种取值:
SIG_BLOCK
:将set
参数所指向的信号集添加到当前信号屏蔽字中。SIG_UNBLOCK
:从当前信号屏蔽字中移除set
参数所指向的信号集中的信号。SIG_SETMASK
:将当前信号屏蔽字设置为set
参数所指向的信号集。这会完全替换当前的信号屏蔽状态。
- 这个参数决定了如何修改信号屏蔽字,它有以下几种取值:
set
参数- 这是一个指向
sigset_t
类型的信号集的指针。
- 这是一个指向
oldset
参数- 这是一个指向
sigset_t
类型信号集的指针,用于保存本次修改前的信号屏蔽字状态。如果不需要保存旧状态,可以将此参数设置为NULL
。
- 这是一个指向
9号信号(
SIGKIILL)
和19号信号(SIGSTOP
)不能block
sigpending
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
为什么有可以修改block表的函数,但是没有修改pend表的函数
因为不需要,我们上节课学习的信号产生的方式都是在修改pend,有他们就够了
实操函数
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
int main(){
//对二号信号进行屏蔽
sigset_t ne;
sigset_t old;
sigemptyset(&ne);
sigemptyset(&old);
sigaddset(&ne,2);//此时还没有把2号加到内核的block中,只是放到了栈上的ne中了
sigprocmask(SIG_BLOCK,&ne,&old);//现在加到内核了
int n=7;
while(true){
n--;
if(n==0)
sigprocmask(SIG_SETMASK,&old,nullptr);
sigset_t pe;
sigemptyset(&pe);
sigpending(&pe);
for(int i=31;i;i--)
std::cout<<sigismember(&pe,i);
std::cout<<std::endl;
sleep(1);
}
}
我们输入ctrl+c即二号命令,但是进程没有立即终止,是因为我们把二号信号写进block表了
7秒之后,block表置零,二号信号不再被block于是被处理了,所以进程终止。
硬件中断
外设资源是否准备好,不能让OS去轮询检测,因为外设非常多,效率会很低,太浪费资源了,这里采用的解决方案就是硬件中断。
硬件中断是指由计算机硬件设备(如键盘、鼠标、硬盘、网卡等)发出的信号,用于通知 CPU暂停当前正在执行的程序,转而处理与该硬件设备相关的特定事件。这使得 CPU 能够及时响应硬件设备的请求或状态变化,从而实现设备与 CPU 之间的高效交互。这些东西的具体实现是依靠硬件电路实现的,我们不用管。
但是外设实在是太多了,所以不能直接连接到cpu的针脚上,于是就设计了一个中断控制器来负责连接他们,每个设备都被编好了中断号,中断控制器通过中断号知晓是哪个设备发来的中断,然后会把该信息发给CPU。CPU会告知OS,OS会根据中断号去中断向量表执行对应的方法(例如去键盘获取信息)。
这里的通知CPU就是其实向着CPU的特定针脚发送高电平信号
程序员在写OS时,就提前给每一种设备准备好处理中断的方案,这些方案本质就是函数,他们汇聚在一起就是一个中断向量表,我们把它当作一个函数指针数组即可。访问该数组元素的下标就是中断号。
当然,如果硬件中断信息告知给CPU时,CPU要开始执行中断处理方法,就不能继续跑之前的进程,为了保证之后还能正常运行它,需要进行CPU现场保护
CPU 现场保护是指在计算机系统发生中断或异常情况时,CPU 暂停当前正在执行的程序,为了能够在后续恢复该程序的执行,将当前程序执行的状态信息进行保存的过程。这些信息包括程序计数器(PC)的值、通用寄存器的内容、状态寄存器(也称为程序状态字 PSW)的内容等。这个和进程切换时的上下文数据保存有些像,但请注意这俩不是一个东西。
时钟中断
进程是在OS在管理控制下运行的,那么OS又是被谁指挥的呢?
时钟源是一个定期发送触发硬件中断信号的硬件,他的中断号对应的中断处理方式就是进程调度
所以OS才可以不断的调度进程,为了提高效率,这个时钟源已经被集成到CPU内部了,他的中断发送不依赖中断控制器,是直接发送给CPU的。我们计算机中有个参数叫做主频,表示时钟源一秒发送多少中断单位一般是GHZ,所以主频快的话,OS响应速度就比较快,效率就高了
于是OS就可以开摆了,OS所谓的调度进程,就是时钟源定时发送中断,然后OS去根据设置好的处理方法处理中断;OS要和外设IO,就是等硬件发送中断,然后它根据中断向量表去调用对应的函数。
无端联想,OS就像一个商店老板,他的店里什么都有,当用户需要一样东西时,OS虽然不知道这个东西是干嘛的,是怎么造出来的,但是他知道他的店里有,而且确切的直到放到哪里了,于是它只需要去把这个东西拿给用户就好。
因此操作系统再初始化完需要的资源后,就直接进入死循环,等待中断信号的到来,然后执行对应的方法即可。
时间片
时钟中断是定时发送的,我们假设它一纳米发送一次,
我们可以给进程task_struct设置一个变量——时间片
int time_piece=1000;
那么时间片的大小就是1000*1纳秒
每次时钟源发送信号,OS进行调度都会把当前进程的时间片减一
当减到零,就进行进程切换,否则继续执行该进程
软中断
有没有可能,上面这一套中断的流程,靠软件也可以实现呢
为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内
部触发上面的中断逻辑。
把这些汇编写在系统接口函数里,不就可以让OS支持系统调用了吗
这里我们以int指令为例,他的中断号是ox80
它对应的中断向量表中的元素也是一个函数我们暂时称他为func(),我们的OS要是实现很多系统接口,这些接口会放到一个数组中,被称为系统调用表,所以要使用某个系统调用,只要使用他在表中下标即可。现在我们要使用系统调用,只需要把系统调用号传到func()中即可。
第一个问题
用户层怎么把系统调用号给操作系统?
把系统调用号写进CPU寄存器,之后查看寄存器即可,只要提前设计好OS去哪个寄存器查看即可,系统接口的蚕食也是依靠寄存器传给OS的。
第二个问题
操作系统怎么把返回值给用户?
把最后return的值放到一个寄存器中,再把该值move到用户用于接受返回值的变量中。
系统调用的过程,其实就是先int 0x80、syscall触发软中断,并且把系统调用号(以及形参)写到一个寄存器里,然后CPU告知OS,OS根据寄存器的值去系统调用表执行对应的函数。
现在我们知道了使用系统接口,不一定就要用系统函数,你可以先通过int汇编代码触发ox80号中断,然后把要用的系统调用接口的编号和参数通过汇编move到指定的寄存器,最后OS会找到要执行的函数,并且运行它。
事实上,这才是OS提供的真正的系统接口,而不是我们用的那些c语言函数(write、read、fork这些)。我们所用的fork这些接口,是c语言封装过的函数,毕竟让用户用真的系统实在是太难了。
这样一来既方便了用户,也保护了OS,毕竟如果你用int(0x80),万一传入的系统调用号非法呢,或者你用了一些别的奇葩操作,都会危害到OS,现在只让你用c语言封装的接口,你就害不了他了
左64位,右32位真系统接口
c语言才是世界上最好的语言!
缺页中断、除零错误、野指针,他们本质都是被转化为软中断,OS会提前给每种情况设置处理方法,也都有自己的中断号。
-
CPU内部的软中断,比如int 0x80或者syscall,他们不是说出现的什么错误,知识单纯让进程陷入内核去进行系统调用的,我们把这种操作叫做陷阱(这个名字听起来有点怪)
-
CPU内部的软中断,比如除零/野指针等,我们叫做异常。
所以,能理解“缺页异常” 为什么这么叫了吗?
OS是什么
它就是躺在中断处理例程上的代码块