回顾
- 进程 = 内核相关数据结构 + 代码和数据,一个可执行程序加载到内存变成进程,不仅仅是把代码和数据加载进去就完事了,得“先描述,再组织”,每个进程都有内核数据结构,地址空间,进程相关页表,CPU寄存器的上下文数据,被打开的文件,进程通信,进程信号等等,由操作系统统一管理
- 进程要访问资源,OS要对进程本身和进程要访问的资源进行管理,就必须要有对应的数据结构来描述,那么进程与访问资源的关系就变成了对相关的数据结构的关系。代码和数据通过页表进行映射,页表分为用户级页表和内核级页表
- 原本的mm_struct地址空间本质也是一个结构体,里面包含着很多start和end,但是我们使用mm_struct申请空间时,由于地址空间的划分的每个区域很大,所以需要对里面的区域更细致地划分,vm_area_struct就是对地址空间进行更细致地划分
- 当上层调用malloc或者其他系统调用来申请内存时,会申请一个个vm_area_struct,里面有start表示虚拟地址起始,end表示虚拟地址结束,然后把很多个结构体用双链表描述起来
- 问题:虚拟地址是如何通过页表映射到物理地址的呢?
①从文件系统地角度来说,我们所说的.exe,本质就是一个文件 ②可执行程序本来就是按照地址空间方式进行编译的 ③可执行程序,其实在磁盘中也按照区域被划分为以4KB为单位的小文件(将这4KB存储的代码和数据我们称为“页帧”,而物理内存的4KB大小称为“页框”,IO说白了就是把页帧装进页框里),物理内存也按照4KB为单位划分的,OS在进行IO的时候,是以4KB为单位的 - 但问题是,OS怎么知道这么多4KB空间哪些用了哪些没用呢?
所以OS肯定是要管理的,而对于这100多万个4KB的管理,就是我们的struct Page。我们通过一个数组struct page mem[100w+]来把这一百万个page管理起来,结构体里有个字段为int flag,就表示该page在数组的下标
缺页中断
- 进程加载时,通过虚拟地址和页表找到位于磁盘上特定位置的文件,同时页表上也有一些二标志位字段,代表当前页表对应的要访问的代码和数据是否已经加载到内存中
- 假设是第一次加载,就直接从物理内存申请一段空间,然后把磁盘上的可执行程序搞到物理内存中,返回虚拟地址
- 在这步操作中,操作系统一般会介入进行特殊处理,称为缺页中断,操作系统会把已经加载到物理内存的地址,替换掉原本页表映射到磁盘上的那个指针,简单来说就是如果再次运行这个程序,就直接去物理内存找了不再去磁盘上找了
- 从用户的视角来看,这样做的最直接的感受就是,第一次打开一个软件时慢一点,但是第二次往后重复打开时,速度会快很多
二级页表
- 映射就一定有KV,K就是虚拟地址,V就是物理地址,KV两个都是4字节,页表中一行映射关系就是KV还有其他字段一共8字节,假设我们一共有2^32映射关系,就是8*2^32就是32G(页表也是在物理内存当中保存的),光光保存页表就要这么多空间,不可能
- 虚拟地址32位,就是32个0,使用虚拟地址的时候并不是把这个32个虚拟地址全部使用的,它把32个比特位划成三份,左10个,中10个,右12个
- 页表也不是只有一个结构,他是多种结构集合,里面有一个一级页表(页目录),它的任务就是只拿着虚拟地址的左10个比特位进行索引,一共需要2^10也就是1024个映射关系条目,每个条目是10个字节,一共就是10KB,就能表示这里的一级页表了
- K是左10个,通过KV模型找到V,但是V也不是直接映射到物理内存,而是映射到匹配的二级页表,也是KV关系的,然后再次通过中10个比特位在二级页表再次进行索引
- 然后二级页表的V指向物理内存中一个页的起始地址,这样,只需要用20个比特位就能通过一级二级页表找到对应的物理内存的4KB的起始地址 --> page起始地址 + 页内偏移(右12位)= 找到具体位置,为什么是12位呢 --> 因为2^12刚好是4KB
- 一级页表和二级页表只需要维护虚拟地址到页的关系,后面就是通过数值计算加偏移量的方式,就算把32个比特位的前面20个全部干满也就是2^20大概1MB,再加上一二级页表的KV一共20个也就是20*1大概20MB空间,而一般二级列表数量很少,所以前面说的空间大小关系完全不用担心