第三章 存储管理
存储管理子系统时操作系统中最重要的组成部分之一。在早期计算时代,由于人们
所需要的内存数目远远大于物理内存,人们设计出了各种各样的策略来解决此问题,
其中最成功的是虚拟内存技术。它使得系统中为有限物理内存竞争的进程所需内存
空间得到满足。
虚拟内存技术不仅仅可让我们可以使用更多的内存,它还提供了以下功能:
巨大的寻址空间
操作系统让系统看上去有比实际内存大得多的内存空间。虚拟内存可以是系统中实
际物理空间的许多倍。每个进程运行在其独立的虚拟地址空间中。这些虚拟空间相
互之间都完全隔离开来,所以进程间不会互相影响。同时,硬件虚拟内存机构可以
将内存的某些区域设置成不可写。这样可以保护代码与数据不会受恶意程序的干扰。
内存映射
内存映射技术可以将映象文件和数据文件直接映射到进程的地址空间。在内存映射
中,文件的内容被直接连接到进程虚拟地址空间上。
公平的物理内存分配
内存管理子系统允许系统中每个运行的进程公平地共享系统中的物理内存。
共享虚拟内存
尽管虚拟内存允许进程有其独立的虚拟地址空间,但有时也需要在进程之间共享内
存。例如有可能系统中有几个进程同时运行BASH命令外壳程序。为了避免在每个进
程的虚拟内存空间内都存在BASH程序的拷贝,较好的解决办法是系统物理内存中只
存在一份BASH的拷贝并在多个进程间共享。动态库则是另外一种进程间共享执行代
码的方式。共享内存可用来作为进程间通讯(IPC)的手段,多个进程通过共享内存
来交换信息。Linux支持SYSTEM V的共享内存IPC机制。
3.1 虚拟内存的抽象模型
在讨论Linux是如何具体实现对虚拟内存的支持前,有必要看一下更简单的抽象模型。
在处理器执行程序时需要将其从内存中读出再进行指令解码。在指令解码之前它必须
向内存中某个位置取出或者存入某个值。然后执行此指令并指向程序中下一条指令。
在此过程中处理器必须频繁访问内存,要么取指取数,要么存储数据。
虚拟内存系统中的所有地址都是虚拟地址而不是物理地址。通过操作系统所维护的一
系列表格由处理器实现由虚拟地址到物理地址的转换。
为了使转换更加简单,虚拟内存与物理内存都以页面来组织。不同系统中页面的大小
可以相同,也可以不同,这样将带来管理的不便。Alpha AXP处理器上运行的Linux
页面大小为8KB,而IntelX86系统上使用4KB页面。每个页面通过一个叫页面框号的
数字来标示(PFN)。
页面模式下的虚拟地址由两部分构成:页面框号和页面内偏移值。如果页面大小为4KB,
则虚拟地址的11:0位表示虚拟地址偏移值,12位以上表示虚拟页面框号。处理器处理
虚拟地址时必须完成地址分离工作。在页表的帮助下,它将虚拟页面框号转换成物理页
面框号,然后访问物理页面中相应偏移处。
X和Y的虚拟地址空间,它们拥有各自的页表。这些页表将各个进程的虚拟页面映射到
内存中的物理页面。在图中,进程X的虚拟页面框号0被映射到了物理页面框号4。理论
上每个页表入口应包含以下内容:
有效标记,表示此页表入口是有效的
页表入口描叙的物理页面框号
访问控制信息。用来描叙此页可以进行哪些操作,是否可写?是否包含执行代码?
虚拟页面框号是为页表中的偏移。虚拟页面框号5对应表中的第6个单元(0是第一个)。
为了将虚拟地址转换为物理地址,处理器首先必须得到虚拟地址页面框号及页内偏移。
一般将页面大小设为2的次幂。将图3.1中的页面大小设为0x2000字节(十进制为8192)
并且在进程Y的虚拟地址空间中某个地址为0x2194,则处理器将其转换为虚拟页面框号1
及页内偏移0x194。
处理器使用虚拟页面框号为索引来访问处理器页表,检索页表入口。如果在此位置的页
表入口有效,则处理器将从此入口中得到物理页面框号。如果此入口无效,则意味着处
理器存取的是虚拟内存中一个不存在的区域。在这种情况下,处理器是不能进行地址转
换的,它必须将控制传递给操作系统来完成这个工作。
某个进程试图访问处理器无法进行有效地址转换的虚拟地址时,处理器如何将控制传递
到操作系统依赖于具体的处理器。通常的做法是:处理器引发一个页面失效错而陷入操
作系统核心,这样操作系统将得到有关无效虚拟地址的信息以及发生页面错误的原因。
再以图3.1为例,进程Y的虚拟页面框号1被映射到系统物理页面框号4,则再物理内存中
的起始位置为0x8000(4 * 0x2000)。加上0x194字节偏移则得到最终的物理地址0x8194。
通过将虚拟地址映射到物理地址,虚拟内存可以以任何顺序映射到系统物理页面。例如
,
进程X的虚拟页面框号0被映射到物理页面框号1而虚拟页面框号7被映射到物理页面框号
0,
虽然后者的虚拟页面框号要高于前者。这样虚拟内存技术带来了有趣的结果:虚拟内存
中的页面无须在物理内存保持特定顺序。
3.1.1 请求换页
在物理内存比虚拟内存小得多的系统中,操作系统必须提高物理内存的使用效率。节省
物理内存的一种方法是仅加载那些正在被执行程序使用的虚拟页面。比如说,某个数据
库程序可能要对某个数据库进行查询操作,此时并不是数据库的所有内容都要加载到内
存中去,而只加载那些要用的部分。如果此数据库查询是一个搜索查询而无须对数据库
进行添加记录操作,则加载添加记录的代码是毫无意义的。这种仅将要访问的虚拟页面
载入的技术叫请求换页。
当进程试图访问当前不在内存中的虚拟地址时,处理器在页表中无法找到所引用地址的
入口。对于虚拟页面框号2,进程X的页表中没有入口,这样当进程X试图访问虚拟页面框
号2内容时,处理器不能将此地址转换成物理地址。这时处理器通知操作系统有页面错误
发生。
如果发生页面错的虚拟地址是无效的,则表明进程在试图访问一个不存在的虚拟地址。
这可能是应用程序出错而引起的,例如它试图对内存进行一个随机的写操作。此时操作
系统将终止此应用的运行以保护系统中其他进程不受此出错进程的影响。
如果出错虚拟地址是有效的,但是它指向的页面当前不在内存中,则操作系统必须将此
页面从磁盘映象中读入到内存中来。由于访盘时间较长,进程必须等待一段时间直到页
面被取出来。如果系统中还存在其他进程,操作系统就会在读取页面过程中的等待过程
中选择其中之一来运行。读取回来的页面将被放在一个空闲的物理页面框中,同时此进
程的页表中将添加对应此虚拟页面框号的入口。最后进程将从发生页面错误的地方重新
开始运行。此时整个虚拟内存访问过程告一段落,处理器又可以继续进行虚拟地址到物
理地址转换,而进程也得以继续运行。
Linux使用请求换页将可执行映象加载到进程的虚拟内存中。当命令执行时,可执行的命
令文件被打开,同时其内容被映射到进程的虚拟内存。这些操作是通过修改描叙进程内
存映象的数据结构来完成的,此过程称为内存映射。然而只有映象的起始部分被调入物
理内存,其余部分仍然留在磁盘上。当映象执行时,它会产生页面错误,这样Linux将决
定将磁盘上哪些部分调入内存继续执行。
3.1.2 交换
如果进程需要把一个虚拟页面调入物理内存而正好系统中没有空闲的物理页面,操作系
统必须丢弃位于物理内存中的某些页面来为之腾出空间。
如果那些从物理内存中丢弃出来的页面来自于磁盘上的可执行文件或者数据文件,并且
没有修改过则不需要保存那些页面。当进程再次需要此页面时,直接从可执行文件或者
数据文件中读出。
但是如果页面被修改过,则操作系统必须保留页面的内容以备再次访问。这种页面被称
为dirty页面,当从内存中移出来时,它们必须保存在叫做交换文件的特殊文件中。相对
于处理器和物理内存的速度,访问交换文件的速度是非常缓慢的,操作系统必须在将这
些dirty页面写入磁盘和将其继续保留在内存中做出选择。
选择丢弃页面的算法经常需要判断哪些页面要丢弃或者交换,如果交换算法效率很低,
则会发生"颠簸"现象。在这种情况下,页面不断的被写入磁盘又从磁盘中读回来,这样
一来操作系统就无法进行其他任何工作。以图3.1为例,如果物理页面框号1被频繁使用
,
则页面丢弃算法将其作为交换到硬盘的侯选者是不恰当的。一个进程当前经常使用的页
面集合叫做工作集。高效的交换策略能够确保所有进程的工作集保存在物理内存中。
Linux使用最近最少使用(LRU)页面衰老算法来公平地选择将要从系统中抛弃的页面。
这种策略为系统中的每个页面设置一个年龄,它随页面访问次数而变化。页面被访问的
次数越多则页面年龄越年轻;相反则越衰老。年龄较老的页面是待交换页面的最佳侯选
者。
3.1.3 共享虚拟内存
虚拟内存让多个进程之间可以方便地共享内存。所有的内存访问都是通过每个进程自身
的页表进行。对于两个共享同一物理页面的进程,在各自的页表中必须包含有指向这一
物理页面框号的页表入口。
两个进程共享物理页面框号4。对进程X来说其对应的虚拟页面框号为4而进程Y的为6。
这个有趣的现象说明:共享物理页面的进程对应此页面的虚拟内存位置可以不同。
3.1.4 物理与虚拟寻址模式
操作系统自身也运行在虚拟内存中的意义不大。如果操作系统被迫维护自身的页表那将
是一个令人恶心的方案。多数通用处理器同时支持物理寻址和虚拟寻址模式。物理寻址
模式无需页表的参与且处理器不会进行任何地址转换。Linux核心直接运行在物理地址空
间上。
Alpha AXP处理器没有特殊的物理寻址模式。它将内存空间划分为几个区域并将其中两个
指定为物理映射地址。核心地址空间被称为KSEG地址空间,它位于地址0xfffffc000000
0000
以上区域。为了执行位于KSEG的核心代码或访问那里的数据,代码必须在核心模式下执
行。
Alpha上的Linux核心从地址0xfffffc0000310000开始执行.
3.1.5 访问控制
页表入口包含了访问控制信息。由于处理器已经将页表入口作为虚拟地址到物理地址的
映
射,那么可以很方便地使用访问控制信息来判断处理器是否在以其应有的方式来访问内
存。
诸多因素使得有必要严格控制对内存区域的访问。有些内存,如包含执行代码的部分,
显
然应该是只读的,操作系统决不能允许进程对此区域的写操作。相反包含数据的页面应
该
是可写的,但是去执行这段数据肯定将导致错误发生。多数处理器至少有两种执行方式
:
核心态与用户态。任何人都不会允许在用户态下执行核心代码或者在用户态下修改核心
数
据结构。
页表入口中的访问控制信息是处理器相关的;Alpha AXP处理器的PTE(Page Table Entr
y)。
这些位域的含义如下:
V 有效,如果此位置位,表明此PTE有效
FOE “执行时失效”,无论合时只要执行包含在此页面中的指令,处理器都将报告页面
错误并将控制传递
FOW “写时失效”, 除了页面错误发生在对此页面的写时,其他与上相同。
FOR “读时失效”,除了页面错误发生在对此页面的读时,其他与上相同。
ASM 地址空间匹配。被操作系统用于清洗转换缓冲中的某些入口。
KRE 运行在核心模式下的代码可以读此页面。
URE 运行在用户模式下的代码可以读此页面。
GH 将整个块映射到单个而不是多个转换缓冲时的隐含粒度。
KWE 运行在核心模式下的代码可以写此页面。
UWE 运行在用户模式下的代码可以写此页面。
page frame number
对于V位置位的PTE,此域包含了对应此PTE的物理页面框号;对于无效PTE,此域不
为0,它包含了页面在交换文件中位置的信息。
以下两位由Linux定义并使用。
_PAGE_DIRTY
如果置位,此页面要被写入交换文件。
_PAGE_ACCESSED
Linux用它表示页面已经被访问过。
3.2 高速缓冲
如果用上述理论模型来实现一个系统,它可能可以工作,但效率不会高。操作系统设计
者和处理器设计者都在努力以提高系统的性能。除了制造更快的CPU和内存外,最好的办
法是在高速缓冲中维护有用信息和数据以加快某些操作。Linux使用了许多与高速缓冲相
关的内存管理策略。
Buffer Cache
这个buffer cache中包含了被块设备驱动使用的数据缓冲。
这些缓冲的单元的大小一般固定(例如说512字节)并且包含从块设备读出或者写入
的信息块。块设备是仅能够以固定大小块进行读写操作的设备。所有的硬盘都是块
设备。
利用设备标志符和所需块号作索引可以在buffer
cache中迅速地找到数据。块设备只能够通过buffer
cache来存取。如果数据在buffer
cache中可以找到则无需从物理块设备(如硬盘)中读取,这样可以加速访问。
Page Cache
用来加速硬盘上可执行映象文件与数据文件的存取。
它每次缓冲一个页面的文件内容。页面从磁盘上读入内存后缓存在page
cache中。
Swap Cache
只有修改过的页面存储在交换文件中。
只要这些页面在写入到交换文件后没有被修改,则下次此页面被交换出内存时,
就不必再进行更新写操作,这些页面都可以简单的丢弃。在交换频繁发生的系统
中,Swap Cache可以省下很多不必要且耗时的磁盘操作。
Hardware Caches
一个常见的hardware
cache是处理器中的页表入口cache。处理器不总是直接读取页表而是在需要时
缓存页面的转换。这种cache又叫做转换旁视缓冲(Translation Look-aside
Buffers),它包含系统中一个或多个处理器的页表入口的缓冲拷贝。
当发出对虚拟地址的引用时,处理器试图找到相匹配的TLB入口。如果找到则直接
将虚拟地址转换成物理地址并对数据进行处理。如果没有找到则向操作系统寻求帮
助。处理器将向操作系统发出TLB失配信号,它使用一个特定的系统机制来将此异
常
通知操作系统。操作系统则为此地址匹配对产生新的TLB入口。当操作系统清除此
异常时,处理器将再次进行虚拟地址转换。由于此时在TLB中已经有相应的入口,
这次操作将成功。
使用高速缓存的缺点在于Linux必须消耗更多的时间和空间来维护这些缓存,并且
当缓存系统崩溃时系统也将崩溃。
3.3 Linux 页表
Linux总是假定处理器有三级页表。每个页表通过所包含的下级页表的页面框号来访问。
每个域提供了某个指定页表的偏移。为了将虚拟地址转换成物理地址,处理器必须得到
每
个域的值。这个过程将持续三次直到对应于虚拟地址的物理页面框号被找到。最后再使
用
虚拟地址中的最后一个域,得到了页面中数据的地址。
为了实现跨平台运行,Linux提供了一系列转换宏使得核心可以访问特定进程的页表。
这样核心无需知道页表入口的结构以及它们的排列方式。
这种策略相当成功,无论在具有三级页表结构的Alpha AXP还是两级页表的Intel X86处
理器中,Linux总是使 用相同的页表操纵代码。
3.4 页面分配与回收
对系统中物理页面的请求十分频繁。例如当一个可执行映象被调入内存时,操作系统必
须为其分配页面。当映象执行完毕和卸载时这些页面必须被释放。物理页面的另一个用
途是存储页表这些核心数据结构。虚拟内存子系统中负责页面分配与回收的数据结构和
机制可能用处最大。
系统中所有的物理页面用包含mem_map_t结构的链表mem_map来描叙,这些结构在系统启
动时初始化。每个mem_map_t描叙了一个物理页面。其中与内存管理相关的重要域如下:
count
记录使用此页面的用户个数。当这个页面在多个进程之间共享时,它的值大于1。
age 此域描叙页面的年龄,用于选择将适当的页面抛弃或者置换出内存时。
map_nr
记录本mem_map_t描叙的物理页面框号。
页面分配代码使用free_area数组来寻找和释放页面,此机制负责整个缓冲管理。另外此
代码与处理器使用的页面大小和物理分页机制无关。
free_area中的每个元素都包含页面块的信息。数组中第一个元素描叙1个页面,第二个
表示2个页面大小的块而接下来表示4个页面大小的块,总之都是2的次幂倍大小。list域
表示一个队列头,它包含指向mem_map数组中page数据结构的指针。所有的空闲页面都在
此队列中。map域是指向某个特定页面尺寸的页面组分配情况位图的指针。当页面的第N
块
空闲时,位图的第N位被置位。
free_area结构。第一个元素有个自由页面(页面框号0),第二个元素有4个页面大小的
2个自由块,前一个从页面框号4开始而后一个从页面框号56开始。
3.4.1 页面分配
Linux使用Buddy算法来有效的分配与回收页面块。页面分配代码每次分配包含一个或
者多个物理页面的内存块。页面以2的次幂的内存块来分配。这意味着它可以分配1个、
2个和4个页面的块。只要系统中有足够的空闲页面来满足这个要求(nr_free_pages>
min_free_page),内存分配代码将在free_area中寻找一个与请求大小相同的空闲块。
free_area中的每个元素保存着一个反映这样大小的已分配与空闲页面的位图。例如,
free_area数组中第二个元素指向一个反映大小为四个页面的内存块分配情况的内存映
象。
分配算法首先搜寻满足请求大小的页面。它从free_area数据结构的list域着手沿链来
搜索空闲页面。如果没有这样请求大小的空闲页面,则它搜索两倍于请求大小的内存块
。
这个过程一直将持续到free_area被搜索完或找到满足要求的内存块为止。如果找到的
页面块大于请求的块则对其进行分割以使其大小与请求块匹配。由于块大小都是2的次
幂所以分割过程十分简单。空闲块被连进相应的队列而这个页面块被分配给调用者。
当系统中有大小为两个页面块的请求发出时,第一个4页面大小的内存块(从页面框号
4开始)将分成两个2页面大小的块。前一个,从页面框号4开始的,将分配出去返回给
请求者,而后一个,从页面框号6开始,将被添加到free_area数组中表示两个页面大小
的空闲块的元素1中。
3.4.2 页面回收
将大的页面块打碎进行分配将增加系统中零碎空闲页面块的数目。页面回收代码在适当
时机下要将这些页面结合起来形成单一大页面块。事实上页面块大小决定了页面重新组
合的难易程度。
当页面块被释放时,代码将检查是否有相同大小的相邻或者buddy内存块存在。如果有,
则将它们结合起来形成一个大小为原来两倍的新空闲块。每次结合完之后,代码还要检
查是否可以继续合并成更大的页面。最佳情况是系统的空闲页面块将和允许分配的最大
内存一样大。
如果释放页面框号1,它将和空闲页面框号0结合作为大小为2个页面的空闲块排入
free_area的第一个元素中。
3.5 内存映射
映象执行时,可执行映象的内容将被调入进程虚拟地址空间中。可执行映象使用的共享
库同样如此。然而可执行文件实际上并没有调入物理内存,而是仅仅连接到进程的虚拟
内存。当程序的其他部分运行时引用到这部分时才把它们从磁盘上调入内存。将映象连
接到进程虚拟地址空间的过程称为内存映射。
每个进程的虚拟内存用一个mm_struct来表示。它包含当前执行的映象(如BASH)以及指
向vm_area_struct的大量指针。每个vm_area_struct数据结构描叙了虚拟内存的起始与
结束位置,进程对此内存区域的存取权限以及一组内存操作函数。这些函数都是Linux在
操纵虚拟内存区域时必须用到的子程序。其中一个负责处理进程试图访问不在当前物理
内存中的虚拟内存(通过页面失效)的情况。此函数叫nopage。它用在Linux试图将可执行
映象的页面调入内存时。
可执行映象映射到进程虚拟地址时将产生一组相应的vm_area_struct数据结构。每个
vm_area_struct数据结构表示可执行映象的一部分:可执行代码、初始化数据(变量)、
未初始化数据等等。Linux支持许多标准的虚拟内存操作函数,创建vm_area_struct数据
结构时有一组相应的虚拟内存操作函数与之对应。
3.6 请求换页
当可执行映象到进程虚拟地址空间的映射完成后,它就可以开始运行了。由于只
有很少部分的映象调入内存,所以很快就会发生对不在物理内存中的虚拟内存区
域的访问。当进程访问无有效页表入口的虚拟地址时,处理器将向Linux报告一
个页面错误。
页面错误带有