1. 信号的处理时机

信号的处理,不是立即处理,而是可以等一会处理,合适的时候再进行处理

那合适的时候到底是什么时候呢?
进程从内核态返回内核态的时候,因为这个时候OS要做信号检查,信号检查就是检查pending表

如果信号是默认呢?忽略呢?
如果信号是忽略,OS查handler表,发现是忽略,此时把pending表的比特位1->0,不做任何处理
如果信号是默认,大多默认是杀掉进程,所以OS直接杀掉进程

执行自定义方法(用户写的),OS也要在做身份切换->必须以用户身份执行
为什么?
因为如果用户写的自定义方法有非法操作,比如是原来以用户身份不让你执行操作,现在你以内核身份执行就会出现Bug

我的进程凭什么进入内核啊?

while true
{
}

这种代码也会进入内核吗?
是的,它也会!它会被调度,当一个进程的时间片到了的时候,操作系统会把它从CPU上剥离下来,放入等待队列中,然后适当的时候再唤醒调度,所以只要是代码就会进入内核
在这里插入图片描述
在这里插入图片描述

4->5:主要为了检查是否有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了
在这里插入图片描述

2. 硬中断

在这里插入图片描述
OS / 进程怎么知道键盘上面有数据了 -> 硬件中断

当外设就绪时,外设会对中断控制器发起中断,然后中断控制器就可以获取中断号,不光CPU有寄存器,外设也有,所以中断控制器也有,里面存的就是中断号,然后中断控制器通知CPU,告知CPU有外设就绪了,比如键盘上面有数据了,此时CPU再从中断控制器中读取中断号,就知道是哪个设备就绪了,这个过程叫硬件中断,这是硬件话题
在这里插入图片描述
这里的通知其实是给cpu的针脚发送高低电频

但是这里只是让CPU知道哪个外设就绪了,但不知道怎么执行数据,比如要不要把数据加载到内存,要不要重新调整空间,要不要进行空间申请,释放,合并,加载到内存的什么位置,这些只有软件最清楚,所以我们需要软件部分,也就是操作系统

中断向量表是OS系统的一部分,它是操作系统内置的,本质就是函数指针数组,下标就是中断号,操作系统加载到内存时,CPU可以通过中断号索引到函数指针内部的方法,然后把方法的入口喂给CPU,CPU就可以执行特定的执行方法了,比如读取键盘数据,这些中断向量表是早就写好了的,拿到中断号就可以执行

当硬件中断时,CPU会保护现场,也就是会保护进程的代码和数据。等处理完中断后,再恢复现场继续执行原来进程的代码和数据,所以OS 再也不关注外部设备是否准备好,而是外设准备好会硬件中断,从而让OS执行固定的方法

在没有中断到来的时候,OS在干什么?
OS系统什么都没做,OS系统是暂停的

for(;;)
pause();//操作系统被暂停什么都不做,除非收到中断或信号才会执行

在这里插入图片描述
但是我们的OS系统是要一直进行工作的,所以cpu内部集成了一个时钟中断,以特定频率向操作系统发送中断让操作系统执行进程调度,很高频,所以操作系统就在硬件的推动下,自动调度了,所以OS就是基于硬件中断工作的软件
在这里插入图片描述

在这里插入图片描述
这是内核代码,由时钟中断引起的,执行0x20编号下的方法_do_timer,判断进程的时间片是否>0,大于0直接返回,继续进程调度,如果时间片到了,switch_to(next),进程切换调度其它进程

2. 软中断->异常

上面都是由硬件产生的中断,当然也有软件产生的硬件中断
在这里插入图片描述
当进程执行自己代码时,如果有除0错误,EFLAGS会识别到除0异常,从而向OS发送中断,执行异常方法,就是给目标进程发送信号,野指针和指针重复释放都是这个原理,缺页中断,当MMU查看页表映射关系,发现只有虚拟地址(合法)没有物理地址时,会触发缺页中断,让操作系统在内存申请一块空间,拿到这段空间的起始物理地址填写页表,完成映射,这些软件出异常而引起硬件的中断叫异常

3. 软中断->陷阱

CPU内部可以通过软件主动中断
x86:int x80
x64:syscall
这些都是指令集,c/c++代码本质就是编译成了指令集+数据
在这里插入图片描述
上面的表属于操作系统

当我们进行系统调用时,具体是怎么进入操作系统,完成调用过程的,毕竟cpu只有一个?
系统调用表,从此以后每个系统调用都有一个唯一下标,这个下标就是系统调用号

用户层面怎么理解系统调用?
pid_t fork(void);

这个系统调用是操作系统提供的吗?
不是,OS只提供系统调用号,这个系统调用是glibc封装的,这个系统调用在内部只做了图下的两件事,把系统调用号给寄存器,然后进入内核
在这里插入图片描述
在这里插入图片描述
整个过程是系统调用,把系统调用号交给eax,然后syscall xxx,或int 0x80产生软中断,进入内核,查找中断编号,一般是0x80,然后执行对应系统调用号的函数指针方法
在这里插入图片描述
内核代码
在这里插入图片描述

4. 内核态和用户态

在这里插入图片描述
系统调用的过程也是在进程地址空间上进行的,所有的函数调用都是地址空间上的跳转

用户的页表存在多份,因为每个进程都要有一个用户页表来执行自己的代码和数据,但是OS只有一个,所以系统页表只有一份,所有进程共享

这就意味着,无论进程怎么调度,我们总能找到OS

1. 用户和内核都在同一个[0,4GB]的地址空间上了,如果用户随便拿一个[3,4GB]的虚拟地址,那用户不就可以随便访问访问内核中的代码和数据了吗?
2. OS系统不相信任何人,必须采用系统调用的方式进行访问,此时这两个结论冲突

所有内核态和用户态的概念就出来了

用户态:以用户的身份,只能访问[0,3GB]
内核态:以内核的身份,运行你通过系统调用的方式,访问OS[3,4GB]

在系统中,用户或者OS自己怎么知道当前属于内核态还是用户态?
CPU里存在cs寄存器,里面会存储代码地址和数据,这个数据是二进制整数,0:内核态,3:用户态,所以当我们代码区系统调用时,它会更改CPU的权限级别,0->3,再把cs的代码地址改成内核区你要执行的那个地址,然后陷入内核区,执行你的系统调用,然后软中断查看你页表对应映射的方法!
在这里插入图片描述
在这里插入图片描述

当前正在处理的信号在执行自定义捕捉方法(递达)之前就会把pending的比特位1->0,并且会把block0->1,所以如果同一个信号如果再来,会把pending的比特位再0->1,但是被阻塞着,无法递达

5. 可重入函数

在这里插入图片描述

假如node1刚执行完p->next=head,然后变成图一的样子,然后突然收到信号,转而去执行sighandler,就会再次进入insert,然后执行2和3,最后信号处理完再回到4,此时就会导致node2没人管理,内存泄漏

insert方法被main执行流和sighandler执行流重复进入了,函数被重入了,如果一个函数被重入不会出现问题称这个函数为可重入函数,如果一个函数被重入后出现了问题称这个函数为不可重入函数

一般函数只在自己内部有临时变量这种函数一般是可从入的,如果有全局变量这种函数就是不可重入的,这个例子里的链表是全局的,所以不可重入函数,大部分函数都是不可重入的,可重入或不可重入是特点,不是说不可重入就是错误

6. volatile

#include <stdio.h> 
#include <signal.h> 
int flag = 0; 
void handler(int sig) 
{ 
 printf("chage flag 0 to 1\n"); 
 flag = 1; 
} 
int main() 
{ 
 signal(2, handler); 
 while(!flag); 
 printf("process quit normal\n"); 
 return 0; 
}

在这里插入图片描述

这里的flag,在自定义捕捉里面被修改了,但是这个自定义捕捉不是直接的函数调用关系,而是处理信号的,所以编译器优化级别比较高的情况下,会把该变量优化到寄存器中,所以当我们对flag进行修改时,虽然值被修改了,但是CPU只会对原来存入寄存器的变量进行逻辑运算,所以这个程序会死循环

一般不优化的情况下,CPU会从内存读取数据然后进行逻辑运算,但是优化后就把该变量内置在CPU了,而不从内存读取,直接拿CPU内置的变量做判断,所以修改该变量也没用,这样优化省掉了CPU读取内存的时间,但是会出现问题,所以我们 volatile int flag=0,防止编译器过度优化,也就是必须让CPU从内存读取数据

7. SIGCHLD信号

子进程在终止时会给父进程发送SIGCHLD,17号信号,但是这个信号的默认动作是忽略
如果我们不想让父进程等待子进程的话,就将SIGCHLD的处理动作改成SIG_IGN,此时系统自动回收子进程,就不会有僵尸问题了

signal(SIGCHLD,SIG_IGN)//父进程
#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h> 
void handler(int sig) 
{ 
 pid_t id; 
 while( (id = waitpid(-1, NULL, WNOHANG)) > 0) { 
 printf("wait child success: %d\n", id); 
 } 
 printf("child is quit! %d\n", getpid()); 
} 
int main() 
{ 
 signal(SIGCHLD, handler); 
 pid_t cid; 
 if((cid = fork()) == 0){//child 
 printf("child : %d\n", getpid()); 
 sleep(3); 
 exit(1); 
 } 
 while(1){ 
 printf("father proc is doing some thing!\n"); 
 sleep(1); 
 } 
 return 0; 
}

所以我们可以在父进程收到信号后,再等待子进程,如果没有收到信号,父进程就做自己的事情,这是个好的代码设计思路

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐