介绍营销的网站,dede网站名称不能保存,电影网站怎么做,wordpress图文安装教程目录
一、信号被处理的时机#xff1a;
1、理解#xff1a;
2、内核态与用户态#xff1a;
1、概念#xff1a;
2、重谈地址空间#xff1a;
3、处理时机#xff1a;
补充知识#xff1a;
1、sigaction#xff1a;
2、函数重入#xff1a;
3、volatile…目录
一、信号被处理的时机
1、理解
2、内核态与用户态
1、概念
2、重谈地址空间
3、处理时机
补充知识
1、sigaction
2、函数重入
3、volatile
4、SIGCHLD 前言
信号在保存后当OS准备对信号进行处理的时候还需要到合适的时机才能进行处理那么什么是合适的时机
一、信号被处理的时机
1、理解
信号的产生是异步的
首先要将信号进行处理之前就需要让OS知道某个进程收到了信号了所以进程就需要在合适的时机查查其对应的blockpendinghandler表但是这三个表都属于内核级别的我们用户级别的进程是不允许访问的所以这里就自然涉及了进程的用户态和内核态之间的转化
合适的时机在进程从内核态转化到用户态的时候对信号进行检测如果有并未被屏蔽就进行处理 怎么进行处理默认忽略用户自定义 2、内核态与用户态
1、概念
进程在执行代码的时候不仅仅是只执行用户的代码还有操作系统的代码想要访问操作系统就需要变成内核态执行用户的代码就要变成用户态 用户态进程只能访问用户自己的代码和数据 内核态进程允许访问操作系统的代码和数据 用户态--内核态 进程时间片到了进行进程切换的时候 调用系统调用接口openread等等 产生异常中断的时候等等 内核态--用户态 进程切换完毕 系统调用结束后 异常中断处理完后 OS是不会收到信号就立即执行的比如当前我正在进行系统调用或者正在切换进程等等在从内核态转化为用户态的时候就进行信号的查询三表和处理信号所以要等到OS将更重要的事情忙完后在进程从内核态到用户态的时候就进行信号的检测和处理 2、重谈地址空间
如下我们回顾下我们地址空间的知识 每个进程都有其独有的虚拟地址空间进程具有独立性 通过页表的映射MMU机制进行虚拟到物理地址之间的转化 进程都具有其对应的虚拟地址空间能够让进程以统一的时间看待我们的代码和数据 虚拟地址空间又可分为两个空间0~3GB的空间为用户空间3~4GB的空间为内核空间 为什么要区分内核空间和用户空间 因为内核空间中存放的是OS的代码和数据是不允许进程随便访问的需要进程切换到内核态才能进行访问并且规划也能够OS进行更好的管理 其中用户空间就是我们的代码和数据当进程在用户态的时候就能够访问这段空间的代码和数据 内核空间中存放的就是OS的代码和数据这里的虚拟地址通过特有的内核级页表从虚拟地址空间映射到物理内存中由于OS是最先被加载的程序所以其映射应该在较为底部的位置 我们知道每一个进程都有其对应的进程地址空间 那么是不是每一个进程都有其独特的内核空间和内核级页表呢
答案是只有一份
对应用户空间和用户级页表有很多份的因为进程具有独立性 但是内核级页表只有一份内核空间比较特殊所有进程最终映射到物理内存都是同一块区域进程只是将操作系统代码和数据映射入自己的进程地址空间而已其中内核级页表只需将虚拟地址空间映射到物理内存所有进程都是如此 所以每一个进程看到的内核空间中的内容看到的内核级页表资源最后映射到的物理内存中的代码和数据都是一样的 所以在用户空间的代码区中执行对应的系统调用 首先将进程从用户态转化到内核态 然后在自己的内核空间中找到对应的系统调用方法 然后通过内核级页表映射到物理内存找到对应的代码和数据 然后在返回在返回的时候进程从内核态转化为用户态 这样就相当与在进程自己的进程地址空间中进行系统调用 那么上述的切换是如何进行切换的呢 首先我们知道CR3 寄存器存储当前进程的页目录表物理地址用于分页机制下的内存管理 在CPU中同样还有一个叫做ESC寄存器的东西这个寄存器的作用就是用来表示当前进程的状态是用户态还是内核态 那么这个寄存器是怎样实现的呢 在这个寄存器中有着最后两个比特位两个比特位有4种表示方式00 01 10 11其中只使用两种方式00和11也就是对应十进制的0和3
当这个寄存器最后两个比特位为0的时候表示当前进程处于内核态 当这个寄存器最后两个比特位为1的时候表示当前进程处于用户态
所以切换进程的状态就需要将ESC寄存器中的最后两个比特位修改为对应的值CPU为我们提供了一种方法来修改自己的工作状态int 0x80指令 小总结 1、每一个进程中的0~3GB中的内容是不一样的因为进程具有独立性 2、每一个进程中的3~4GB中的内容是一样的在整个系统中无论进程再怎么切换 3~4GB中的内核空间内容是不变的 3、在进程视角我们进行系统调用就是在我自己的地址空间中进行执行的 4、操作系统视角任何一个时刻都有进程执行想执行OS的代码可以随时执行 5、操作系统本质是一个基于时钟中断的一个死循环 怎么理解操作系统的执行逻辑呢或者说操作系统的本质是什么
其可以理解为一种基于中断驱动的“死循环”模型核心在于通过时钟中断等机制实现任务调度资源管理和实时响应
主框架循环
操作系统的核心代码是一个死循环for(; ;) pause(); 这个循环并非是空转的而是通过中断机制被动唤醒的当没有外部事件用户输入I/O完成或内部事件如时间片耗尽时操作系统会进入低功耗状态或执行空闲任务
其中操作系统本会一直卡在pause()这个行代码暂停等到发生中断机制被进程“推着走”才能够让代码得以运行 那有什么中断机制呢 时钟中断每过一定很短的时间产生一次用于更新系统时间、检查进程时间片、触发调度
硬件中断如键盘输入由外设通过中断控制器通知CPU
软件中断如系统调用允许用户程序访问内核空间 进程是如何被操作系统调用的呢 进程被调度就意味着它的时间片到了操作系统会通过时钟中断检测到是哪一个进程的时间片到了然后通过系统调用函数schedule()保存进程的上下文数据然后选择合适的进程去运行
3、处理时机
有了上述铺垫的知识接下来可以理解
我们前面了解到在进程从内核态转化到用户态的时候对信号进行检测处理方式如下图 对于上述的执行用户自定义有两个问题
为什么要切回用户态再执行对应的方法 因为如果在内核态中执行用户自定义的方法可能自定义方法中存在危害操作系统的代码这是不安全的
为什么在用户态执行完毕后还要返回内核态再到用户态 需要回到内核态找到进程的上下文将上下文带出到用户态才行并且自定义的动作和待返回的进程是属于不同的堆栈不能够直接返回 对于信号捕捉的理解可以看看下面这张图 如上这是一个横着的8然后用一横贯穿上面是用户态下面是内核态注意有4个交点并且8的交点是在横线下方的也就是在内核态中
接下来解释上图
四个绿色的圈圈就表示两态之间的切换了四次当进程时间片到了进行进程切换产生异常中断的时候等等就进行用户态到内核态之间的转化
进程切换完毕系统调用结束后异常中断处理完后进行内核态到用户态之间的转化
此时就进行信号的检测首先看pending表如果其为1并且该信号没有被阻塞就执行对应动作如果被屏蔽或者为0就继续从pending表向下找下一个以此类推
二、补充知识
1、sigaction 其中有个新的结构体sigaction其内部成员如下
我们只关心第一个和第三个成员 成功返回0失败返回-1错误码被设置
第一个参数signum信号编号
第二个参数act 传入该类结构体设置屏蔽信号什么的在之前就要设置好
第三个参数oldact保存修改前的结构体 其中sigaction中的第一个成员变量就是signal的第二个参数需要自己设置自定义 第三个成员变量就是屏蔽的信号集怎么理解呢 首先我们知道比如当在处理2号信号的时候如果sa_mask默认那么OS就只会屏蔽2号信号如果还想要屏蔽更多信号就需要sigaddset(act.sa_mask,1);这样在待传入结构体中的sa_mask进行更多的设置来屏蔽 void Printpending()
{sigset_t set;sigpending(set);for(int signo 31; signo1; signo--){if(sigismember(set,signo)) cout 1;else cout0;}cout endl;
}void myhandler(int signo)
{cout get a signo : signo endl;while(1){Printpending();sleep(1);}
}int main()
{struct sigaction act,oact;memset(act,0,sizeof(act));memset(oact,0,sizeof(oact));//清空信号集sigemptyset(act.sa_mask);//添加屏蔽信号sigaddset(act.sa_mask,1);sigaddset(act.sa_mask,3);sigaddset(act.sa_mask,4);sigaddset(act.sa_mask,9);//设置自定义方法act.sa_handler myhandler;sigaction(2,act,oact); while(1){cout i am a process mypid : getpid() endl;sleep(1);}return 0;
}
如上这样就将1 3 4号都屏蔽了9号和19号信号可以经过试验发现不可屏蔽 2、函数重入 可以被重复进入的函数称为可重入函数 如下是一个场景 如上当在insert函数中捕捉到了信号并且在信号的自定义动作中又调用了insert这样函数就会重入这样就会导致函数节点丢失在释放的时候无法释放node2就会导致内存泄漏
这就是函数重入导致的内存泄漏问题
我们把这种函数称为可重入函数注意这个是特性不具有褒贬含义
3、volatile
int flag;void myhandler(int signo)
{cout get a signo : signo endl;flag 1;
}int main()
{signal(2,myhandler);while(!flag); //flag 0 为假 !flag 为真cout a process quit ! endl;return 0;
} 如上上述本来是一个死循环但是当给当前进程发送2号信号的时候就会进入我们的自定义函数调用这个时候在里面将flag修改为1这样就能够退出while死循环了并且我们的代码运行起来也是达到预期了
但是在编译中有个-O1/O2/O3这种的优化如下我们把这种优化带着在运行代码试试 如上此时发现尽管我们给当前进程发送2号信号并且也看到了自定义代码中打印的字符串但是进程却没有退出while这个死循环
这是为什么呢
这是因为在编译的时候进行了O1的优化 如上正常情况下CPU在每次进行逻辑检测的时候每次都从内存中进行读入这种IO比较费事当进行O1的优化之后就会对整个代码进行检测此时没有信号就检查不到flag被修改了此时就会将flag放入逻辑检测的寄存器中这样当在自定义中修改了flag这只是把内存中的flag修改了但是CPU寄存器中的flag并没有被修改所以就会一直继续死循环
为了防止这种过度优化保存内存的可见性可以在全局变量前面加上volatile关键字修饰这样就无法将flag放入到寄存器中老老实实地每次都从内存中进行读入 4、SIGCHLD
在前面实现进程等待的时候每次当子进程退出的时候父进程都需要等待回收子进程防止其成为僵尸进程造成内存泄漏问题
父进程有两种方式等待子进程设置0为阻塞等待或者设置WNOHANG为非阻塞轮询
但是上述两种方式都有缺陷要么父进程阻塞不能做其自己的事情要么每次工作的时候都还要关心关心子进程的状态
那么有没有一种方式能够让父进程不在关心子进程当子进程退出的时候自动回收呢
有的有的
首先要了解子进程在退出的时候会向父进程发送17号信号所以我们可以在父进程中将17好信号捕捉然后在自定义函数中等待这里设置第一个参数为-1为等待任意进程
void myhandler(int signo)
{pid_t rid waitpid(-1,nullptr,0);cout a signo get : signo mypid getpid() rid : rid endl;
}int main()
{signal(17,myhandler);pid_t id fork();if(id 0){//childcout child process getpid() myppid : getppid() endl;sleep(5);exit(0);}//fatherwhile(1){cout father process getpid() endl;sleep(1);}return 0;
} 如上这样父进程就可以不用管子进程了能够完成子进程的自动回收
但是这样还不够如果有多个子进程呢能够全部回收吗
这显然是不会的SIGCHLD这是一个信号当父进程收到了第一个信号的时候会将block表中的17号信号置为1使其屏蔽这样其他子进程的信号就丢失了就会导致僵尸进程
那么如何解决呢
我们在自定义中采取while循环的方式回收即可 当然还有一种更方便的但是只能在Linux中有效 将SIGCHLD这个信号的默认动作设置为忽略这样父进程不会对其处理但是当子进程退出之后OS会对其负责这样的话就会自动清理资源并回收不会产生僵尸进程引起内存泄漏