1.遗留问题
前面在fork创建子进程的内容中遗留了一个问题,一个 变量既等于0又大于0.
2.地址空间的概念
(仅有栈区从高地址处向低地址处)
(堆区和栈区之间有一大块的镂空,这里暂时不作介绍)
使用代码验证上图的大致结构
在我们使用static修饰局部变量 时,它处于字符常量区
在我们使用fork创建子进程的时候我们观察到这样一个现象
变量g_val 的同一个地址指向了两个不同的值
由此我们可以推出,变量的地址并不是物理地址,而是虚拟地址
3.页表
从而,我们通过程序地址空间,引出页表的概念。
页表可以通过虚拟地址(即语言层面上的地址)找到内存中的物理地址,即一种映射关系,在我们创建子进程的过程中,子进程的代码继承自父进程,从而它的虚拟地址同父进程一样,但是这个虚拟地址指向的物理地址却可以不一样。
子进程进行写时拷贝是由操作系统自动完成的,本质是重新开辟空间,但是虚拟地址是0感知,物理地址的改变并不会影响它。
4.知识补充
(内存和cpu都有短暂的数据存储能力)
在32位计算机中,有32位的地址和数据总线 -----每一根总线根据电脉冲的强弱,在软件层面以二进制的1和0表示,通过这种表示,硬件可以访问内存中相应的位置。
5.地址空间究竟是什么?
什么叫做地址空间 ---- 地址总线排列组合形成的地址范围[0,2^32]
所谓的地址空间本质描述的是一个进程可视范围的大小,即一个进程可以看到多大的内存
6.如何理解地址空间上的区域划分?
地址空间内一定要存在各种区域划分,对线性地址规定开始和结束的地址start和end即可。在这之间的一个个地址都是该区域可用的地址,如栈区地址,堆区地址等等。
同时,程序的地址空间也是内核的一个数据结构对象,类似pcb,地址空间也是要被操作系统管理的,先描述,再组织
再taks_struct中存有struct mm_struct的物理地址可以直接找到struct mm——struct
struct mm struct里面有对于区域的划分
也就是因为这样,系统可以很容易的判断我们的访问是否越界
7.我们为什么要有地址空间?
1.使进程可以使用统一的视角看待内存,每一个进程启动时都会有一个进程地址空间,因为一个进程不可能占满所有,所有每一个进程看到的内存都是4GB,当它去申请的时候,系统就给它分配空间。
2.增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,它会被直接拦截,该请求不会到达物理内存,可以起到保护物理内存的作用
3.因为地址空间和页表的存在,进程管理模块和内存管理模块可以进行解耦合
我们继续扩大页表的概念并加入cpu。
task_struct中存有页表的起始地址,cpu中有一个cr3寄存器可以储存该进程的页表信息,(页表信息也属于上下文),因此当需要访问内存的时候,cpu可以通过cr3寄存器找到该进程的页表,再通过虚拟地址去找到物理地址。
在页表条目中还有一个标志位,会规定该地址所对应的代码或数据是可读还是可写。
众所周知,linux中还有一个挂起状态,那我们如何知道代码和数据是否以及被加载到内存呢?我们继续扩大页表的概念
这个标志位可以使用位图,或者说以0 1来表示对应的代码是否已经被加载到内存
进程的虚拟地址可以都存放在页表中,但是物理地址可以不存放,这时候我们将其一个标志位改为0。
当我们要访问内存时,我们先找到虚拟地址,再看其标志位,当标志位为1,那么我们直接访问对应的物理内存。
当标志位为0,系统会先申请一块空间,将可执行程序剩下的代码和数据存放到内存中,并将它的物理地址填充。再继续进行访问内存,这个过程是自动的。
上面这一中断的情况被称之为缺页中断
缺页中断还包括我们之前学习的写时拷贝,在子进程想修改从父进程处拷贝来的数据时,该地址对应的数据为只读,系统也会再申请一块空间,在内存中放置子进程修改的数据,再填充物理地址
8.知识补充
现代操作系统几乎不做浪费时间和空间的操作
操作系统对于大文件可以进行分批加载
同时因为cpu性能 时间片等的影响,cpu短期之内可能跑不完已经加载到内存里的代码和数据,因此操作系统采用的是惰性加载的方式,几乎是用多少给你多少,例如我们有500M的数据我们可以把页表的虚拟地址全部填好,但是不用将所有的物理地址也填好。
9.进程具有独立性
1.因为进程具有它们各自独立的内核数据结构
2.由于页表的存在,每一个进程的代码和数据就解耦合了,他们或许有相同的虚拟地址,但是这些虚拟地址可以映射不同的物理地址。同时因为这种映射性,虚拟地址可以以连续的形式呈现给进程,但是物理地址可以乱序。
10.进程的组成
现在我们对于进程的理解有扩大了一些,现在我们意识到
进程 = 内核数据结构(task struct && mm struct && 页表)+ 数据与代码
11.进程的切换
我们切换进程需要切换进程的pcb,mm struct ,页表
但是只要我们切换了pcb,它可以找到对应的mm struct, 又因为页表的地址属于进程的上下文,当切换时我们将进程的上下文在cpu中恢复时,我们可以找到对应的页表。