为什么要虚拟内存
我们知道,在之前上微机原理时,我们的程序是可以直接访问内存的,而且访问的是直接的物理内存,在实模式下,寄存器是16位的,数组总线(data bus)是16位的,但地址总线是20位的,这带来了一个问题,需要两个寄存器去访问一个数据,为什么地址中心不是16位呢,那样不就好了,不是不想,是不够用啊,16位的地址总线才能访问64KB内存。所以用CS:IP
两个寄存器去访问内存。
这样做有什么问题呢,碎片。碎片分为内部碎片和外部碎片,内部碎片是指已经被分配但是没有被使用的地址空间,比如你申请了7bit但是内存为了对齐不得不给你分配8bit的空间,这就产生了1bit的碎片,外部碎片是指未分配且未使用的地址空间,申请4字节的Int类型,再申请8字节的long类型,为了内存对齐,其中4字节无法装入8字节类型,这就产生了4字节的外部碎片。
内部碎片是已经被分配的内存,是操作系统不可以利用的空间,但是外部碎片是未被分配的,但是该空间过小,无法装载资源,无法被利用,外部碎片是可以解决的。
为了解决内存的碎片化问题,计算机专家们提出了分段式管理的思想,但是再说分段之前需要说一下早期的虚拟内存模型
早期虚拟内存模型
在经历了纯物理地址后,科学家们期望解决这种内存模型难以统一的问题,于是虚拟内存技术孕育而生,但困扰科学家们的是,如何将虚拟地址转换成物理地址。早期的科学家想到的是将整个程序作为一个整体,并为每个进程分配一个基址寄存器和界限寄存器,基址寄存器存放该虚拟地址在实际物理地址的起点,界限寄存器用来判定程序是否访问非法地址,通过这种方式,实际的地址是很好计算的(实际地址=虚拟地址+基址),这种方式虽然解决了地址翻译问题,但是产生了大量的内部碎片。
从图中可以看出,如果我们将整个地址空间放入物理内存,那么栈和堆之间的空间并没有被进程使用,却依然占用了实际的物理内存。因此,简单的通过基址寄存器和界限寄存器实现的虚拟内存很浪费。
另外,我们必须要确保内存足够放得下进程的虚拟地址空间,但通常主存成本是比较昂贵的,不如磁盘廉价,这种方式通常不支持大的虚拟地址,如果剩余物理内存无法提供连续区域来放置完整的地址空间,进程便无法运行。例如现在32位的进程空间通常是4GB,主存根本就装不下几个进程。
所以我们需要物尽其用,早起的科学家们想到了分段这种思想
分段式管理的思想
分段思想其实就是将基址加界限的概念泛化,在上述例子中,我们为代码、堆、栈分别设置一个段基址加段界限寄存器,这样我们不必要每次都强制装入整个进程空间,每个基址寄存器存放在该段在物理地址的实际空间,界限寄存器用来保护地址空间,对程序未使用的空间我们没必要为其分配,我们仍需要为堆分配较多的内存,除了堆段,其余空间都是在编译期间就确定了的,这样便大大提高了内存的利用率,而且我们发现可以离散的分配内存空间,即物理内存中的地址不必要时连续的,能大大提高对物理地址的利用率。
分段地址的转换
分段式的思想讲完了,但是还有一些问题,地址怎么转换,其实分段地址转换与基址加界限的思想差不多,操作系统通过维护一个段表来维护各段的信息
段表的地址是操作系统维护的,段表项主要维护段长和段基址,段基址指该段在物理内存中的起始地址,那么该段中的虚拟地址对于实际地址为 段基址+段内偏移
分段系统的逻辑地址结构是段号(段名) 和 段内地址(段内偏移量)组成的,下图很好的说明了分段是怎么找到实际内存地址的。
你可能发现了,虚拟地址中每个段的起始地址都是固定的,每个段的总大小也是固定的,其大小为 2 p 2^p 2p 字节 其中 p p p 为段内地址的位数。此外,栈是反向增长的,因此段表中必须维护一个比特位,描述是否为栈段。
分段的优点
**1、**生成了虚拟内存,避免了程序直接访问物理内存
**2、**很大程度上避免了内存浪费
**3、**可以很好的支持共享
随着分段机制的不断改进,系统设计人员很快意识到,通过再多一点的硬件支持,就能实现新的效率提升。具体来说,要节省内存,有时候在地址空间之间共享(share)某些内存段是有用的。尤其是,代码共享很常见,今天的系统仍然在使用。
为了支持共享,需要一些额外的硬件支持,这就是保护位(protection bit)。基本为每个段增加了几个位,标识程序是否能够读写该段,或执行其中的代码。通过将代码段标记为只读,同样的代码可以被多个进程共享,而不用担心破坏隔离。
**4、**虚拟地址翻译太慢
我们每次翻译一个虚拟地址都需要去找寻段表中的段表项,相当于多一次地址访问,这太慢了!解决方案是为计算机设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问段表。这种设备称为转换检测缓冲区 (Translation Lookaside Buffer,TLB),有时又称为快表。快表是一个小的高速缓存,现代操作系统无论是分段还是分页中都利用了这种软件技术。
分段的缺点
**1、**太多的外部碎片
例如4kb的空间装入3kb的段,产生的1kb的空间无法在装入任何段,产生碎片的主要原因是因为分段使用的大小是不确定的。外部碎片可通过紧凑的方式以合成较大的空闲空间,但这需要大量成本,操作系统难以维护。由于这个原因分页思想诞生
分页式管理的思想
分段式管理一段时间后主存上会遍布着大大小小的外部碎片,操作系统难以维护,分页的思想就是将空间划分成较小的,固定长度的分片,这就是分页式管理,分页式管理将程序资源划分为固定大小的页,将每一个虚拟页映射到物理页之中,由于每个页是固定大小的,操作系统可以整齐的分配物理内存空间,避免产生了外部碎片,例如一个页大小是4kb,而主存是40kb,操作系统稍加管理便能确保无论何时都能整齐的装入10个页面。
页在物理内存中也不是连续存在的,进程未使用的页也没有必要为其分配内存,通过这种方式我们就解决了由分段产生的大量外部碎片问题,同时由于页比较小,只有在已使用的页才会产生较少的内部碎片,这是可以接受的,目前来看,分页是一种很好的办法。
分页地址的转换
分页地址的转换与分段地址转换一致,为页基址+页内偏移,页表是操作系统维护的,操作系统知道页表的起始位置,页表项大小是固定的,在32位地址空间中通常是8字节,这64bit中不仅存储了页基址,还存放着一些其他重要的数据。
假设我们有一个线性地址,已经经过了分段机制的转换,其地址为二进制表示0000000011_0100000000_000000000000
,那他的转换过程就如下图所示
高 10 位负责在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。
现在我们是不是出现了很多关于地址的名词,逻辑地址,线性地址,物理地址,虚拟地址,这我们就不得不讲讲Intel管理内存的两板斧了,分段与分页,对就是上面说的,进入保护模式后,分段就必须打开了,分页不是必须的,
分段机制的目的是为每个程序或者任务提供单独的代码段数据段和栈段,使其不会相互干扰。
分页机制的目的使程序可以按需取用内存,也可以在多任务的时候起到隔离内存空间的作用
逻辑地址:我们程序员写代码时给出的地址叫逻辑地址,其中包含段选择子和偏移地址两部分。
线性地址:通过分段机制,将逻辑地址转换后的地址,叫做线性地址。而这个线性地址是有个范围的,这个范围就叫做线性地址空间,32 位模式下,线性地址空间就是 4G。
物理地址:就是真正在内存中的地址,它也是有范围的,叫做物理地址空间。那这个范围的大小,就取决于你的内存有多大了。
虚拟地址:如果没有开启分页机制,那么线性地址就和物理地址是一一对应的,可以理解为相等。如果开启了分页机制,那么线性地址将被视为虚拟地址,这个虚拟地址将会通过分页机制的转换,最终转换成物理地址。