物理内存与地址空间
# 为什么会有地址空间?
如果把物理地址直接暴露给进程会带来几个严重的问题:
第一,如果用户程序可以寻址内存的每个字节,那么它们可以很容易故意地或者偶然地破坏操作系统,从而使系统慢慢地停止。
第二,在这种情况下,要想在内存中同时运行两个程序是不可能的。如果第一个程序在2000的位置写入一个新的值,将会擦掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的,这两个程序会立刻崩溃。
为了保证多个应用程序同时处于内存且相互不会受到影响,所以引入了地址空间这种内存抽象。地址空间是一个进程可用于寻址内存的一套地址集合,每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间。
# 如何给每个进程分配一个自己的地址空间?
要使得一个进程中的地址所对应的地址空间与另一个程序中的地址所对应的物理地址不同,需要使用动态重定位方法。
所谓的动态重定位,就是给每个CPU配置两个特殊的硬件寄存器,通长叫做基址寄存器和界限寄存器。通过这两个寄存器,程序装载到内存中连续的空闲位置且装载期间无需重定位,而只需要将程序的起始物理地址装载到基址寄存器,并将程序的长度装载到界限寄存器中即可。
每次一个进程访问内存,取一条指令,读或写一个数据字,CPU硬件会在把地址发送到内存总线前,自动把基址值加到进程发出的地址值上。
# 内存交换技术
如果计算机物理内存足够大,可以保存所有进程,那么之前提及的所有方案都或多或少是可行的。但实际上,所有进程所需的RAM数量总和通常要远远超出存储器能够支持的范围。
当前重要的应用程序能轻易地占据50~200MB甚至更多的空间。因此,把所有进程一直保存在内存中需要巨大的内存,如果内存不够,就做不到这一点。
有两种处理内存超载的通用方法。最简单的策略是交换(swapping)技术,即把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。空闲进程主要存储在磁盘上,所以当它们不运行时就不会占用内存(尽管它们的一些进程会周期性地被唤醒以完成相关工作,然后就又进入睡眠状态)。另一种策略是虚拟内存(virtual memory),该策略甚至能使程序在只有一部分被调入内存的情况下运行。
# 空闲内存管理
在动态分配内存时,操作系统必须对其进行管理。一般而言,有两种方式跟踪内存使用情况:位图和空闲链表。
# 使用位图的存储管理
使用位图方法时,内存可能被划分成小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0表示空闲,1表示占用(或者相反)。一块内存区和其对应的位图如图3-6所示。
因为内存的大小和分配单元的大小决定了位图的大小,所以它提供了一种简单的利用一块固定大小的内存区就能对内存使用情况进行记录的方法。
这种方法的主要问题是,在决定把一个占k个分配单元的进程调入内存时,存储管理器必须搜索位图,在位图中找出有k个连续0的串。查找位图中指定长度的连续0串是耗时的操作(因为在位图中该串可能跨越字的边界),这是位图的缺点。
# 使用链表的存储管理
另一种记录内存使用情况的方法是,维护一个记录已分配内存段和空闲内存段的链表。链表中的每一个结点都包含以下域:空闲区(H)或进程(P)的指示标志、起始地址、长度和指向下一结点的指针。
在本例中,段链表是按照地址排序的,其好处是当进程终止或被换出时链表的更新非常直接。一个要终止的进程一般有两个邻居(除非它是在内存的最底端或最顶端),它们可能是进程也可能是空闲区,这就导致了图3-7所示的四种组合。
# 动态分区分配算法
当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以用来为创建的进程(或从磁盘换入的已存在的进程)分配内存。这里,假设存储管理器知道要为进程分配的多大的内存。
首次适配(first fit)算法:空闲分区按地址递增连接,存储管理器沿着段链表进行搜索,直到找到一个足够大的空闲区,除非空闲区大小和要分配的空间大小正好一样,否则将该空闲区分为两部分,一部分供进程使用,另一部分形成新的空闲区。
缺点:地址部分被不断划分,会留下许多难以利用的,很小的空闲分区即碎片。
下次适配(next fit)算法:它的工作方式和首次适配算法相同,不同点是每次找到合适的空闲区时都记录当时的位置。以便在下次寻找空闲区时从上次结束的地方开始搜索,而不是像首次适配算法那样每次都从头开始。
最佳适配(best fit)算法:按容量大小进行链接,最佳适配算法搜索整个链表(从开始到结束),找出能够容纳进程的最小的空闲区。最佳适配算法试图找出最接近实际需要的空闲区,以最好地区配请求和可用空闲区,而不是先拆分一个以后可能会用到的大的空闲区。
也会产生碎片
最差适配(worst fit)算法:容量大小从大到小,总是分配最大的可用空闲区,使新的空闲区比较大从而可以继续使用。