虚拟存储器(Virtual Memory)
- 它的基本思想是对于一个程序来说,它的程序(code)、数据(data)和堆栈(stack)的总大小可以超过实际物理内存的大小;
- 操作系统把当前使用的部分内容放到物理内存中,而把其他未使用的内容放到更下一级存储器,如硬盘(disk)或闪存(flash)上。
- 举例来说,一个大小为 32MB 的程序运行在物理内存只有 16MB 的机器上
- 操作系统通过选择,决定各个时刻将程序中一部分16MB 的内容(或者更小)放在物理内存中,
- 将其他的内容放到硬盘中,并在需要的时候在物理内存和硬盘之间交换程序片段
- 这样就可以把大小为32MB的程序放到物理内存为16MB的机器上运行,而且在运行之前也不需要程序员对程序进行分割。
有了上面的概念,此时就可以说,一个程序是运行在虚拟存储器空间的,它的大小由处理器的位数决定,例如对于一个32位处理器来说,其地址范围就是0~0xFFFF FFFF,也就是4GB,而对于一个64位处理器来说,其地址范围就是0-OxFFFF FFFF FFFF FFFF;
- 这个范围就是程序能够产生的地址范围;
- 其中的某一个地址就称为虚拟地址;
和虚拟存储器相对应的就是物理存储器,它是在现实世界中能够直接使用的存储器,其中的某一个地址就是物理地址。
- 物理存储器的大小不能够超过处理器最大可以寻址的空间
- 例如,对于32位的x86 PC来说,它的物理存储器(一般都简称为内存)可以是256MB,即PM的范围是0~0xFFF FFFF
- 当然,也可以将物理内存增加到4GB,此时虚拟存储器和物理存储器这两个地址空间的大小就是相同的,在当前使用的32位×86 PC中,不可能使用比4GB更大
如上图所示,如果使用了VA, 则此时CPU产生的地址,不能直接送给物理存储器当中,而是需要先进行地址转换,负责地址转换的部件称之为MMU;
使用VA的好处:
- 使用虚拟存储器不仅可以便于程序在处理器中运行,还可以给程序的编写带来好处;
- 在直接使用物理存储器的处理器中,如果要同时运行多个程序,就需要为每个程序都分配一块地址空间,每个程序都需要在这个地址空间内运行,这样极大地限制了程序的编写,而且不能够使处理器随便地运行程序
- 对于大型系统,我们也无法事先限制软件运行的地址空间的,这时候就需要虚拟存储器了。
- 使用它之后,每个程序总是认为它独占处理器的所有地址空间,因此程序可以任意使用处理器的地址资源,这样在编写程序的时候,不需要考虑地址的限制,每个程序都认为处理器中只有自己在运行;
- 当这些程序真正放到处理器中运行的时候,由操作系统负责调度,将物理存储器动态地分配给各个程序,将每个程序的虚拟地址转化为相应的物理地址。
- 通过操作系统动态地将每个程序的虚拟地址转化为物理地址,还可以实现程序的保护。
- 即使两个程序都使用了同一个虚拟地址,它们也会对应到不同的物理地址,因此可以保证每个程序的内容不会被其他的程序随便改写。
- 而且,通过这样的方式,还可以实现程序间的共享,例如操作系统内核提供了打印(printf)函数,第一个程序在地址A使用了printf函数,第二个程序在地址B使用了printf函数,操作系统在地址转换的时候,会将地址A和B都转换为同样的物理地址,这个物理地址就是printf函数在物理存储器中的实际地址,这样就实现了程序的共享,虽然两个程序都使用了printf 函数,但是没必要真的使printf函数占用物理存储器的两个地方
- 因此,使用虚拟存储器不仅可以降低物理存储器的容量需求,它还可以带来另外的好处,如保护(protect)和共享(share)。
地址转换
基于page的virtual memory
当前大多数的处理器都使用了这种分页机制,虚拟地址空间的划分以页(page)为单位,典型的页大小为4KB,相应的物理地址空间也进行同样大小的划分。
- 由于历史的原因,在物理地址空间中不叫做页,而称为frame,它和页的大小必须相等。
- 它们的典型值都是4KB,当程序开始运行时,会将当前需要的部分内容从硬盘中搬移到物理内存中,每次搬移的单位就是一个页的大小,因此页和frame的大小也就必须相等了。
demand page
只有在需要的时候才将一个页的内容放到物理内存中,这种方式就称为demand page;
它使处理器可以运行比物理内存更大的程序。
术语解析
- 对于一个虚拟地址VA来说,VA[11:0]用来表示页内的位置,称为page offset,
- VA剩余的部分用来表示哪个页,也称为VPN(Virtual Page Number)
- 相应的,对于一个物理地址PA来说,PA[11:0]用来表示frame内的位置,称为frame offset
- 而PA剩余的部分用来表示哪个frame,也称为PFN(Physical Frame Number),
- 由于页和frame的大小是一样的,所以从VA到PA的转化实际上也就是从VPN到PFN的转化,offset的部分是不需要变化的。
单级页表
page fault
- 当需要的page不在physical mem中时,此时就会产生page fault类型的异常,需要访问下一级mem(硬盘等);
- 硬盘访问需要的时间,都是ms级别的,严重降低了处理器的性能,所以我们要尽量较少page fault的频率;
解决方式
- 用一张表格来存储从VA到PA(实际是VPN->PFN)的映射关系,称之为page table, PT;
- 这个表格通常放到物理内存中;
- 每个程序都有自己的PT, 用来将这个程序的虚拟地址映射到物理内存中的某个地址
- 每次操作系统将一个程序调入物理内存中执行时,会将该程序的页表的起始地址放入一个寄存器中;
- 这个寄存器称之为page table register;
- 这种方式,要求页表是放在物理内存中的一块连续的地址空间;
具体实例
- PTR存放的是一个物理地址,该地址指向了当前程序的页表的物理起始地址;
- 使用PTR找到该页表的起始地址后,在使用VA,找到该VA在页表中对应的entry;
- 找到entry后,如果valid为1:
- 说明这个虚拟地址所在的page, 已经被操作系统映射到了物理内存中,查找映射,就可以得到该VA对应的PA是多少;
- 拿到该PA后,就将之前VA的访问,转换成了PA的访问,完成读写操作;
- 如果valid为0:
- 说明此时VA对应的4K空间的page, 还没有被操作系统映射到物理内存中(为了节省物理内存的使用,操作系统只会把那些当前被使用的页映射到物理内存中)
- 产生page fault的excp, 操作系统进入excp handler,;
- 在excp handler中,异常服务程序:
- 会从下一级的mem中,将这个page对应的4KB内容搬移到物理内存中;
- 同时在PT中,建立好该page对应的物理地址的映射关系;
页表大小问题
PT中的一个entry, 可以映射4KB大小,为了能够映射整个4G空间,则页表entry的个数为4GB/4KB=1M,也就是说,需要1M个entry;
每个entry中,会存放如下信息:
- valid
- PFN;(不需要存offset,因为page内部的offset同虚拟地址的offset)
- page的一些属性信息(可读可写)等;
所以,基本上会把物理内存32bit位宽都用上,因此,总的PT的空间为:1M * 4B = 4MB;
进程的组成与概念
进程(process)的状态由下面几类组成:
- 该进程对应的页表;
- PC;
- 通用寄存器
进程分为两个状态:
- inactive: 进程未被处理器执行;
- active: 操作系统通过将一个进程的状态加载到处理器中,就可以使这个进程进入活跃的状态。
可以说,进程是一个动态的概念,当一个程序只是放在硬盘中,并没有被处理器执行的时候,它只是一个由一条条指令组成的静态文件,只有当这个程序被处理器执行时,例如用户打开了一个程序,此时才有了进程;
需要操作系统为其分配物理内存中的空间,创建页表和堆栈等,这时候一个静态的程序就变为了动态的进程,这个进程可能是一个,也可能是多个,这取决于程序本身。当然,进程是有生命期的,一旦用户关闭了这个程序,进程也就不存在了,它所占据的物理内存也会被释放掉。
当一个进程进行状态保存时,只需要保存页表的PTR即可;
访问速度问题
页表放到物理内存中,这样要得到一个虚拟地址对应的数据,需要访问两次物理内存。
- 第一次访问物理内存中的页表,得到对应的物理地址;
- 第二次使用这个物理地址来访问物理内存,这样才能得到需要的数据;
因为要访问物理内存,所以这样的速度是很慢的,我们一般会用TLB和Cache来加速这一过程;
多进程页表空间问题
前文讲过,单个进程的页表大小为4M(32位系统), 并且是连续的空间;
如果由多个进程,通常上百个时,每个进程都要连续的4MB空间来存储页表,这显然是不可能的;
要解决这个问题,就要用到下面介绍的多级页表的概念;
多级页表
如图,这里我们将上面描述的4MB空间,划分成多个更小的页表,称之为子页表;
处理器在执行进程的时候,不需要一下把整个线性页表都放入物理内存中,而是根据需求逐步的放入子页表;
并且,这样有个好处,4MB的页表,不再需要占用连续的物理内存空间了;
举例来说,对于一个32位虚拟地址、页大小为4KB的系统来说,如果采用线性页表,则页表中的表项个数是2**20,将其分为1024(2**10)等份,每个等份就是一个第二级页表,共有1024 个第二级页表,对应着第一级页表中的 1024 个表项。也就是说,第一级页表需要 10位地址进行寻址。每个二级页表中,表项的个数是 2**20/2**10=2**10个,也需要 10 位地址才能寻址第二级页表,如图 3.8 所示为上述过程的示意图。
工作流程如下所示:
- 页表中的表项,称之为PTE;
- 当操作系统创建一个进程时,就会在物理内存中,为这个进程找到一块连续的4K空间(第一级页表),存放第一级页表;
- 第一级页表创建后,将第一级页表的物理地址,放入PRT(通常是一个特殊的寄存器);
- 此时随着程序的进行,查找第一级页表,valid为0,产生page fault;
- 处理器进入异常服务程序,进行如下处理:
- 创建第二级页表,并将第二级页表的物理地址,写入第一级页表对应的entry中;
再根据VA,(不是VA, 而是软件分配好的该VA对应的外部存储器的地址,怎么获取,后文会讲)将该VA对应的page写到物理内存中,同时将page的物理地址,填入第二级页表中;
- 退出异常服务程序,再次用该VA进行访问,此时能够找到对应的物理地址(PFN), 可以正常访问;
这样做的好处
1. 对于同样大小的程序,如果程序中使用不同的虚拟地址,此时页表的存储空间,会大不相同;
考虑一个4MB大小的程序,在32bit系统中执行,采用多级页表,那么我们需要多少空间来存储页表呢?
我们考虑两种极端情况:
- 最好情况
VA连续,变化范围是Ox00000000~0x003FFFFF,则地址的高10bit,一直是0,中间10bit, 0x000~0x3FF,正好占用了一个完整的二级页表;
所以页表所占用的空间为,一个一级页表,一个二级页表,4K+4K=8KB;
- 最坏情况
VA不连续,而是每一个4KB都分布在每一个4MB空间内;
这样分布,会导致每一个VA,都会在一个4MB空间,也就是说,第一级页表所有的entry, 都有效,也就意味着,所有的二级页表都有效,则页表所占用的空间为:
4KB + 1024*4KB = 4100KB;
2. 具备极强的扩展性;
当处理器的位数增加时,可以通过增加级数的方式来减少页表对于物理内存的占用
使用VA的优点
- 让每个程序都有独立的地址空间,例如在32位的处理器中,每个程序都认为自己拥有整个 4GB 的地址空间;
- 引入虚拟地址到物理地址的映射,为物理内存的管理带来了方便,可以更灵活地对其进行分配和释放,在虚拟存储器上连续的地址空间可以映射到物理内存上不连续的空间。
- 在处理器中如果存在多个进程,为这些进程分配的物理内存之和可能大于实际可用的物理内存,虚拟存储器的管理使得这种情况下各个进程仍能够正常运行;
当物理内存不够用时,将物理内存中的一些不常用的页保存到硬盘上的swap空间;
而需要用到这些页时,再将其从硬盘的swap空间加载到物理内存;
因此,处理器中等效可以使用的物理内存的总量是物理内存的大小+硬盘中 swap 空间的大小。
- 利用虚拟存储器,可以管理每一个页的访问权限;
Page Fault
使用页表将虚拟地址转换为物理地址,如果一个进程中的虚拟地址在访问页表时,发现对应的PTE中,有效位(valid)为0,这就表示这个虚拟地址所属的页还没有被放到物理内存中,因此在页表中就没有存储这个页的映射关系,这时候就说发生了Page Fault;
此时需要从下级存储器,例如硬盘中,将这个页取出来,放到物理内存中,并将这个页在物理内存中的起始地址写到页表中;
该excp由软件处理
(1) 由于 Page Fault时要访问硬盘,这个过程需要的时间很长,通常是毫秒级别,和这个“漫长”的时间相比,即使 Page Fault 对应的异常处理程序需要使用几百条指令,这个时间相比于硬盘的访问时间也是微乎其微的,因此没必要使用硬件来处理 Page Fault。
(2)发生 Page Fault 时,需要从硬盘中搬移一个或几个页到物理内存中,当物理内存中没有空余的空间时,就需要从其中找到一个最近不经常被使用的页,将其进行替换,使用软件可以根据实际情况实现灵活的替换算法,找到最合适的页进行替换。如果使用硬件的话,很难实现复杂的替换算法,而且不能够根据实际情况进行调整,缺少灵活性。
page_fault时怎么知道在外部存储器中的页表地址
需要注意的是,直接使用虚拟地址并不能知道一个页位于硬盘的哪个位置,也需要一种机制来记录一个进程的每个页位于硬盘中的位置。
通常,操作系统会在硬盘中为一个进程的所有页开辟一块空间,这就是之前说过的 swap空间,在这个空间中存储一个进程所有的页,操作系统在开辟swap空间的同时,还会使用一个表格来记录每个页在硬盘中存储的位置;
这个表格的结构其实和页表是一样的,它可以单独存在,从理论上来讲当然也可以和页表合并在一起。
如图 3. 14 所示在一个页表内记录了一个进程中的每个页在物理内存或在硬盘中的位置:
- 当页表中某个PTE的有效位(valid)为 1时,就表示它对应的页在物理内存中,访问这个页不会发生 Page Fault;
- 相反,如果有效位是 0,则表示它对应的页位于硬盘,访问这个页就会发生Page Fault,此时操作系统需要从硬盘中将这个页搬移到物理内存中,并将这个页在物理内存中的起始地址更新到页表中对应的PTE内
虽然从图 3.14看来,映射到物理内存的页表和映射到硬盘的页表可以放到一起,但是在实际当中,物理上它们仍然是分开放置的,因为不管一个页是不是在物理内存中,操作系统都必须记录一个进程的所有页在硬盘中的位置,因此需要单独地使用一个表格来记录它。
page内容的改变
在前文已经说过,物理内存相当于是硬盘的Cache;
因为对一个程序来说,它的所有内容其实都存在于硬盘中,只有最近被使用的一部分内容存在于物理内存中,这样符合Cache的特征。
因为一个程序的某些内容既存在于硬盘中,又存在于物理内存中,当物理内存中某个地址的内容被改变时(例如执行了一条 store指令,改变了程序中的某个变量),对于这个地址来说,在硬盘中存储的内容就过时了,这种情况在Cache中也出现过,有两种处理方法。
(1)写通(Write Through),将这个改变的内容马上写回硬盘中,考虑到硬盘的访问时间非常慢,这样的做法是不现实的。
(2)写回(Write Back),只有等到这个地址的内容在物理内存中要被替换时,才将这个内容写回到硬盘中,这种方式减少了硬盘的访问次数,因此被广泛地使用。
其实,写通(Write Through)的方式只可能在 L1 Cache 和 L2 Cache 之间使用,因为 L2Cache的访问时间在一个可以接受的范围之内,而且这样可以降低Cache一致性的管理难度,
但是更下层的存储器需要的访问时间越来越长,因此只有写回(Write Back)方式才是可以接受的方法。
如何记录page内容的改变?
既然在虚拟存储器的系统中采用了写回的方式,当发生Page Fault并且物理内存中已经没有空间时,操作系统需要从其中找到一个页进行替换,如果这个页中的某些内容被修改过,那么在覆盖这个页之前,需要先将它的内容写回到硬盘中,然后才能进行覆盖。
为了支持这个功能,需要记录每个页是否在物理内存中被修改过,通常是在页表的每个PTE中增加一个脏(dirty)的状态位,当一个页内的某个地址被写入时,这个脏的状态位会被置为1。
当操作系统需要将一个页进行替换之前:
- 会首先去页表中检查它对应PTE的脏状态位;
- 如果为1,则需要先将这个页的内容写回到硬盘中;
- 如果为0,则表示这个页从来没有被修改过,那么就可以直接将其覆盖了,因为在硬盘中还保存着这个页的内容。
- 从一个时间点来看物理内存,会存在很多的页处于脏的状态,这些页都被写入了新的内容;
- 当然,一旦这些脏的页被写回到硬盘中,它们在物理内存中也就不再是脏的状态了。
需要注意的是:
- 在写回(Write Back)类型的Cache中,load/store 指令在执行的时候,只会对 D-Cache起作用,对物理内存中页表的更新可能会有延迟
- 当操作系统需要查询页表中的这些状态位时,首先需要将D-Cache中的内容更新到物理内存中,这样才能够使用到页表中正确的状态位。
页表的替换
复杂一点,可以采用LRU算法;
简单一点,在PTE中,增加一个use bit,用来记录每个page最近是否使用过;
- 最近访问过,置1;
- 否则,保持0;
系统会周期性的清零;
操作流程总结
正常场景
- 处理器送出的虚拟地址(VA)首先送到 MMU中。
- MMU使用页表的基址寄存器PTR和VA[31:12]组成一个访问页表的地址,这个地址被送到物理内存中。
- 物理内存将页表中被寻址到的 PTE 返回给 MMU.
- MMU判断PTE中的有效位,发现其为1,也就表示对应的页存在于物理内存中,因此使用PTE中的PFN和原来虚拟地址的[11:0]组成实际的物理地址,即PA=(PFN, VA[11:0]},并用这个地址来寻址物理内存,得到最终需要的数据。
也就是说,在使用单级页表的情况下,要得到一个虚拟地址对应的数据,需要访问两次物理内存。
Page Fault场景
1,2,3这三个步骤和上面的情况是一样的,处理器送出虚拟地址到 MMU,MMU 使用PTR和VA[31:12]组成访问页表的地址,从物理内存中得到对应的PTE,送回给MMU
4. MMU查看PTE当中的有效位,发现其为0,也就是需要的页此时不在物理内存当中,此时MMU会触发一个Page Fault类型的异常送给处理器,这会使处理器跳转到PageFault对应的异常处理程序中,在这一步,MMU还会把发生Page Fault的虚拟地址VA也保存到一个专用的寄存器中,供异常处理程序使用。
5. 假设此时物理内存中已经没有空闲的空间了,那么 Page Fault 的异常处理程序需要按照某种算法,从物理内存中找出一个未来可能不被使用的页,将其替换,这个页称为Victim page,如果这个页对应的脏状态位是1,表示这个页中的内容在以前曾经被修改过,因此需要首先将它从物理内存写到硬盘中,当然,如果脏状态位是0,那么就不需要写回硬盘这个过程了。
6. Page Fault 的异常处理程序会使用 MMU刚才保存的虚拟地址 VA 来寻址硬盘,找到对应的页,将其写到物理内存中Victim page所在的位置,并将这个新的映射关系写到页表中,这里需要注意的是,寻址硬盘的时间是很长的,通常是毫秒级别,因此这一步的处理时间也是很长的。
7. 从 Page Fault 的异常处理程序中返回的时候,那条引起 Page Fault 的指令会被重新取到流水线中执行,此时处理器会重新发出虚拟地址送到MMU中,因为所需要的页已经被放到物理内存中了,因此这次访问肯定不会发生Page Fault,会按照不发生Page Fault时的过程进行处理。
程序保护
小结
到目前为止,总结起来,在页表中的每个 PTE都包括如下的内容。
(1) PFN,表示虚拟地址对应的物理地址的页号;
(2) Valid,表示对应的页当前是否在物理内存中;
(3) Dirty,表示对应页中的内容是否被修改过;
(4) Use,表示对应页中的内容是否最近被访问过;
(5) AP,访问权限控制,表示操作系统和用户程序对当前这个页的访问权限;
(6) Cacheable,表示对应的页是否允许被缓存。