虚拟内存

在正式讲述虚拟内存之前需要提及存储器的层级结构以及进程在内存中的结构。存储器的层级结构速度从快到慢排列如下
寄存器——L1高速缓存——L2高速缓存——L3高速缓存——主存——磁盘——分布式文件系统

图片描述
而成本也是从高到低,空间是从低到高。两个相邻的存储设备,前者往往是充当后者的高速缓存,后者往往存储比前者更完整的数据。后面的内容会涉及到高速缓存和主存。

为了简单的理解可以假设主存是一个线性的数组。每个元素可以是一个字节。而主存与硬盘或者与高速缓存间做数据传输时,每次可以传输若干个字节,我们可以把这若干个字节定为一个新的单位,叫"块"。而高速缓存中每个存储的元素则是一个块。高速缓存的存储结构
图片描述

如上图所示,高速缓存被分成S个组,每个组里面有E行,每行都有一个有效位,t个标记为和B个字节,这B个字节就组成了之前提到的块。并且和内存地址有这样的一个关系m=t+s+b。即有t个标记位,s个位作为组索引,b个位作为块偏移,并且是t,s,b是从高位到低位排列。这个关系决定了给定一个地址,它该存放在高速缓存中的哪个位置。

另外整个高速缓存的总容量是C=SBE;按照S和B的变换可以把高速缓存分成下面三种类型

1.直接映射

2.组相联

3.全相联
直接映射和全相联是两种极端情况。
直接映射是缓存中每个组只有1行,即E=1。对于这种情况,当某个当某个地址在缓存中发生冲突时则直接替换。

图片描述

全相联是缓存中只有一个组,所有行都在这个唯一的组里面,相当于S=1。这种情况所有缓存都放在一个组里面,然后轮询找出哪一行是未使用的,如果全部组都已经使用了,那就按照一定的算法找出一个块踢出,再放上新的块。
组相连则是介于上面两种情况,每个组最少有两行。这种情况跟全相联一样,不命中的时候在组里面找不到未使用的块,也是需要有策略选出一个牺牲的块,替换上新的。
图片描述
下面则用直接映射来模拟一下读取高速缓存的过程现在有一个直接映射的高速缓存如下
(S,E,B,m)=(4,1,2,4)

由此可以得出s=2,b=1,t=m-b-s=4-1-2=1,整个地址构成如下

t=1s=2b=1

所有地址的标记位,组索引和块偏移如下表所示,另外给内存中的块编上一个序号
图片描述
高速缓存如下
图片描述

假设现在读地址0的字,按照上面的表,组块索引是0,标记位是0,缓存不命中,需要从内存中读取块0写入高速缓存,因此m[0]和m[1]会被写入组0,标记位是0,有效位置1,写后高速缓存如下

图片描述

接着读地址1的字,内存地址是0001,组索引为0,标记位是0,查找高速缓存,组号和标记位对上,有效位是1,取偏移是1的值m[1]

接下来读地址8的字,内存地址是1001,组索引是0,标记位是1,查找高速缓存不命中,而且该组已经被用上了,牺牲了原本的0组的两个值m[0]和m[1],读取新的值m[8]和m[9],块偏移是1,取值m[8]

图片描述

假设再读地址0的字时,又会发现不命中,重复类似第一次读地址0的操作。

图片描述

接下来补充链接的少部分的内容,这些内容会在内存映射和内存分配的时候会涉及到
图片描述

上面的这幅图在众多C语言的书都有出现,其他语言的都有类似的流程图。一个C文件经过预处理和编译,得出了一份汇编语言的源码文件。这个文件经过汇编器汇编之后,就产生了人类无法直接阅读的可重定向目标文件。这个文件具有一定的结构,

图片描述

它会把原本程序员写的源代码拆分成若干部分:指令放到.text节;全局静态变量放到.data节;内部定义的函数名、引用外部函数名这些符号信息放到.symtab节……但是外部引用的函数地址(如我们最常用的printf)在这个阶段还是没有的,

图片描述
这些需要引用地址的信息都放到一个叫"可重定向条目"的结构里面,此时需要往下走到链接这个过程。
图片描述
最后链接器链接时会把这些外部引用的函数地址给填上去,
图片描述

最终生成了像上图结构的可执行文件,常见的是windows的exe文件。
可执行文件结构如此,一个程序运行后,进程在内存中的结构是如下这样子。
图片描述

这幅图跟可执行文件的结构较为相似,底部是低地址,地址往上递增。低地址处是代码区.text已初始化数据区.data未初始化数据区.bss。这几个区是在程序运行的时候通过内存映射附加上去的。在高地址处有个区域是用户栈,这个栈栈底在高地址处,栈顶在低地址处。栈顶和栈底由两个指针记录他们的地址,指针的值存放在CPU寄存器中,分别是%rbp和%rsp。用户栈的作用有两个,其一是存放函数调用的栈帧,其二是存放函数调用期间所用到的临时变量。另外一个区域是位于整个空间中部的运行时堆,这个运行时堆主要是用于给程序申请内存空间的时候用的,即调用malloc的时候。动态内存分配也属于虚拟内存范畴的内容,但本篇暂不作介绍。

下面则进入虚拟内存范畴的内容。从物理内存讲起,因为物理内存大家较为熟悉,一个内存上分配了若干的存储空间,每个存储空间都有一个唯一能定位到它的地址称为物理地址。把一个物理地址传给内存控制器就能往指定地址的内存空间上读/写数据。而虚拟内存则是现代操作系统对内存做了一层抽象。与物理地址对应,虚拟内存上的每个存储空间的地址称为虚拟地址。尽管是虚拟地址,最终提供或者存放数据的地方还是物理地址,在这个过程中虚拟地址会同通过一个在CPU上的名为内存管理单元(简称MMU)的硬件翻译成物理地址,这个过程就叫做地址翻译。在之前可执行文件的汇编语言截图中的地址是虚拟地址,进程的内存结构中的几个地址都是虚拟地址。

虚拟内存实际是存放在磁盘中的,在该空间中划分了一段一段的连续的空间,这个空间叫作一个"页",与之前高速缓存中的"块"概念类似。同样地,在内存中也有一样的分页机制。这个页就用于主存跟磁盘间相互传递数据的最小单元。一个虚拟页被存放到物理内存中,这个行为也叫作缓存。磁盘空间比物理内存空间要大得多,同样地虚拟地址空间也会比用作虚拟页的空间要大。通常一个虚拟页的状态有三个
1.未分配:实际上该页是未存在的,在磁盘上未开辟空间;

2.已分配:与未分配对应,已经在磁盘上开辟了空间;

3.已缓存:该页已经被分配了,并已经被存放到物理内存中。

图片描述

还有一个比较重要的结构叫做页表,页表中每个元素叫作页表条目(简称PTE),PTE的数量与虚拟页数量相同。页表是存放在物理内存中。每个PTE都有一个有效位,当有效位为0时,表明该页未分配;当有效位为1是,PTE中存放的要么是虚拟页的页号,要么是已经缓存到物理内存的物理地址。

图片描述

CPU平时只能往物理内存读写数据,当刚好取到的页属于已分配状态,就会引发缺页异常,这时候系统会从物理内存中选出一页作为牺牲页,把PTE指回虚拟内存的页号,把新的页从磁盘加载到物理内存,PTE指向物理内存的地址中。

之前也有提及从虚拟地址转换得出物理地址的过程叫地址翻译,由CPU里面的MMU执行。下面则来讲述这个过程。

图片描述

如上图所示,一个虚拟地址可以划分成两部分,低的p位作为虚拟页的偏移量,而剩下部分是作为虚拟页的页号,实质上就是PTE在页表上的索引而已。同样物理地址也是分了两部分,物理页偏移量和物理页号,有个额外的规则是物理页的偏移量=虚拟页的偏移量。这里P=2p,P是页的大小,字节为单位。各种量的简称如上图所示。地址翻译过程的简述如下:

1.给定一个虚拟地址,划分出虚拟页偏移量和虚拟页号

2.通过页表基址寄存器找出页表,找出对应页号的PTE

3.看从PTE得出页是否已缓存,不缓存则要找出牺牲页替换,得出物理页号

4.把物理页号和虚拟页偏移量组合成物理地址

在上述过程中的第三部查找页表搜出PTE在整个过程中较为耗时,因为涉及到访存。假设这部分操作利用上缓存则会节省一部分的时间。TLB正是解决了这个问题,TLB叫翻译后备缓存器(Translation Lookaside Buffer),存放在MMU中。有了这种机制一个虚拟地址的结构将再次被划分,这次划分的区域是虚拟页号

图片描述

下面通过一个例子,结合主存,高速缓存,PTE,TLB,页表来模拟这个地址翻译的过程

图片描述

TLB,页表,高速缓存入下面所示

图片描述

图片描述

图片描述

现在要求出虚拟地址0x03d4的值。
按照前面的条件得出
p=6,所以VPN=14-6=8;

TLB是4路组相联,所以TLB索引占两位,剩余的标记位是TLBT=VPN-TLBI=8-2=6
虚拟地址可以拆成
图片描述

TLB行数是3,标记位是03的命中,PPN是0D,PPN是12-6=6位,因为PPO=VPO,所以重新组织后物理地址是0x354,MMU的地址翻译就到此结束了,。

高速缓存是直接映射,行大小是4字节,一共有16个组
所以得出以下信息
B=4=2^2,b=2;

S=16=2^4,s=4;

t=m-s-b=12-4-2=6

图片描述

高速缓存索引号是5,标记位0D命中,偏移是0。最终结果虚拟地址0x03d4的值是0x36。

假设上面TLB不命中,虚拟页号是0x0F的,查页表也得出有效位是1,PPN是0x0D。

内存映射
内存映射与虚拟内存极为相似,也利用了虚拟内存。这两者也是利用了页表。内存映射是与磁盘上的文件加载有关的。比如我们的可执行文件加载到内存,或者一些像libc.so共享库,也或者是普通一个txt。

图片描述

图片描述

图片描述
当一个文件要加载到内存中时,实际上在虚拟内存中开辟了一开虚拟内存空间,这个区域的地址是指向了一个页表,这个页表类似于虚拟内存那样与磁盘的内容进行关联。比如现在要读取某共享库的一个地址的内容,该地址是位于进程的虚拟内存的共享库的区域内,而这个地址指向的是一个内存映射的页表PTE。如同虚拟寻址那样,如果该PTE是无效的或者是指向文件中的某个地址,则会发生缺页异常,然后会就在物理内存中选中一个牺牲的物理页替换成这个待读取的页。加载到物理页完毕后,就可以在物理页中读取想要的数据。
内存映射有利于节省内存空间,现在如libc.so这样的文件加载到内存中,如果每个进程都加载一份到内存中那回造成浪费,现在利用内存映射,每个进程都可以把他们共同需要的libc.so映射到同一片的内存区域。

图片描述

。另外一种场景,假设两个进程A和B都打开了某个文件,现在有一个进程B需要写文件的内容,但是另外一个A进程完全不能受到这次写的影响,那进程B则会在物理内存中拷贝一份跟原有页一样的内容,把虚拟内存指向到这个新的页。

图片描述

用户态的内存映射实际上利用了一个叫mmap的函数。函数的定义如下
void* mmap(void* start ,size_t length , int prot , int flags , int fd , off_t offset);Start是待映射的虚拟内存的起始地址length是映射的长度
fd是文件描述符,指定了磁盘文件中的起始地址
offset是最后一个参数,他指定了开始映射的地址距离文件起始地址的偏移量。prot是映射后虚拟内存的访问权限,这个权限是可读可写之类的flag指定了映射的对象类型,这些类型是匿名对象,共享对象,私有对象之类的。本篇属于个人学习完的总结。文中有摘抄了网上或书籍中的图片。如有错误的请及时指出,以便本人及时更正。谢谢