内存管理子系统是操作系统最重要的组成部分之一。自从计算机发展的早期,人们就一直需要比系统中物理存在的内存更多的内存。为了克服这种限制,人们开发了各种策略,其中最成功的就是虚拟内存。虚拟内存通过在竞争进程之间按需共享内存,使系统看起来拥有比实际更多的内存。
虚拟内存不仅仅是让您计算机的内存使用更有效率。内存管理子系统提供:
共享内存也可以用作进程间通信(IPC)机制,两个或多个进程通过它们共有的内存交换信息。Linux 支持 Unix TM System V 共享内存 IPC。
在考虑 Linux 用于支持虚拟内存的方法之前,考虑一个没有过多细节的抽象模型是有用的。
当处理器执行程序时,它从内存中读取指令并对其进行解码。在解码指令时,它可能需要获取或存储内存位置的内容。然后处理器执行指令并继续执行程序中的下一条指令。通过这种方式,处理器始终访问内存,要么获取指令,要么获取和存储数据。
在虚拟内存系统中,所有这些地址都是虚拟地址,而不是物理地址。这些虚拟地址由处理器根据操作系统维护的一组表中保存的信息转换为物理地址。
为了使这种转换更容易,虚拟内存和物理内存被分成称为页的方便大小的块。这些页面的大小都相同,它们不必如此,但如果它们大小不同,系统将很难管理。Alpha AXP 系统上的 Linux 使用 8 KB 的页面,而 Intel x86 系统上使用 4 KB 的页面。每个页面都被赋予一个唯一的编号;页帧号(PFN)。
在这种分页模型中,虚拟地址由两部分组成:偏移量和虚拟页帧号。如果页面大小为 4 KB,则虚拟地址的位 11:0 包含偏移量,位 12 及以上是虚拟页帧号。每次处理器遇到虚拟地址时,它都必须提取偏移量和虚拟页帧号。处理器必须将虚拟页帧号转换为物理页帧号,然后访问该物理页中正确偏移量的位置。为此,处理器使用页表。
图 3.1 显示了两个进程(进程 X 和进程 Y)的虚拟地址空间,每个进程都有自己的页表。这些页表将每个进程的虚拟页映射到内存中的物理页。这表明进程 X 的虚拟页帧号 0 映射到物理页帧号 1 中的内存,而进程 Y 的虚拟页帧号 1 映射到物理页帧号 4。理论页表中的每个条目都包含以下信息:
页表使用虚拟页帧号作为偏移量进行访问。虚拟页帧 5 将是表的第 6 个元素(0 是第一个元素)。
为了将虚拟地址转换为物理地址,处理器必须首先计算出虚拟地址的页帧号以及该虚拟页内的偏移量。通过使页面大小为 2 的幂,可以通过掩码和移位轻松完成此操作。再次查看图 3.1 并假设页面大小为 0x2000 字节(十进制 8192),进程 Y 的虚拟地址空间中的地址为 0x2194,则处理器会将该地址转换为偏移量 0x194 到虚拟页帧号 1 中。
处理器使用虚拟页帧号作为进程页表的索引,以检索其页表条目。如果该偏移量处的页表条目有效,则处理器从此条目中获取物理页帧号。如果条目无效,则进程访问了其虚拟内存中不存在的区域。在这种情况下,处理器无法解析该地址,必须将控制权传递给操作系统,以便它可以修复问题。
处理器如何通知操作系统正确的进程尝试访问虚拟地址,但该地址没有有效的转换,这取决于处理器。无论处理器如何传递,这都称为页错误,并且操作系统会收到错误虚拟地址以及页错误原因的通知。
假设这是一个有效的页表条目,处理器会获取该物理页帧号并将其乘以页面大小,以获得物理内存中页面基地址。最后,处理器将偏移量添加到它需要的指令或数据中。
再次使用上面的示例,进程 Y 的虚拟页帧号 1 映射到物理页帧号 4,物理页帧号 4 从 0x8000(4 x 0x2000)开始。加上 0x194 字节偏移量,我们得到最终的物理地址 0x8194。
通过这种方式将虚拟地址映射到物理地址,虚拟内存可以以任何顺序映射到系统的物理页中。例如,在图 3.1 中,进程 X 的虚拟页帧号 0 映射到物理页帧号 1,而虚拟页帧号 7 映射到物理页帧号 0,即使它在虚拟内存中高于虚拟页帧号 0。这展示了虚拟内存的一个有趣的副产品;虚拟内存的页面不必以任何特定顺序存在于物理内存中。
由于物理内存比虚拟内存少得多,因此操作系统必须小心,不要低效地使用物理内存。节省物理内存的一种方法是仅加载执行程序当前正在使用的虚拟页面。例如,可以运行数据库程序来查询数据库。在这种情况下,不需要将整个数据库都加载到内存中,只需加载正在检查的数据记录即可。如果数据库查询是搜索查询,则加载数据库程序中处理添加新记录的代码是没有意义的。这种仅在访问时才将虚拟页面加载到内存中的技术称为按需分页。
当进程尝试访问当前不在内存中的虚拟地址时,处理器找不到所引用虚拟页面的页表条目。例如,在图 3.1 中,进程 X 的页表中没有虚拟页帧号 2 的条目,因此,如果进程 X 尝试从虚拟页帧号 2 中的地址读取,则处理器无法将该地址转换为物理地址。此时,处理器通知操作系统发生了页错误。
如果错误的虚拟地址无效,则意味着进程尝试访问它不应该访问的虚拟地址。可能是应用程序在某些方面出错了,例如写入内存中的随机地址。在这种情况下,操作系统将终止它,从而保护系统中的其他进程免受此恶意进程的侵害。
如果错误的虚拟地址有效,但它引用的页面当前不在内存中,则操作系统必须从磁盘上的映像中将相应的页面带入内存。相对于来说,磁盘访问需要很长时间,因此进程必须等待相当长的时间,直到页面被获取。如果有其他进程可以运行,则操作系统将选择其中一个进程来运行。获取的页面被写入空闲的物理页帧,并且虚拟页帧号的条目被添加到进程的页表中。然后,进程在发生内存错误的机器指令处重新启动。这次进行虚拟内存访问,处理器可以进行虚拟地址到物理地址的转换,因此进程继续运行。
Linux 使用按需分页将可执行映像加载到进程的虚拟内存中。每当执行命令时,包含该命令的文件将被打开,并且其内容被映射到进程的虚拟内存中。这是通过修改描述此进程内存映射的数据结构来完成的,称为内存映射。但是,只有映像的第一部分实际上被带入物理内存。映像的其余部分保留在磁盘上。当映像执行时,它会生成页错误,Linux 使用进程的内存映射来确定将映像的哪些部分带入内存以供执行。
如果进程需要将虚拟页面带入物理内存,并且没有可用的空闲物理页面,则操作系统必须通过从物理内存中丢弃另一个页面来为该页面腾出空间。
如果要从物理内存中丢弃的页面来自映像或数据文件,并且尚未写入,则该页面不需要保存。相反,它可以被丢弃,如果进程再次需要该页面,则可以从映像或数据文件中将其带回内存。
但是,如果页面已被修改,则操作系统必须保留该页面的内容,以便以后可以访问它。这种类型的页面称为脏页,当它从内存中移除时,它会保存在一种称为交换文件的特殊文件中。相对于处理器和物理内存的速度而言,访问交换文件非常慢,操作系统必须权衡将页面写入磁盘的需求与将它们保留在内存中以供再次使用的需求。
如果用于决定丢弃或交换哪些页面的算法(交换算法)效率不高,则会发生称为抖动的情况。在这种情况下,页面不断地被写入磁盘,然后又被读回,操作系统过于繁忙,无法执行太多实际工作。例如,如果经常访问图 3.1 中的物理页帧号 1,则它不是交换到硬盘的好选择。进程当前正在使用的一组页面称为工作集。高效的交换方案应确保所有进程的工作集都在物理内存中。
Linux 使用最近最少使用(LRU)页面老化技术来公平地选择可能从系统中移除的页面。该方案涉及系统中每个页面都有一个年龄,该年龄随着页面的访问而变化。页面被访问得越多,它就越年轻;页面被访问得越少,它就越老旧和陈旧。旧页面是交换的良好候选者。
虚拟内存使多个进程轻松共享内存。所有内存访问都通过页表进行,每个进程都有自己独立的页表。对于共享物理内存页面的两个进程,其物理页帧号必须出现在它们页表条目中。
图 3.1 显示了两个进程,每个进程都共享物理页帧号 4。对于进程 X,这是虚拟页帧号 4,而对于进程 Y,这是虚拟页帧号 6。这说明了关于共享页面的一个有趣的观点:共享的物理页面不必存在于共享它的任何或所有进程的虚拟内存中的相同位置。
Alpha AXP 处理器没有特殊的物理寻址模式。相反,它将内存空间划分为多个区域,并将其中两个指定为物理映射地址。此内核地址空间称为 KSEG 地址空间,它包含从 0xfffffc0000000000 开始的所有地址。为了从 KSEG 中链接的代码(根据定义,内核代码)执行或访问那里的数据,代码必须在内核模式下执行。Alpha 上的 Linux 内核链接为从地址 0xfffffc0000310000 执行。
页表条目还包含访问控制信息。由于处理器已经使用页表条目将进程的虚拟地址映射到物理地址,因此它可以轻松地使用访问控制信息来检查进程是否以不应有的方式访问内存。
有很多原因需要限制对内存区域的访问。某些内存(例如包含可执行代码的内存)自然是只读内存;操作系统不应允许进程覆盖其可执行代码的数据。相比之下,包含数据的页面可以写入,但尝试将该内存作为指令执行应失败。大多数处理器至少有两种执行模式:内核和用户。您不希望用户执行内核代码,也不希望内核数据结构可访问,除非处理器在内核模式下运行。
访问控制信息保存在 PTE 中,并且是处理器特定的;图 3.2 显示了 Alpha AXP 的 PTE。位字段具有以下含义:
以下两位由 Linux 定义和使用:
缓冲区缓存包含块设备驱动程序使用的数据缓冲区。
这些缓冲区的大小是固定的(例如 512 字节),并且包含已从块设备读取或正在写入块设备的信息块。块设备是一种只能通过读取和写入固定大小的数据块来访问的设备。所有硬盘都是块设备。
缓冲区缓存通过设备标识符和所需的块号进行索引,用于快速查找数据块。块设备始终仅通过缓冲区缓存访问。如果在缓冲区缓存中可以找到数据,则不需要从物理块设备(例如硬盘)读取数据,并且访问速度更快。
用于加快对磁盘上映像和数据的访问。
它用于缓存文件中逻辑内容,一次缓存一个页面,并通过文件和文件内的偏移量进行访问。当页面从磁盘读取到内存中时,它们会被缓存在页面缓存中。
只有修改过的(或脏)页面才会保存在交换文件中。
只要这些页面在写入交换文件后没有被修改,那么下次页面被换出时,就不需要将其写入交换文件,因为该页面已经在交换文件中。相反,该页面可以简单地丢弃。在重度交换系统中,这可以节省许多不必要的且代价高昂的磁盘操作。
一种常用的硬件缓存是在处理器中实现的;页表条目的缓存。在这种情况下,处理器并不总是直接读取页表,而是缓存它需要的页面的转换。这些是转换后备缓冲区,包含系统中一个或多个进程的页表条目的缓存副本。
当引用虚拟地址时,处理器将尝试查找匹配的 TLB 条目。如果找到匹配的条目,它可以直接将虚拟地址转换为物理地址,并对数据执行正确的操作。如果处理器找不到匹配的 TLB 条目,则必须让操作系统帮助。它通过向操作系统发出 TLB 未命中的信号来完成此操作。系统特定的机制用于将该异常传递给可以修复问题的操作系统代码。操作系统为地址映射生成新的 TLB 条目。当异常被清除后,处理器将再次尝试转换虚拟地址。这次它将工作,因为现在 TLB 中存在该地址的有效条目。
使用缓存(无论是硬件缓存还是其他缓存)的缺点是,为了节省精力,Linux 必须花费更多时间和空间来维护这些缓存,并且如果缓存损坏,系统将会崩溃。
Linux 假设存在三级页表。访问的每个页表都包含下一级页表的页帧号。图 3.3 显示了如何将虚拟地址分解为多个字段;每个字段提供特定页表的偏移量。为了将虚拟地址转换为物理地址,处理器必须获取每个级别字段的内容,将其转换为包含页表的物理页面的偏移量,并读取下一级页表的页帧号。重复此过程三次,直到找到包含虚拟地址的物理页面的页帧号。现在,虚拟地址中的最后一个字段(字节偏移量)用于查找页面内的数据。
Linux 运行的每个平台都必须提供转换宏,以允许内核遍历特定进程的页表。这样,内核不需要知道页表条目的格式或它们的排列方式。
这是如此成功,以至于 Linux 对具有三级页表的 Alpha 处理器和具有两级页表的 Intel x86 处理器使用相同的页表操作代码。
系统中的所有物理页面都由mem_map数据结构描述,该数据结构是mem_map_t
1 结构的列表,该列表在启动时初始化。每个mem_map_t描述系统中的单个物理页面。重要的字段(就内存管理而言)是:
页面分配代码使用free_area向量来查找和释放页面。整个缓冲区管理方案都由此机制支持,就代码而言,处理器使用的页面大小和物理分页机制是无关紧要的。
的每个元素free_area包含有关页面块的信息。数组中的第一个元素描述单个页面,下一个元素描述 2 个页面的块,下一个元素描述 4 个页面的块,依此类推,以 2 的幂次方递增。list元素用作队列头,并具有指向page数据结构的指针mem_map数组。空闲的页面块在此处排队。map是指向位图的指针,该位图跟踪此大小的已分配页面组。如果第 N 个页面块是空闲的,则设置位图的第 N 位。
图 free-area-figure 显示了free_area结构。元素 0 有一个空闲页面(页帧号 0),元素 2 有 2 个 4 页面的空闲块,第一个从页帧号 4 开始,第二个从页帧号 56 开始。
Linux 使用伙伴算法 2 来有效地分配和释放页面块。页面分配代码
尝试分配一个或多个物理页面的块。页面以 2 的幂次方大小的块分配。这意味着它可以分配 1 个页面、2 个页面、4 个页面等等的块。只要系统中有足够的空闲页面来满足此请求(nr_free_pages > min_free_pages),分配代码将搜索free_area以查找请求大小的页面块。的每个元素free_area都有一个内存映射,描述了该大小块的已分配和空闲页面块。例如,数组的元素 2 有一个内存映射,描述了每个 4 页长的空闲和已分配块。
分配算法首先搜索请求大小的页面块。它沿着在free_area数据结构的list元素上排队的空闲页面链。如果没有请求大小的页面块空闲,则查找下一个大小(是请求大小的两倍)的块。此过程一直持续到搜索完所有free_area或找到页面块为止。如果找到的页面块大于请求的页面块,则必须将其分解,直到有大小合适的块为止。由于块的大小都是 2 的幂次方页面,因此这种分解过程很容易,因为您只需将块分成两半即可。空闲块在适当的队列上排队,并将分配的页面块返回给调用者。
例如,在图 3.4 中,如果请求了 2 个页面的块,则第一个 4 个页面的块(从页帧号 4 开始)将被分解为两个 2 个页面的块。第一个块(从页帧号 4 开始)将作为分配的页面返回给调用者,第二个块(从页帧号 6 开始)将作为 2 个页面的空闲块排队到free_area数组的元素 1 上。
分配页面块往往会使内存碎片化,更大的空闲页面块被分解成更小的页面块。页面释放代码
在可能的情况下,将页面重新组合成更大的空闲页面块。实际上,页面块大小很重要,因为它允许轻松地将块组合成更大的块。
每当释放页面块时,都会检查相邻或伙伴的相同大小的块,以查看它是否空闲。如果是,则将其与新释放的页面块组合,以形成下一个大小页面块的新空闲页面块。每次将两个页面块重新组合成更大的空闲页面块时,页面释放代码都会尝试将该块重新组合成更大的块。通过这种方式,空闲页面块的大小与内存使用量允许的大小一样大。
例如,在图 3.4 中,如果要释放页帧号 1,则将其与已空闲的页帧号 0 组合,并作为大小为 2 页面的空闲块排队到free_area的元素 1 上。
当执行映像时,可执行映像的内容必须被带入进程的虚拟地址空间。对于可执行映像已链接使用的任何共享库,情况也是如此。可执行文件实际上并未被带入物理内存,而是仅仅链接到进程的虚拟内存中。然后,当运行的应用程序引用程序的部分时,映像将从可执行映像带入内存。将映像链接到进程的虚拟地址空间称为内存映射。
每个进程的虚拟内存都由mm_struct数据结构表示。这包含有关其当前正在执行的映像的信息(例如bash),并且还具有指向多个vm_area_struct数据结构的指针。每个vm_area_struct数据结构描述了虚拟内存区域的起始和结束位置、进程对该内存的访问权限以及该内存的一组操作。这些操作是 Linux 在操作此虚拟内存区域时必须使用的一组例程。例如,虚拟内存操作之一在进程尝试访问此虚拟内存但发现(通过页错误)内存实际上不在物理内存中时执行正确的操作。此操作是 nopage 操作。当 Linux 按需分页可执行映像的页面到内存中时,将使用 nopage 操作。
当可执行映像映射到进程的虚拟地址时,会生成一组vm_area_struct数据结构。每个vm_area_struct数据结构代表可执行映像的一部分;可执行代码、初始化的数据(变量)、未初始化的数据等等。Linux 支持许多标准虚拟内存操作,并且在创建vm_area_struct数据结构时,正确的虚拟内存操作集与它们关联。
一旦可执行映像被内存映射到进程的虚拟内存中,它就可以开始执行。由于只有映像的开头部分被物理拉入内存,因此它很快就会访问尚未在物理内存中的虚拟内存区域。当进程访问没有有效页表条目的虚拟地址时,处理器将向 Linux 报告页错误。
页错误描述了发生页错误的虚拟地址以及导致错误的内存访问类型。
Linux 必须找到vm_area_struct表示页错误发生的内存区域的。由于搜索vm_area_struct数据结构对于有效处理页错误至关重要,因此这些结构链接在 AVL(Adelson-Velskii 和 Landis)树结构中。如果此错误的虚拟地址没有vm_area_struct数据结构,则此进程访问了非法虚拟地址。Linux 将向进程发出信号,发送SIGSEGV信号,如果进程没有该信号的处理程序,它将被终止。
接下来,Linux 会根据为此虚拟内存区域允许的访问类型检查发生的页错误类型。如果进程以非法方式访问内存,例如写入仅允许读取的区域,也会向其发出内存错误信号。
现在 Linux 已经确定页错误是合法的,它必须处理它。
Linux 必须区分交换文件中的页面和磁盘上某处的可执行映像的一部分的页面。它通过使用此错误虚拟地址的页表条目来做到这一点。
如果页面的页表条目无效但不为空,则页错误是针对当前保存在交换文件中的页面。对于 Alpha AXP 页表条目,这些条目是没有设置其有效位但其 PFN 字段中具有非零值的条目。在这种情况下,PFN 字段保存有关页面在交换中的位置(以及哪个交换文件)的信息。有关如何处理交换文件中的页面的描述将在本章稍后介绍。
并非所有vm_area_struct数据结构有一组虚拟内存操作,即使是那些操作也可能没有 nopage 操作。这是因为默认情况下,Linux 会通过分配一个新的物理页并为其创建有效的页表项来修复访问。如果此虚拟内存区域存在 nopage 操作,Linux 将使用它。
通用的 Linux nopage 操作用于内存映射的可执行镜像,它使用页缓存将所需的镜像页带入物理内存。
无论所需的页如何被带入物理内存,进程页表都会被更新。可能需要硬件特定的操作来更新这些条目,特别是如果处理器使用转换后备缓冲器。现在页错误已被处理,它可以被解除,并且进程在导致错误虚拟内存访问的指令处重新启动。
Linux 页缓存的作用是加速对磁盘上文件的访问。内存映射文件一次读取一页,这些页存储在页缓存中。图 3.6 显示页缓存由以下内容组成:page_hash_table,一个指向mem_map_t数据结构的指针向量。
Linux 中的每个文件都由 VFS 标识inode数据结构(在第 文件系统章节 中描述),每个 VFSinode都是唯一的,并且完全描述一个且仅一个文件。页表的索引是从文件的 VFSinode和文件中的偏移量派生的。
每当从内存映射文件中读取页面时,例如,当需要在请求分页期间将其带回内存时,该页面将通过页缓存读取。如果该页面存在于缓存中,则返回一个指向mem_map_t表示它的数据结构的指针给缺页错误处理代码。否则,必须从保存镜像的文件系统中将页面带入内存。Linux 分配一个物理页并从磁盘上的文件读取该页。
如果可能,Linux 将启动对文件中下一页的读取。这种单页预读意味着如果进程正在串行访问文件中的页面,则下一页将在内存中等待进程。
随着时间的推移,页缓存会随着镜像的读取和执行而增长。当页面不再需要时,例如当镜像不再被任何进程使用时,页面将从缓存中删除。当 Linux 使用内存时,它可能会开始物理页不足。在这种情况下,Linux 将减小页缓存的大小。
当物理内存变得稀缺时,Linux 内存管理子系统必须尝试释放物理页。这项任务落在内核交换守护进程 (kswapd) 的身上。
内核交换守护进程是一种特殊类型的进程,即内核线程。内核线程是没有虚拟内存的进程,相反,它们在物理地址空间中的内核模式下运行。内核交换守护进程的名称有点用词不当,因为它不仅仅是将页面交换到系统的交换文件中。它的作用是确保系统中有足够的空闲页,以保持内存管理系统高效运行。
内核交换守护进程 (kswapd) 由内核 init 进程在启动时启动,并等待内核交换计时器定期过期。
每次计时器过期时,交换守护进程都会查看系统中空闲页的数量是否变得太低。它使用两个变量,free_pages_high 和 free_pages_low 来决定是否应该释放一些页面。只要系统中空闲页的数量保持在 free_pages_high 之上,内核交换守护进程就不会执行任何操作;它会再次休眠,直到其计时器下次过期。为了进行此检查,内核交换守护进程会考虑当前正在写入交换文件的页数。它在 nr_async_pages 中保留了这些页面的计数;每次将页面排队等待写入交换文件时,此计数都会递增,而在写入交换设备的写入完成时,此计数会递减。free_pages_low 和 free_pages_high 在系统启动时设置,并且与系统中的物理页数相关。如果系统中空闲页的数量已降至 free_pages_high 以下,或者更糟糕的是降至 free_pages_low 以下,则内核交换守护进程将尝试三种方法来减少系统使用的物理页数
如果系统中空闲页的数量已降至 free_pages_low 以下,则内核交换守护进程将在下次运行之前尝试释放 6 个页面。否则,它将尝试释放 3 个页面。依次尝试上述每种方法,直到释放了足够的页面。内核交换守护进程会记住上次尝试释放物理页面时使用的方法。每次运行时,它都会开始尝试使用上次成功的方法来释放页面。
在释放了足够的页面后,交换守护进程会再次休眠,直到其计时器过期。如果内核交换守护进程释放页面的原因是系统中空闲页的数量已降至 free_pages_low 以下,则它仅休眠通常时间的一半。一旦空闲页的数量超过 free_pages_low,内核交换守护进程就会恢复到检查之间休眠更长时间。
页缓存和缓冲区缓存中保存的页面是释放到free_area向量中的良好候选者。页缓存包含内存映射文件的页面,可能包含占用系统内存的不必要的页面。同样,缓冲区缓存包含从物理设备读取或写入物理设备的缓冲区,也可能包含不需要的缓冲区。当系统中的物理页开始耗尽时,从这些缓存中丢弃页面相对容易,因为它不需要写入物理设备(与将页面换出内存不同)。丢弃这些页面不会产生太多有害的副作用,除了使访问物理设备和内存映射文件变慢之外。但是,如果从这些缓存中丢弃页面的操作是公平地完成的,则所有进程将平等地受到影响。
每次内核交换守护进程尝试缩小这些缓存时
它都会检查mem_map页向量中的一个页面块,以查看是否可以从物理内存中丢弃任何页面。如果内核交换守护进程正在密集地交换,即如果系统中空闲页的数量已降至危险的低水平,则检查的页面块的大小会更大。页面块以循环方式检查;每次尝试缩小内存映射时,都会检查不同的页面块。这被称为时钟算法,就像时钟的分针一样,整个mem_map页向量一次检查几个页面。
检查的每个页面都会检查它是否缓存在页缓存或缓冲区缓存中。您应该注意,此时不考虑丢弃共享页面,并且一个页面不能同时存在于两个缓存中。如果页面不在任何一个缓存中,则检查mem_map页向量中的下一个页面。
页面缓存在缓冲区缓存中(或者更确切地说,页面中的缓冲区被缓存)以提高缓冲区分配和释放的效率。内存映射缩小代码尝试释放包含在被检查页面中的缓冲区。
如果所有缓冲区都被释放,那么包含它们的页面也将被释放。如果检查的页面在 Linux 页缓存中,则将其从页缓存中删除并释放。
当在此尝试中释放了足够的页面后,内核交换守护进程将等待直到下次定期唤醒。由于释放的页面都不是任何进程的虚拟内存的一部分(它们是缓存的页面),因此无需更新任何页表。如果未丢弃足够的缓存页面,则交换守护进程将尝试换出一些共享页面。
内核交换守护进程在换出 System V 共享内存页时也使用时钟算法。
。每次运行时,它都会记住上次换出哪个共享虚拟内存区域的哪个页面。它通过保留两个索引来做到这一点,第一个索引是指向shmid_ds数据结构集的索引,第二个索引是指向 System V 共享内存的此区域的页表项列表的索引。这确保了它公平地选择 System V 共享内存区域作为牺牲品。
由于 System V 共享内存的给定虚拟页面的物理页帧号包含在共享此虚拟内存区域的所有进程的页表中,因此内核交换守护进程必须修改所有这些页表,以表明该页面不再在内存中,而是现在保存在交换文件中。对于它正在换出的每个共享页面,内核交换守护进程都会在每个共享进程的页表中找到页表项(通过从每个vm_area_struct数据结构中跟踪指针)。如果此进程的 System V 共享内存页面的页表项有效,它会将其转换为无效但已换出的页表项,并将此(共享)页面的用户计数减一。已换出的 System V 共享页表项的格式包含一个指向shmid_ds数据结构集的索引和一个指向 System V 共享内存的此区域的页表项列表的索引。
如果在修改了所有共享进程的页表后,页面的计数为零,则可以将共享页面写入交换文件。指向shmid_ds此 System V 共享内存区域的数据结构指向的列表中的页表项被替换为已换出的页表项。已换出的页表项无效,但包含一个指向打开的交换文件集和该文件中可以找到已换出页面的偏移量的索引。当页面必须被带回物理内存时,将使用此信息。
交换守护进程依次查看系统中的每个进程,以查看它是否是交换的良好候选者。
良好的候选者是可以交换的进程(有些进程不能交换)以及具有一个或多个可以从内存中交换或丢弃的页面的进程。只有当页面中的数据无法通过其他方式检索时,页面才会被从物理内存换出到系统的交换文件中。
可执行镜像的许多内容来自镜像文件,并且可以很容易地从该文件中重新读取。例如,镜像的可执行指令永远不会被镜像修改,因此永远不会写入交换文件。这些页面可以简单地丢弃;当进程再次引用它们时,它们将从可执行镜像中带回内存。
一旦找到要交换的进程,交换守护进程就会遍历其所有虚拟内存区域,查找未共享或未锁定的区域。
Linux 不会换出它选择的进程的所有可交换页面;相反,它只删除少量页面。
如果页面锁定在内存中,则无法交换或丢弃页面。
Linux 交换算法使用页面老化。每个页面都有一个计数器(保存在mem_map_t数据结构中),该计数器为内核交换守护进程提供了一些关于页面是否值得交换的想法。页面在未使用时老化,并在访问时恢复活力;交换守护进程仅换出旧页面。首次分配页面时的默认操作是为其初始年龄设置为 3。每次触摸页面时,其年龄都会增加 3,最多为 20。每次内核交换守护进程运行时,它都会使页面老化,将其年龄减 1。这些默认操作可以更改,因此,它们(和其他交换相关信息)存储在swap_control数据结构中。
如果页面已旧(年龄 = 0),则交换守护进程将进一步处理它。脏页是可以换出的页面。Linux 使用 PTE 中特定于体系结构的位来描述页面(参见图 3.2)。但是,并非所有脏页都必须写入交换文件。进程的每个虚拟内存区域都可能有其自己的交换操作(由vm_ops指针指向vm_area_struct),并且使用该方法。否则,交换守护进程将在交换文件中分配一个页面并将该页面写入设备。
页面的页表项被替换为一个标记为无效的页表项,但该页表项包含有关页面在交换文件中的位置的信息。这是页面所在的交换文件中的偏移量以及正在使用的交换文件的指示。无论使用何种交换方法,原始物理页都通过将其放回free_area来释放。干净(或者更确切地说,非脏)页面可以被丢弃并放回free_area以供重用。
如果已换出或丢弃了足够多的可交换进程页面,则交换守护进程将再次休眠。下次唤醒时,它将考虑系统中的下一个进程。通过这种方式,交换守护进程逐渐减少每个进程的物理页面,直到系统再次达到平衡。这比换出整个进程公平得多。
当将页面换出到交换文件时,Linux 会避免在不必要时写入页面。有时,页面既在交换文件中又在物理内存中。当从内存中换出的页面在被进程再次访问时又被带回内存时,就会发生这种情况。只要内存中的页面未被写入,交换文件中的副本仍然有效。
Linux 使用交换缓存来跟踪这些页面。交换缓存是页表项的列表,系统中每个物理页面一个页表项。这是用于换出页面的页表项,描述了页面所在的交换文件及其在交换文件中的位置。如果交换缓存条目非零,则它表示一个页面,该页面保存在未修改的交换文件中。如果页面随后被修改(通过写入),则其条目将从交换缓存中删除。
当 Linux 需要将物理页面换出到交换文件时,它会查阅交换缓存,如果此页面存在有效条目,则无需将页面写入交换文件。这是因为内存中的页面自上次从交换文件读取以来未被修改。
交换缓存中的条目是已换出页面的页表项。它们被标记为无效,但包含允许 Linux 找到正确的交换文件以及该交换文件中的正确页面的信息。
特定于处理器的缺页错误处理代码必须找到vm_area_struct数据结构,该数据结构描述包含导致错误的虚拟地址的虚拟内存区域。它通过搜索此进程的vm_area_struct数据结构来完成此操作,直到找到包含导致错误的虚拟地址的数据结构。这是非常注重时间的代码,并且进程的vm_area_struct数据结构经过安排,以使此搜索花费尽可能少的时间。
在执行了适当的特定于处理器的操作并发现导致错误的虚拟地址是虚拟内存的有效区域后,缺页错误处理变得通用,并且适用于 Linux 运行的所有处理器。
通用的缺页错误处理代码查找导致错误的虚拟地址的页表项。如果它找到的页表项是用于换出页面的,则 Linux 必须将页面换回物理内存。用于换出页面的页表项的格式是特定于处理器的,但所有处理器都将这些页面标记为无效,并将查找交换文件内页面的必要信息放入页表项中。Linux 需要此信息才能将页面带回物理内存。
此时,Linux 知道导致错误的虚拟地址,并且有一个页表项,其中包含有关此页面已交换到何处的信息。vm_area_struct数据结构可能包含指向例程的指针,该例程会将它描述的虚拟内存区域的任何页面换回物理内存。这是它的 swapin 操作。如果此虚拟内存区域存在 swapin 操作,则 Linux 将使用它。实际上,这就是处理换出的 System V 共享内存页面的方式,因为它需要特殊处理,因为换出的 System V 共享页面的格式与普通换出的页面略有不同。可能没有 swapin 操作,在这种情况下,Linux 将假定这是一个不需要特殊处理的普通页面。
它分配一个空闲物理页,并从交换文件读取换出的页面。告诉它交换文件中的位置(以及哪个交换文件)的信息取自无效的页表项。
如果导致缺页错误的访问不是写入访问,则该页面将保留在交换缓存中,并且其页表项未标记为可写。如果随后写入该页面,则会发生另一个缺页错误,并且此时,该页面被标记为脏页,并且其条目将从交换缓存中删除。如果未写入该页面,并且需要再次换出该页面,则 Linux 可以避免将页面写入其交换文件,因为该页面已在交换文件中。
如果导致页面从交换文件带入的访问是写入操作,则此页面将从交换缓存中删除,并且其页表项标记为脏页和可写。
1 容易混淆的是,该结构也称为 page 结构。
2 此处为参考文献