内核映像部分也称为标识映射段,而内核模块部分经常称为页表映射段。对高端地址空间(0xFFFF ..)的访问机制与平台相关。在 32位系统上,每个进程的虚址空间为 4GB;而在 64 位系统上,每个进程在理论上的虚址空间大小2^64通常并未全部利用。 某些系统(实质上是真正的处理器)只允许每个进程的虚址空间大小为 2^44。
一、内存与地址空间
由于物理内存子系统的延迟低于磁盘子系统, 虚存子系统所面临的挑战之一是将访问最频繁的内存部分保持在速度更快的主存中。当物理内存短缺时, 虚存子系统需要释放出部分物理内存。这通过将较少使用的内存页面输出到备份存储器上来完成。因此,进程无需管理物理内存分配的细节,从这个意义上说, 虚存子系统提供了资源虚拟化功能。进程也无需对信息和故障的隔离加以管理,因为每个进程都在自己的地址空间中执行。大多数情况下,通过阻止进程访问其合法地址空间之外的内存, 内存管理部件中的硬件机制可以执行内存保护功能。其例外情形是在多个进程之间显式共享的内存区域。
由称为虚拟页面的同等大小的内存容量构成。在IA-32环境中,页面大小为4KB;在IA-64体系结构中, 页面大小可配置为 4KB、 8KB、 16KB或 64KB。 任何 Linux进程的虚址空间又进一步划分为两个主要区域: 用户空间和内核空间。 用户空间驻留在地址空间的较低部分,从地址零开始,其上限为在 processor.h中规定的与平台有关TASK_SIZE取值。 其余地址空间则保留给内核。 地址空间的用户部分被标记为私有的, 这表明它由进程自己的页表加以映射;另一方面, 内核空间则由所有进程共享。根据硬件基础架构的不同, 内核地址空间或者映射到每个进程地址空间的高端部分, 或者占用 CPU虚址空间的顶端部分。在用户级上执行操作时,只能访问到用户地址空间,因为对内核虚址进行操作会导致保护违规错误(protection violation fault);而在内核模式中执行操作时, 用户空间和内核空间都是可访问的。
下图所示,进程试图读取地址 z处的一个字。实际的读操作如序号 1所示。由于页表假定为空的,该读操作会导致一个页面故障。为了响应这个页面故障, Linux内核会搜索该特定进程的 VM区域(area)列表, 以便定位包含该故障地址的 VM区域。在确定了针对该特定请求必须访问的页面之后, Linux发起一个磁盘文件读操作,如序号 2所示。当I/O子系统提供该文件后, 操作系统将数据复制到一个可用的页帧中,如序号3所示。完成这个读页面故障处理所需的最后步骤是对页表进行更新以便将虚址映射至包含数据的物理页帧。之后系统可以重新初始化这个读请求。此时该请求将成功完成,因为所需的数据已经可用。
诸如 kswapd或 pdflush线程等只访问内核地址空间的任务使用了一个匿名的地址空间,因此这些情况下的 mm指针引用值为 NULL。因为 mm结构包含了两个用于建立虚存环境的主要数据结构指针,所以被看作是虚存子系统核心的入口点。第一个结构是页表,第二个结构称为虚存区域。从内核的角度而言, 系统范围内的页表足以实现虚存机制。 一些更传统的大型页表, 包括分簇页表机制, 在表示大型地址空间时的效率并不高。
面所需的所有信息。如图 上图所示,通过 VM区域列表, Linux内核为映射到该特定进程地址空间中的任何具体地址创建实际的页表项。该场景的后果是每个进程的页表都可看成一个 cache子系统。换句话说,如果存在着转换项, 内核即可使用;如果该转换项不存在时,则内核可以基于相应的 VM区域来创建。将页表看作 cache可提供极大灵活性,因为干净页面的转换项可以随意地删除;而脏页面的转换项只有当该页面经由文件备份后才能删除。在删除之前,需要将页面内容写回到文件中,从而清空这些页面。Linux中页表这种类似cache的使用行为提供了一种非常高效的写时复制(copy-on-write)机制的实现基础。
VM区域机制的使用示例是如果一个进程将大量的不同文件映射到其地址空间中,则该进程(更具体地, Linux内核)可能需要维护一个长达数百项的 VM区域列表。这导致系统运行速度随着 VM 区域列表的增长而减慢,因为在每次发生页面错误时都需要遍历该表。为了减少遍历该列表的性能影响, Linux操作系统会记录该列表上的 VM区域数目。在该列表的大小达到特定门限值(通常为 32项)时, 系统创建另一个数据结构, 将VM区域组织成自平衡的二叉搜索树。基于二叉树搜索算法,可以通过一系列步骤来定位与虚址相匹配的 VM区域结构。 这些步骤与地址空间中的 VM区域数目存在着对数关系。为了加快系统访问所有 VM区域结构的速度, Linux内核同时维护(在到达门限之后)线性列表和二叉树结构。
内核模块空间由内核私有页表映射,主要用于实现内核的 vmalloc()区域。 这允许系统分配连续的大量虚存范围。例如,可以在这个地址空间中分配用于加载特定内核模块所需的内存。与 vmalloc()相关的地址范围由两个与平台相关的参数 VMALLOC_START和 VMALLOC_ END控制。 vmalloc()区域并不一定占据整个页表映射段, 因此有可能将
该内存段的部分空间用于平台相关的目的和功能。
内核映像空间在以下意义上是唯一的:在该内存段中的某个虚址及其所转换成的物理地址之间存在着直接关联或映射。这种映射与平台相关,但一对一的对应关系为该内存段赋予了名称。这个内存段可以通过页表机制实现,但也可以使用与平台相关的更高效的技术。换句话说, 系统可以使用一个类似于(pfn = (addr - PAGE_OFFSET) /PAGE_SIZE)的简单映射公式。该公式能够最小化完全基于页表机制的系统实现的开销。尽管存在着这种简单方法,但某些 Linux系统使用了一个称为页帧位图(page frame map)的表来记录系统中物理页帧的状态。该表对于每个页帧都提供一个页帧描述符(pageframe descriptor, pfd), 其中包含了各种与资源有关的系统维护数据。 这些信息包括正在使用该页帧的地址空间计数或数量,以及各种指示页帧是否能换出到磁盘上或者页面是
否标记为脏状态的标志。
在 Linux中, 物理地址空间的实际大小和虚址空间的大小之间不存在着直接关联,但其容量都是有限的。为了更好地管理地址空间, Linux提供了对高端内存的支持。
二、高端内存支持
当代计算机系统上, 虚址空间的大小通常超出物理地址空间的大小。但物理地址空间容量的增长大致符合莫尔定律(Moore's Law), 该定律指出每隔 18个月, 芯片容量将翻倍增长。另外, 虚址空间的大小与平台相关,因此无法轻易改变。当物理内存空间接近虚址空间大小时,这个场景对 Linux系统提出了一个特有挑战:实体映射段的大小可
能不足以映射整个物理地址空间。
因此支持高端内存功能是 Linux内核的可选组件。 例如, 该功能在 Linux IA-64系统上是禁用的。
为了更高效地使用内存, 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操作系统都使用分页机制而不使用交换机制。
四、Linux 页表
Linux系统在物理内存中为每个进程维护一个页表,并通过实体映射内核段来访问实际页表。 Linux中的页表无法被换出到交换空间中,这意味着一个分配了大量地址空间的进程有可能会导致内存子系统饱和,因为页表本身就将耗尽所有可用的内存。类似地,由于系统里包含数百个同时活跃着的进程,所有页表的组合大小也有可能消耗全部可用的内存。当今计算机系统上提供的大型内存子系统使得这种情形很少见,但仍然反映了一个需要解决的容量规划问题。将页表保持在物理内存中可以简化内核设计,并且无需处理嵌套的页故障。进程的页表布局基于三层树结构。第一层由全局目录(global directory,pgd)组成,第二层由中间目录(middle directory,pmd)组成,第三层由页表项(page
tableentry, pte)组成。 通常, 每个目录结点占用一个页帧并包含固定的项数。 pgd和 pmd 目录中的各项或者不存在或者指向下一层中的某个目录。 Pte项表示该树的叶子结点,包含实际的页表项。由于 Linux中的页表布局类似于一棵多层的树,其空间需求与使用中的实际虚址空间成比例。 因此, 该空间需求不是虚址空间的最大容量。 另外, 由于 Linux将内存作为一组页帧来管理,基于固定结点大小的方法并不需要基于线性页表的系统实现所需的物理连续的大型内存区域 。