《操作系统导论》这本书写的真的好,下面是我对内存虚拟化内容的一些理解。
内存虚拟化的目的
使得多个进程可以共享内存,并且使得内存易于使用,每个进程认为自己独占了物理内存,内存分配对进程是透明的(注意,此处的透明指的是好像不存在)。
注意:这意味着所有进都是访问的虚拟地址而不是实际的物理地址。
设计目标
易用:内存分配等一些列内容对进程是透明的,易于使用。
保护:应该使得一个进程不能访问其他进程的内容(否则可能有严重错误)
高效:追求高效的虚拟内存系统,包括时间和空间(各种机制和策略都可以理解为是对着一个目标的追求)
一些基本机制
硬件
为了追求效率,用硬件完成虚拟地址到实际物理地址的转换。比如最简单的虚拟化方法中,只需要使用一对基址/界限寄存器即可完成转换,当虚拟地址小于界限时:
物
理
地
址
=
基
址
+
虚
拟
地
址
物理地址=基址+虚拟地址
物理地址=基址+虚拟地址
操作系统
操作系统负责物理内存的管理,要能响应对应的错误(如越界错误),还需要在进程切换时修改硬件记录的内容(比如要把基址/界限寄存器从进程A改为进程B)
最简单的方法:一对基址/界限寄存器
假设每个进程的地址空间可以全部放到物理内存里,每个进程都认为自己的地址空间从0开始,且把地址空间连续地映射到物理内存中。那么只需要知道该进程的地址空间在物理内存的起始地址和占用大小即可实现虚拟地址到物理地址的转换,并且实现保护机制。
此时 物 理 地 址 = 基 址 + 虚 拟 地 址 物理地址=基址+虚拟地址 物理地址=基址+虚拟地址 ,当虚拟地址大于界限寄存器的值时应当触发异常,由操作系统终止该进程。
问题
首先把整个地址空间放入操作系统,会导致大量内部碎片。我们知道进程的地址空间由代码、栈和堆组成,其中栈和堆在地址空间的两端且增长方向相反。这样栈和堆之间会有大量没有用到的区域,但却占据了物理内存,导致大量内部碎片。
其次,由于进程的地址空间连续地映射到物理空间,可能会导致大量的外部碎片。这点可见具体的空闲管理策略。
优化(避免内部碎片):分段
显然上一种方法效率太低,造成了大量物理内存的浪费,我们考虑如何在空间上进行优化。可以把程序的地址空间分为三个部分,代码区域、栈区域和堆区域,每个区域独立地分配到物理内存中去。显然这样做避免了栈和堆之间的空闲区域,因为我们不必为这一段没有用到的地址空间分配内存。
操作系统在切换进程时必须修改每一对寄存器的值。
地址转换
分段会给地址转换打来一些问题,由于三个段独立分配,显然我们需要三对基址/界限寄存器来记录每一段在物理空间的位置和大小。但要注意,进程认为自己的地址空间是连续的,且栈是反向增长的(从高地址向低地址),所以地址转换要进行一些处理。
如何区分段
①显式方式,把虚拟地址的前几位作为段号,用以选择基址/界限寄存器。
②隐式方式,硬件根据地址产生的方式确定段。
计算物理地址
注意栈段的基址寄存器,记录的是栈所在物理内存的最高地址
堆
物
理
地
址
=
堆
段
基
址
+
(
虚
拟
地
址
−
段
起
始
虚
拟
地
址
)
堆物理地址= 堆段基址 + (虚拟地址-段起始虚拟地址)
堆物理地址=堆段基址+(虚拟地址−段起始虚拟地址)
栈
物
理
地
址
=
栈
段
基
址
+
(
虚
拟
地
址
−
栈
最
大
虚
拟
地
址
)
栈物理地址 = 栈段基址 + (虚拟地址-栈最大虚拟地址)
栈物理地址=栈段基址+(虚拟地址−栈最大虚拟地址)
代
码
物
理
地
址
=
代
码
段
基
址
+
虚
拟
地
址
代码物理地址=代码段基址+ 虚拟地址
代码物理地址=代码段基址+虚拟地址
问题:
分段方法中,操作系统管理物理内存的空闲区域,由于不同段需要各自连续存储在物理内存中,不同进程的段的大小各不相同,会出现大量外部碎片(因为过小而无法分配)。
优化(避免外部碎片):分页
为什么分段会有外部碎片呢?因为我们是按每个段来分配物理内存,而段大小是不同的,且每个段连续地分配到物理内存中,这样就导致可能会有一些空间无法利用。解决方法就是把地址空间和物理空间分割成固定大小的单元,用这些固定单元进行内存分配,这样所有的物理空间都可以利用起来(这样又会导致内部碎片,所以每个模块的大小需要仔细设计)。我们把这个固定大小的单元称为页。
这样做最大的优点就是灵活,地址空间不必连续的映射到物理内存中,实际上地址空间的每一个页可以映射到物理内存的任意一个页。
地址转换
这样做地址转换会更加复杂,不能再简单地使用基址/界限寄存器了,必须为每个进程保存其虚拟页到物理页的映射关系,并能够从虚拟地址转化为物理地址。
页表
物理地址计算
问题
速度太慢
页表太大