内核映像部分也称为标识映射段,而内核模块部分经常称为页表映射段。对高端地址空间(0xFFFF ..)的访问机制与平台相关。在 32位系统上,每个进程的虚址空间为 4GB;而在 64 位系统上,每个进程在理论上的虚址空间大小2^64通常并未全部利用。 某些系统(实质上是真正的处理器)只允许每个进程的虚址空间大小为 2^44。
一、内存与地址空间
由于物理内存子系统的延迟低于磁盘子系统, 虚存子系统所面临的挑战之一是将访问最频繁的内存部分保持在速度更快的主存中。当物理内存短缺时, 虚存子系统需要释放出部分物理内存。这通过将较少使用的内存页面输出到备份存储器上来完成。因此,进程无需管理物理内存分配的细节,从这个意义上说, 虚存子系统提供了资源虚拟化功能。进程也无需对信息和故障的隔离加以管理,因为每个进程都在自己的地址空间中执行。大多数情况下,通过阻止进程访问其合法地址空间之外的内存, 内存管理部件中的硬件机制可以执行内存保护功能。其例外情形是在多个进程之间显式共享的内存区域。
1、地址空间 进程虚址空间的定义是作为运行环境呈现给进程的内存地址范围。在进程生命周期的任何时间点上,都会有一些进程地址被映射到物理地址,而另一些进程地址则不被映射。 当对fork()系统调用进行初始化时, 内核创建进程虚址空间的基本框架。进程内部的虚址空间布局由动态链接器建立,可以随着硬件平台的不同而变化。一般地, 虚址空间
由称为虚拟页面的同等大小的内存容量构成。在IA-32环境中,页面大小为4KB;在IA-64体系结构中, 页面大小可配置为 4KB、 8KB、 16KB或 64KB。 任何 Linux进程的虚址空间又进一步划分为两个主要区域: 用户空间和内核空间。 用户空间驻留在地址空间的较低部分,从地址零开始,其上限为在 processor.h中规定的与平台有关TASK_SIZE取值。 其余地址空间则保留给内核。 地址空间的用户部分被标记为私有的, 这表明它由进程自己的页表加以映射;另一方面, 内核空间则由所有进程共享。根据硬件基础架构的不同, 内核地址空间或者映射到每个进程地址空间的高端部分, 或者占用 CPU虚址空间的顶端部分。在用户级上执行操作时,只能访问到用户地址空间,因为对内核虚址进行操作会导致保护违规错误(protection violation fault);而在内核模式中执行操作时, 用户空间和内核空间都是可访问的。
2、用户地址空间 在 Linux内核中,每个地址空间都通过一个称为 mm结构的对象来表示。由于多个任务可能共享同一个地址空间, mm结构是一个只要引用数目大于零就存在的引用计数(referencecounted)对象。每个任务的数据结构都包含 mm指针, 指向定义了该任务(进程)的地址空间的 mm结构。
下图所示,进程试图读取地址 z处的一个字。实际的读操作如序号 1所示。由于页表假定为空的,该读操作会导致一个页面故障。为了响应这个页面故障, Linux内核会搜索该特定进程的 VM区域(area)列表, 以便定位包含该故障地址的 VM区域。在确定了针对该特定请求必须访问的页面之后, Linux发起一个磁盘文件读操作,如序号 2所示。当I/O子系统提供该文件后, 操作系统将数据复制到一个可用的页帧中,如序号3所示。完成这个读页面故障处理所需的最后步骤是对页表进行更新以便将虚址映射至包含数据的物理页帧。之后系统可以重新初始化这个读请求。此时该请求将成功完成,因为所需的数据已经可用。
诸如 kswapd或 pdflush线程等只访问内核地址空间的任务使用了一个匿名的地址空间,因此这些情况下的 mm指针引用值为 NULL。因为 mm结构包含了两个用于建立虚存环境的主要数据结构指针,所以被看作是虚存子系统核心的入口点。第一个结构是页表,第二个结构称为虚存区域。从内核的角度而言, 系统范围内的页表足以实现虚存机制。 一些更传统的大型页表, 包括分簇页表机制, 在表示大型地址空间时的效率并不高。
3、VM区域 为了避免大型页表带来的问题, Linux并不使用页表本身来表示地址空间,而是使用了 VM区域结构的一组列表。 该方法的思想是将一块地址空间划分成可按照相同方式处理的多个连续页面范围, 其中每个范围都可以通过单个 VM区域结构来表示。 如果进程访问一个在页表中没有转换项的页面, 负责该特定页面的 VM区域拥有建立和安装该页
面所需的所有信息。如图 上图所示,通过 VM区域列表, Linux内核为映射到该特定进程地址空间中的任何具体地址创建实际的页表项。该场景的后果是每个进程的页表都可看成一个 cache子系统。换句话说,如果存在着转换项, 内核即可使用;如果该转换项不存在时,则内核可以基于相应的 VM区域来创建。将页表看作 cache可提供极大灵活性,因为干净页面的转换项可以随意地删除;而脏页面的转换项只有当该页面经由文件备份后才能删除。在删除之前,需要将页面内容写回到文件中,从而清空这些页面。Linux中页表这种类似cache的使用行为提供了一种非常高效的写时复制(copy-on-write)机制的实现基础。
VM区域机制的使用示例是如果一个进程将大量的不同文件映射到其地址空间中,则该进程(更具体地, Linux内核)可能需要维护一个长达数百项的 VM区域列表。这导致系统运行速度随着 VM 区域列表的增长而减慢,因为在每次发生页面错误时都需要遍历该表。为了减少遍历该列表的性能影响, Linux操作系统会记录该列表上的 VM区域数目。在该列表的大小达到特定门限值(通常为 32项)时, 系统创建另一个数据结构, 将VM区域组织成自平衡的二叉搜索树。基于二叉树搜索算法,可以通过一系列步骤来定位与虚址相匹配的 VM区域结构。 这些步骤与地址空间中的 VM区域数目存在着对数关系。为了加快系统访问所有 VM区域结构的速度, Linux内核同时维护(在到达门限之后)线性列表和二叉树结构。
4、内核地址空间
整个内核地址空间可以分解成内核映像空间(也称为实体映射段)和内核模块空间(也称为页表映射段)。
4.1 内核模块空间
内核模块空间由内核私有页表映射,主要用于实现内核的 vmalloc()区域。 这允许系统分配连续的大量虚存范围。例如,可以在这个地址空间中分配用于加载特定内核模块所需的内存。与 vmalloc()相关的地址范围由两个与平台相关的参数 VMALLOC_START和 VMALLOC_ END控制。 vmalloc()区域并不一定占据整个页表映射段, 因此有可能将
该内存段的部分空间用于平台相关的目的和功能。
4.2 内核映像空间
内核映像空间在以下意义上是唯一的:在该内存段中的某个虚址及其所转换成的物理地址之间存在着直接关联或映射。这种映射与平台相关,但一对一的对应关系为该内存段赋予了名称。这个内存段可以通过页表机制实现,但也可以使用与平台相关的更高效的技术。换句话说, 系统可以使用一个类似于(pfn = (addr - PAGE_OFFSET) /PAGE_SIZE)的简单映射公式。该公式能够最小化完全基于页表机制的系统实现的开销。尽管存在着这种简单方法,但某些 Linux系统使用了一个称为页帧位图(page frame map)的表来记录系统中物理页帧的状态。该表对于每个页帧都提供一个页帧描述符(pageframe descriptor, pfd), 其中包含了各种与资源有关的系统维护数据。 这些信息包括正在使用该页帧的地址空间计数或数量,以及各种指示页帧是否能换出到磁盘上或者页面是
否标记为脏状态的标志。
在 Linux中, 物理地址空间的实际大小和虚址空间的大小之间不存在着直接关联,但其容量都是有限的。为了更好地管理地址空间, Linux提供了对高端内存的支持。
二、高端内存支持
当代计算机系统上, 虚址空间的大小通常超出物理地址空间的大小。但物理地址空间容量的增长大致符合莫尔定律(Moore's Law), 该定律指出每隔 18个月, 芯片容量将翻倍增长。另外, 虚址空间的大小与平台相关,因此无法轻易改变。当物理内存空间接近虚址空间大小时,这个场景对 Linux系统提出了一个特有挑战:实体映射段的大小可
能不足以映射整个物理地址空间。
high mem接口 诸如 Solaris等操作系统在特定平台上运行时也面临着类似挑战。 Linux系统通过high mem接口来解决这个问题。在 Linux中,高端内存定义为无法通过实体映射段加以寻址的物理内存。 highmem接口提供了对该内存空间的间接访问方法,将高端内存页面动态映射到一小块专门保留的内核地址空间(称为 kmap()段)。 kmap()接口将页面参数所指定的页帧映射到 kmap段。该参数必须是一个指向被映射页面的页帧描述符的指针。这个例程会返回该页面已映射到的虚址。在 kmap段饱和的情况下,该例程将阻塞,直至出现可用的空间。因此,不能无限期阻塞执行的代码例如中断处理程序等将无法利用高端内存。总而言之, 系统中支持高端内存的话将会产生一些本应尽量避免的额外开销,
因此支持高端内存功能是 Linux内核的可选组件。 例如, 该功能在 Linux IA-64系统上是禁用的。
为了更高效地使用内存, Linux还提供了分页和交换机制。
三、分页与交换机制 Linux中的每个用户进程都在连续的单一虚址空间上运行。该虚址空间由多种不同类型的内存对象组成, 例如程序代码(只读)和程序数据(写时复制)。 当一个程序被加载到某个进程的虚址空间时,该区域由程序可执行代码完成初始的内存映射和备份。因此虚存系统可以非常方便地释放和重用这些文本页面。例如,当修改一个数据页面时, 虚存
系统需要创建该页面的一个私有副本并将其分配给发起该更新操作的进程。私有数据页面最初称为写时复制(copy-on-write)页面或按需填零(zero-fill-on-demand)页面。当出现页换出情况时,需要区别对待这些页面。大多数应用程序都会分配比其在任何特定时刻所用的内存更多的虚存。 例如,程序的文本段经常包含大量很少执行或从不执行的错误处
理代码。为了避免将内存浪费在从未访问的虚拟页面上, Linux(以及大多数其他 UNIX操作系统)使用了按需页面调度(demand paging)的方法。 在这种方法中, 虚址空间在最初时为空, 即所有虚拟页面在页表中都标记为不存在(not present)的状态。 当访问一个并不存在的虚拟页面时, CPU会生成页故障(page fault)。这个错误由 Linux内核截获,从而激活页故障处理程序。其结果是内核分配一个新的页帧,确定被访问页面的内容,加载该页面,最后更新页表将该页面标记为存在状态。然后执行流程返回到导致该页面错误的进程。由于所需的页面此时已存在因而可以使用,因此指令继续执行而不会导致页故障。
当来自不同应用程序的多个线程竞争稀缺资源时,诸如内存之类的物理资源会面临短缺问题。在这种情况下,Linux系统需要选择一个备份了某个最近未被访问的虚拟页面的页帧,并且需要将该页面写至磁盘上一个称为交换空间的特殊区域。之后系统就可以重用该页帧来备份所请求的新虚拟页面。旧页面的准确写入位置与基础架构中所用的交换空间类型相关。 Linux支持多个交换空间区域,其中每个区域可以由整个磁盘分区或者现有文件系统中某个特殊格式的文件组成。因此,需要相应地更新与旧页面相关联的页表。 Linux系统通过将该页表项标记为不存在状态来维护这个更新过程。为了记录旧页面的存储位置, Linux需要保存该页面的磁盘位置。概言之,一个标记为存在状态的页表项包含了备份该虚拟页面的物理页帧的编号,标记为不存在状态的页表项包含了该页面的磁盘位置。从某个进程借取一个页面并将其写至磁盘子系统的技术称为页面调度(paging)。 与之相关的一种技术称为交换(swapping), 这是一种更强大的页面调度形式,不仅可以借取单个页面,而且还能借取一个进程的全部页面集合。 Linux以及多数其他UNIX操作系统都使用分页机制而不使用交换机制。
1、替换策略 从执行和稳定性的角度而言,当内存短缺时借取某个特定页面的关联性并不重要。另一方面,从性能的角度来看,要借取哪个页面以及何时借取是最重要的。确定从主存子系统收回哪个页面的过程称为替换策略。例如,最近最少使用(least recently used, LRU)方法会分析历史行为并选取未访问时间最长的页面。虽然 LRU可以作为一种页面替换算法来实现,但并不实用。该方法需要在每次访问主存时都更新某个数据结构,从而会产生极大的开销。实际中,大多数 UNIX操作系统都采用了各种较低开销的替换策略的变型, 例如最近未使用(not recently used, NRU)策略。 Linux则采取一种基于 LRU的方法。在 Linux环境中, 内核使用的不可分页内存的数量可变这个事实进一步复杂化了页面替换机制。例如,用于存储文件数据的缓冲区 cache可以动态地增大或缩小。当内核需要分配一个新的页帧时, 系统可以从内核或者某个进程中借取一个页面。换句话说, 内核不仅需要实现一种替换策略,还要实现一种内存平衡策略来确定可用于内核缓冲区的内存量以及用于备份虚拟页面的内存量。
2、页面替换与内存平衡 页面替换和内存平衡策略的组合是一项没有清晰且完美解决方案的艰巨任务。因此,Linux内核使用了多种在实际中可能会工作良好的执行过程。从实现的角度而言, Linux系统中与平台相关的内核部分在每个页表项中添加额外的 2个数据位, 称为访问(access)标志位和脏(dirty)标志位。访问位指示自从最近一次清空该位以来,是否访问过相应页面;脏标志位指示该页面自从最近一次换入之后是否曾被修改过。 Linux的 kswapd线程定期检查这两个数据位。在检查之后, kswapd清空访问位。如果 kswapd检测到内核将要面临内存不足的情形, 则抢先将最近未用的内存页换出。如果一个页面设置了脏标志位后,则在释放该页帧之前,需要将该页面写入磁盘。由于这是一种相对昂贵的操作, kswapd更趋向于释放已清除了访问位和脏位(置为 0)的页面。根据定义, 这些页面最近未被访问过并且在释放页帧之前不需要被写入磁盘,因此可以较低的性能开销回收。 各种系统对物理页面的管理方法不同, Linux体系结构中采用了三级页表的方法。这些页表可以实现虚址与物理地址的相互转换。
四、Linux 页表
Linux系统在物理内存中为每个进程维护一个页表,并通过实体映射内核段来访问实际页表。 Linux中的页表无法被换出到交换空间中,这意味着一个分配了大量地址空间的进程有可能会导致内存子系统饱和,因为页表本身就将耗尽所有可用的内存。类似地,由于系统里包含数百个同时活跃着的进程,所有页表的组合大小也有可能消耗全部可用的内存。当今计算机系统上提供的大型内存子系统使得这种情形很少见,但仍然反映了一个需要解决的容量规划问题。将页表保持在物理内存中可以简化内核设计,并且无需处理嵌套的页故障。进程的页表布局基于三层树结构。第一层由全局目录(global directory,pgd)组成,第二层由中间目录(middle directory,pmd)组成,第三层由页表项(page
tableentry, pte)组成。 通常, 每个目录结点占用一个页帧并包含固定的项数。 pgd和 pmd 目录中的各项或者不存在或者指向下一层中的某个目录。 Pte项表示该树的叶子结点,包含实际的页表项。由于 Linux中的页表布局类似于一棵多层的树,其空间需求与使用中的实际虚址空间成比例。 因此, 该空间需求不是虚址空间的最大容量。 另外, 由于 Linux将内存作为一组页帧来管理,基于固定结点大小的方法并不需要基于线性页表的系统实现所需的物理连续的大型内存区域 。
当实现虚页面至物理页面转换时, 虚址被分解成多个部分。用于页表查询操作的各个虚址部分依赖于 pgd、 pmd和 pte索引(见下图)。存储于 mm结构中的页表指针发起一个查询操作。该页表指针引用作为页表树根目录的全局目录。 pgd索引所标识的项包含了中间目录地址。利用该地址与 pmd索引的组合能够定位 pte目录的地址。对该机制展一个额外层次, 可以确定该虚址实际映射到的页面的 pte。 通过 pte可以计算出物理页帧的地址,在利用虚址的偏移量取值即可标识出正确的内存位置。 Linux中的多层页表实现方法代表了一种与平台无关的解决方案。若某个特定实现不需要整个树结构来支持的话,该方案允许将中间目录转变成全局目录。 IA-32环境中可以采用这种方法,将页面中间目录的大小置为 1。换句话说, IA-32体系结构中将 32位的虚址如下分解: 10位用于页面目录, 10位用于页表项,其余 12位用于偏移量部分。这个地址转换过程是硬件(内存管理单元(memory management unit, MMU))和软件(内核)之间协作完成的。 内核与 MMU通信,为每个用户地址空间标识出映射至物理页面的虚拟页面。 MMU能够将该过程中的任何错误条件通知给内核。最常见的错误条件与页故障有关,这时内核需要从辅助存储器中获取所需页面。其他错误条件可能与任何潜在的页面保护问题有关或由其触发。从物理地址的角度来看, Linux会区分不同的内存存储区(ZONE_DMA、ZONE_NORMAL和 ZONE_ HIGHMEM),每个存储区都具有不同功用。大多数的内存分配操作发生在 ZONE_NORMAL区域中, 而 ZONE_HIGHMEM表示大于 896MB的物理地址空间 。