当前位置: 首页 > news >正文

[Linux] 信号保存与处理

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

所属专栏:青果大战linux

总有光环在陨落,总有新星在闪烁


信号的保存

下面的概念务必记住

  1. 实际执行信号的处理动作称为信号递达(Delivery)

  2. 信号从产生到递达之间的状态,称为信号未决(Pending)

  3. 进程可以选择阻塞 (Block )某个信号。

  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

  5. 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

信号的内核描述

如图所示,我们的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类型的数组,这个数组就是用来当位图用的
OS还很贴心的准备了一组接口用于操作该类型的变量
#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);
  1.  sigemptyset:使信号集set中的所有比特位变为0

  2. sigfillset:使信号集set中的所有比特位变为1

  3. sigaddset:使信号集set的第signum位变为1

  4. sigdelset:使信号集set的第signum位变为0

  5. sigismember:检测信号集set的第signum位是0还是1 

  • 在使用sigset_ t类型的变量前,⼀定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • 这四个函数都是成功返回0,出错返回-1。
  • sigismember是⼀个布尔函数,用于判断⼀个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

 sigprocmask

调⽤函数 sigprocmask 可以读取或更改进程的block表。
#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是什么

它就是躺在中断处理例程上的代码块


http://www.mrgr.cn/news/80875.html

相关文章:

  • 1小时放弃Rust(1): Hello-World
  • Docker的容器
  • 大型系统中的 MySQL 部署与优化(一)
  • QT:QDEBUG输出重定向和命令行参数QCommandLineParser
  • datasets库之load_dataset
  • 强化学习的产业界探索
  • 单片机:实现延时函数(附带源码)
  • 《剑网三》遇到找不到d3dx9_42.dll的问题要怎么解决?缺失d3dx9_42.dll是什么原因?
  • 字节跳动C++面试题及参考答案(下)
  • git使用和gitlab部署
  • [LeetCode-Python版] 定长滑动窗口3——1461. 检查一个字符串是否包含所有长度为 K 的二进制子串
  • 二十一、Ingress 进阶实践
  • 十大排序算法汇总(基于C++)
  • Unity开发哪里下载安卓Android-NDK-r21d,外加Android Studio打包实验
  • Fast-Planner 改进与优化:支持ROS Noetic构建与几何A*路径规划
  • ENSP实验
  • 红队规范:减少工具上传,善用系统自带程序
  • Linux基础及命令复习
  • Makefile文件编写的学习记录(以IMX6ULL开发板的Makefile文件和Makefile.build文件来进行学习)
  • Express (nodejs) 相关
  • [LeetCode-Python版] 定长滑动窗口1(1456 / 643 / 1343 / 2090 / 2379)
  • 【NLP 16、实践 ③ 找出特定字符在字符串中的位置】
  • jmeter中的prev对象
  • Qt学习笔记第71到80讲
  • 字符串类算法
  • Linux-Profile工具