18页回收和页交换

18页回收和页交换

内核将很少使用的部分内存换出到块设备,这种机制称为页交换(swapping)或换页(paging)。但页交换不是从内存逐出页的唯一机制。如果一个很少使用的页的后备存储器是一个块设备(例如,文件的内存映射),那么就无须换出被修改的页,而是可以直接与块设备同步。腾出的页帧可以重用,如果再次需要该数据,可以从来源重新建立该页。如果页的后备存储器是一个文件,但不能在内存中修改(例如,二进制可执行文件的数据),那么在当前不需要的情况下,可以直接丢弃该页这三种技术,连同选择很少使用页的策略,统称为页面回收(page reclaim)。请注意,分配给核心内核(即并非用于缓存)的页是不能回收的,因为这种做法带来的复杂性的增加,将超出其好处。

18.1 概述

前一章描述了数据与底层块设备的同步,这能够缓解内核在可用物理内存达到极限时所面临的态势。将缓存的数据回写,可以释放一些内存页,以便将物理内存用于更重要的功能。所涉及的数据可以在需要时从块设备再次读取,虽然会花费时间,但不会丢失信息。

该方法也有其局限性。在某些时候,会遇到这样的情况,缓存和缓冲区都不能再收缩。另外,数据同步对动态产生的内存页是不适用的,因为这种页没有后备存储器。

内核连同处理器(处理器管理的虚拟地址空间比实际存在的物理内存要大很多)可以征用部分磁盘,用作内存的扩展。由于硬盘比物理内存慢很多,页交换只是一种紧急情况下的备用方案,它使得系统可以运行,但速度会降低很多

交换(swapping)最初是指换出整个进程,包括其所有的数据、代码等,而不像现在这样,将进程数据选择性地逐页换出到二级存储。UNIX的早期版本采用了换出整个进程的策略,在某些情况下这可能是合适的,但现在这种行为是不可想象的。这种策略所导致的上下文切换期间的延迟,使得交互性工作反应缓慢,令人难以容忍。下文并不区分交换和换页。二者都表示细粒度换出进程的数据。

在内核中考虑如何实现页交换和页面回收,必须回答下面两个问题

  1. 根据何种方案来回收页,即内核为确保最大可能利益以及最小可能损失,如何判断应该回收哪些页?
  2. 换出的页在交换区中如何组织,内核如何将页写入到交换区,如何在此后需要时再次读取?内核如何将页与后备存储设备进行同步?

第一个问题,即换出哪些页、在物理内存中保留哪些页的决策,对系统性能有决定性的影响。

18.1.1 可换出的页(能换出到交换区的页)

只有少量几种页可以换出到交换区,如下所述.对其他页来说,换出到块设备上与之对应的后备存储器即可

  1. 类别为MAP_ANONYMOUS的页,没有关联到文件(或属于/dev/zero的一个映射),例如,这可能是进程的栈或是使用mmap匿名映射的内存区。(GNU C标准库的参考手册或系统程序设计方面通常的标准工具书,都提供了有关此类映射的更多信息。)
  2. 进程的私有映射用于映射修改后不向底层块设备回写的文件,通常换出到交换区。因为这种情况下文件不能再用作后备存储器,因而在物理内存较少时,需要将相关页换出到交换区,因为此时不能从文件恢复页的内容。内核(以及C标准库)使用MAP_PRIVATE标志来创建此类映射。
  3. 所有属于进程堆以及使用malloc分配的页(malloc又使用了brk系统调用或匿名映射),参见第3章
  4. 用于实现某种进程间通信机制的页。例如,用于在进程之间交换数据的共享内存页。

内核本身使用的内存页决不会换出。原因是显然的。这将显著增加内核代码的复杂性。由于与其他用户应用程序相比,内核不需要非常多的内存,而且与付出的额外工作量相比,将内核内存页换出的潜在收益太低。

很自然,用于将外设映射到内存空间的页也不能换出。换出这些页是没有意义的,特别是,这些页只用作应用程序和设备之间通信的手段,而并非用于持久存储数据。

尽管不可能换出所有类型的页,但内核的页交换和页面回收机制仍然必须考虑到基于其他后备存储器的页面类型。最常见的页面类型是与映射到内存中的文件数据有关的页。最终,将哪种类型的页从物理内存写到后备存储器其实是不相关的,因为效果总是同样的:释放了一个页帧,为更重要、必须驻留在物理内存中的数据腾出了空间。

18.1.2 页颠簸(同一页反复在交换区和内存间交换)

在进行页交换时,可能发生的另一个问题是页颠簸(page thrashing)。顾名思义,这个问题涉及交换区和物理内存之间密集的数据传输问题归结为页的来回、反复的交换。在系统进程的数目增加时,这种现象发生的几率也会增加。在换出重要的数据后不久再次需要该数据时,会发生这种现象。

为防止页颠簸,内核必须解决的主要的问题是,尽可能精确地确定一个进程的工作集(working set,即使用最频繁的那些内存页),将最不重要的那些页移到交换区或其他后备存储器,而真正重要的页则一直驻留在内存中。

为此,内核需要一种合适的算法,来评估整个系统中的页的重要性。一方面,对页重要性的评估必须尽可能公平,使得进程不会得到太多偏爱或受到太多损失。另一方面,该算法的实现必须简单高效,确保不会花费太多的时间来选择需要换出的页。

许多类型的CPU提供了不同的方法,来支持内核完成此任务,各个方法的复杂程度各有不同。但Linux不可能使用所有的方法,因为比较简单的CPU不见得提供了这些方法,而同时仿真复杂的方法可能又比较困难。照例,必须找到一个最小公分母,使得内核能够在此基础上建立硬件无关层。

这里有一个特别简单但很重要的技巧,它完全独立于处理器的能力,即在系统中维护一个交换令牌(swap token),赋予换入页的进程。内核会试图避免从该进程换出页,以减轻该进程的颠簸,使之有时间完成任务。不久之后,交换令牌将传递到另一个进程,该进程也经历了页交换,而且比当前令牌持有者更需要内存

18.1.3 页交换算法介绍

在过去几十年中,已经为页面交换开发了一整套算法,其中每个算法都有自身特定的优点和缺点。操作系统方面的一般文献包括了这方面的详细描述和分析。下面将描述Linux页交换实现所基于的两种技术。

  1. 二次机会法
    第二次机会(second chance)是一种算法,实现非常简单,对经典FIFO算法有一点小的改进。在FIFO算法中,系统的页在一个链表中管理。在发生缺页异常时,新引用的页置于该链表的开始,这自动将现存的页向后移动一个位置。由于在FIFO队列中只有有限个位置,系统必定在某个时候达到其容量极限。那时,队列尾部的页将“脱离”链表并被换出。当再次需要这些页时,处理器会触发一个缺页异常,使内核再次读取对应页的数据,并将该页置于链表开头。
    这个实现不是特别巧妙。在换出页时,没有考虑该页的使用情况,是使用频繁还是很少使用。在确定数目的缺页异常(由队列中的位置数决定)之后,页将写出到交换区。如果经常需要使用某页,则会立即再次读取,这不利于系统性能。
    这种情况是可以改进的,只需在换出一页之前,向其提供第二次机会。每页都指定一个专门的字段,包含一个由硬件控制的比特位。在访问该页时,该比特位自动设置为1。软件(即内核)负责清除该比特位
    在一页到达链表末尾时,内核不会立即将其换出,而是首先检查前述的比特位是否置位。如果置位,则清除该比特位,并将该页移动到FIFO队列的开始。换言之,将其作为添加到系统的新页处理。如果该比特位没有置位,则将其换出。
    有了该扩展,该算法对页是否频繁使用的考虑降到了最低限度,但却提供了最新的内存管理技术所预期的性能。在与其他技术联合使用时,第二次机会算法是一个很好的起点
  2. LRU算法
    LRU是least recently used(最近最少使用)的缩写,指一系列试图根据一种相似的方案来找到使用最少的页的算法。这种逆向方法规避了比较复杂的搜索最常用页的操作。
    很显然,过去一段时间内频繁使用的页,在不久的将来很可能再次使用。LRU算法基于上述说法的逆命题,假定最近不使用的页在较短的时间内也不会频繁需要使用。因而在内存缺乏时,这样的页将成为换出操作的可能候选者
    LRU的基本原理可能比较简单,但合理实现该算法却比较难。内核如何尽可能简单地标记页或进行排序,以便在不需要太多时间组织数据结构的情况下,就能估算出页的访问频度?最简单的LRU算法使用一个双链表,其中包括系统中的所有页。每次访问内存时,该链表都重新排序。访问的页被找到,并移动到链表的开头。随着时间的推移,这将导致一种“热平衡”,经常使用的页位于链表的开头,而最少使用的页刚好位于链表末尾(第16章讨论了一个类似的算法,用于管理块缓存)。
    该算法的工作很漂亮,但其效率只能处理少量数据。这意味着不能将其原本的形式直接用于内存管理,否则在系统性能方面损失太大。因而需要更简单的实现,消耗更少的CPU时间。
    处理器的专门支持能够使LRU算法实现的开销大大降低。遗憾的是,仅有少量体系结构提供了这种支持,因而Linux不能使用。毕竟,不应该根据特定的处理器类型来调整内存管理子系统。因此,引入一个计数器,每个CPU周期都将该计数器加1。每次访问页时,都将页的一个计数器字段设置为系统计数器的值。该操作必须由处理器自身来执行,确保足够高的速度。如果因为某个需要的页不可用而发生缺页异常,操作系统只需要比较所有页的计数器,即可确定哪个页的访问时间离现在最远。这种技术仍然需要在每次发生缺页异常时搜索所有内存页的链表,但不需要在每次内存访问后都进行冗长的链表操作。

18.2 Linux内核中的页面回收和页交换实现要注意的问题

在考虑Linux页面回收子系统的技术实现以及该子系统是怎样满足需求的之前,本节概述该子系统的设计决策
如果从比较高的层次考虑页交换,而不考虑开发细节,那么页的换出和所有相关的操作看起来都不是非常复杂。遗憾的是,事实刚好相反。内核的任何其他部分都没有虚拟内存子系统那么多技术困难,而页交换的实现只是其中之一。为使实现成功运转,不仅需要考虑大量琐碎的硬件细节,尤其要考虑与内核各个部分的大量关联。速度在其中发挥了关键作用,因为系统性能最终决定于内存管理子系统的性能。内存管理成为最热的内核开发主题之一绝非无由,它已经引起了无数讨论、UseNet上的激烈争论和相互竞争的实现。
在讨论页交换子系统的设计时,需要考虑以下各方面的问题

  1. 应该如何在块设备介质上组织交换区?该组织方式不仅需要能够唯一标识换出的每一页,而且应该尽可能高效地利用内存空间,使得读写操作能够以最高速度进行。
  2. 内核能够利用哪些方法来检查在何时将多少页换出?在为即将出现的需求提供空闲页帧和最小化页交换操作所需时间这两者之间,这些方法应该尽可能达成均衡
  3. 根据何种原则选择换出的页?换言之,应该选用哪种页面替换算法?
  4. 如何尽可能高效而快速地处理缺页异常,页如何从交换区返回到系统物理内存?
  5. 哪些数据可以从各种系统缓存删除(例如,从inode或dentry缓存),而不需要与后备存储器同步(因为它们可以根据其他信息间接重建)?该问题实际上与页交换操作的执行没有直接关系,但同时涉及缓存和页交换子系统。但因为缩减缓存是由页交换子系统发起的,所以将在下文阐述该问题。

如前所述,为实现一个高效而强大的页交换系统,最重要的不仅仅是技术细节,也包括整个系统的设计,该设计必须能够支持系统各组件之间尽可能充分的交互,以确保页交换能够流畅协调地进行。

18.2.1 交换区使用说明

换出的页或者保存在一个没有文件系统的专用分区中,或者存储在某个现存文件系统中的一个定长文件中。每个系统管理员都知道,可以同时使用几个这样的区域。还可以根据各个交换区的速度不同,为其指定优先级。内核使用交换区时可以根据优先级进行选择。
每个交换区都细分为若干连续的槽(slot),每个槽的长度刚好与系统的一个页帧相同。在大多数处理器上,是4 KiB。但较新的系统通常会使用更大的页
本质上,系统中的任何一页都可以容纳到交换区的任一槽中。但内核还使用了一种称为聚集(clustering)的构造法,使得能够尽快访问交换区。进程内存区中连续的页(或至少是连续换出的页)将按照特定的聚集大小(通常是256页)逐一写到硬盘上。如果交换区中没有更多空间可容纳此长度的聚集,内核可以使用其他任何位置上的空闲槽位
如果使用了几个优先级相同的交换区,内核将使用一种循环进程来确保尽可能均匀地利用各个交换区。如果交换区的优先级不同,内核首先使用高优先级的交换区,然后逐渐转移到优先级较低的交换区。
为跟踪内存页在交换分区中的位置,内核必须维护一些数据结构,将该信息保存在内存中。结构中,最重要的数据成员是一个位图,用于跟踪交换区中各槽位的使用/空闲状态。其他成员包含的数据用于支持选择接下来使用的槽位,以及聚集的实现。
有两个用户空间工具可用于创建和启用交换区,分别是mkswap(用于“格式化”一个交换分区/文件)和swapon(用于启用一个交换区)。因为这些程序对一个正常运转的页交换子系统十分关键,因而本书将在下文描述这两个程序(以及用于swapon的系统调用)。

18.2.2 检查内存使用情况的机制

在换出内存页之前,内核会检查内存的使用情况,确定可用内存容量是否较低。与同步页的情况相似,内核联合使用了如下两种机制

  1. 一个周期性的守护进程(kswapd)在后台运行,该进程不断检查当前的0内存使用情况,以便在达到特定的阈值时发起页的换出操作。使用该方法,确保了不会出现突然需要换出大量页的情况。这种情况将导致系统出现很长的等待时间,必须不惜一切代价防止。
  2. 但内核在某些情况下,必须能够预期可能突然出现的严重内存不足,例如在通过伙伴系统分配一大块内存时,或创建缓冲区时。如果没有足够的物理内存可用来满足对内存的请求,内核必须尽快换出页,以期释放一些内存空间。在紧急情况下的换出操作,属于直接回收(direct reclaim)的一部分。

如果内核无法满足对内存的请求,甚至在换出页之后也是如此,那么虚拟内存子系统只有一个选择,即通过OOM(out of memory,内存不足) killer来结束一个进程。虽然OOM killer有时候可能导致严重的损失,总比系统完全崩溃要好。如果在内存不足的情况下不采取措施,很可能导致系统崩溃。

18.2.3 换出页选择机制

页交换子系统面临的关键问题总是同样的。在需要用最低成本为系统带来最大收益的前提下,应该换出哪些页呢?内核混合使用了此前讨论的思想,实现了一种粗粒度的LRU方法,只使用了一种硬件特性,即在访问一页之后设置一个访问位,该功能在内核支持的所有体系结构上都可用,而且还可以毫不费力地进行仿真。

与通用的算法相比,内核对LRU的实现基于两个链表,分别称为活动链表惰性链表系统中的每个内存域都有这样的两个链表)。顾名思义,所有处于活动使用状态的页在一个链表上,而所有惰性页则保存在另一个链表上,这些页虽然可能映射到一个或多个进程,但不经常使用。为在两个链表之间分配页,内核需要定期执行均衡操作,通过上述访问位来确定一页是活动的还是惰性的,换言之,即该页是否经常被系统中的应用程序访问。页在两个链表之间可能会发生双向转移。页可以从活动链表转移到惰性链表,反之亦然。但这种转移不是每访问一页都会发生,它发生的时间间隔会比较长。

随着时间的推移,最不常用的页将收集到惰性链表的末尾。在出现内存不足时,内核将选择换出这些页。因为这些页到换出时,一直都很少使用,所以根据LRU原理,换出这些页对系统的破坏是最小的。

18.2.4 处理缺页异常

Linux运行的所有体系结构都支持缺页异常的概念,当访问虚拟地址空间中的一页,但该页不在物理内存中的时候,将触发缺页异常。缺页异常通知内核从交换区和其他的后备存储器读取缺失的数据。当然,可能需要先删除其他页,以便为新数据腾出空间。

缺页处理分为两个部分。首先,必须使用与处理器相关性较强的代码(汇编语言)来截获该缺页异常,并查询相关的数据。其次,使用系统无关代码进一步处理该异常。由于内核在管理进程时所采用的优化,因而只在后备存储器中查找相关页并将其加载到物理内存是不够的,因为缺页异常可能是由其他原因触发的(参见第4章)。例如,这可能涉及写时复制页,这些页仅在进程分支后执行第一次写访问时,才会进行复制。在按需换页时也会发生缺页异常,按需换页法是指映射的页仅在实际需要时才加载。但这里将忽略这些问题,以专注于换出页需要重新加载到物理内存的情形。

这种情况下,需要完成的工作同样不止是从交换区中查找到目标页。因为如果需要将磁头移动到一个新位置(磁盘寻道),对硬盘的访问可能比普通情况下更慢,所以内核使用了一种预读机制来预测接下来将需要哪些页,读操作包括对这些页的读取。有了上文提到的聚集方法,读取连续页时,磁头在理论上只需要单向移动,而无须来回跳转。

18.2.5 缩减内核缓存

换出属于用户空间应用程序的页,并不是内核释放内存空间的唯一方法。缩减大量缓存,通常也会有很好的成效。很自然,在这里内核也需要判断从缓存移除哪些数据,以及在不过度损害系统性能的情况下能够将用于这些数据的内存空间缩减多少,内核必须据此均衡利弊。因为内核缓存通常不是特别大,所以仅在万不得已时,内核才考虑缩减缓存

在前几章解释过,内核在很多领域提供了各种缓存。这使得很难定义一个一般性的方案并据此缩减缓存,因为很难评估各种缓存所包含的数据的重要性。为此,早期的内核带有大量特定于缓存的函数,以便为各个缓存评估数据的重要性。

现在,用于缩减各种缓存的方法仍然是分别实现的,因为各种缓存的结构有很大的不同,很难采用一种通用的缓存收缩算法。但现在内核提供了一种通用框架,来管理各种缓存收缩方法。用于缩减缓存的函数在内核中称为收缩器(shrinker),可以动态注册。在缺乏内存时,内核将调用所有注册的收缩器来获得内存

18.3 管理交换区

Linux对交换区的支持,是相对比较灵活的。如前所述,可以用不同的优先级来管理几个交换区。这些交换区既可以是本地分区,也可以是具有预定义长度的文件。在活动的系统上,可以动态添加/删除交换分区,而无须重启。

内核尽可能使各种方法在技术上的差别对用户空间透明。内核的模块结构也意味着,与页交换相关的算法可以采用一种通用设计,不同方法的差别只存在于较低的技术层次上。

18.3.1 数据结构

照例,本节从介绍核心数据结构开始,这些结构形成了实现的主干,保存了内核所需的全部信息和数据。交换区管理的基石是mm/swap-info.c中定义的swap_info数组,其中各数组项存储了关于系统中各个交换区的信息(在内核版本2.6.18开发期间,已经添加了在NUMA结点之间物理上迁移页的内容但仍然保持其虚拟地址不变的功能。这要求使用两个swap_info项来处理当前正处于迁移状态的页,因此实际上减少了交换文件可能的数目。如果要在内核中包括页面迁移代码,需要启用配置选项MIGRATION。该选项在NUMA系统是有帮助的,例如可以将页迁移到与使用该页的处理器比较接近的物理内存中,或用于内存的热插拔。但本书不会详细讲述页面迁移。):

//mm/swapfile.c
//存储了各交换区的信息,可以是交换分区或交换文件,通常只使用一个数组成员
static struct swap_info_struct swap_info[MAX_SWAPFILES];

内核使用交换文件(swap file)这个术语时,不仅是指用于页交换的文件,还包括交换分区,因此上述数组包括了这两种类型。因为通常只使用一个交换文件,将数组长度限制为某个特定的数值,不会有什么影响。这个特定的数组长度限制,也不会对其他内存密集型程序带来任何限制,根据具体的体系结构,现在交换区的长度可以达到千兆字节。旧版本中128 MiB的限制不再适用

  1. 交换区的数据结构
    struct swap_info_struct描述了一个交换区

    //include/linux/swap.h
    //交换分区/文件信息
    struct swap_info_struct {
        unsigned int flags;//标志,SWP_USED 等
        int prio;			/*优先级,越大越高*//* swap priority */
        struct file *swap_file;//与该交换区关联的file结构,指向交换分区块设备/dev/hda5或交换文件/mnt/swap1
        struct block_device *bdev;//指向文件/分区所在底层块设备的block_device结构
        struct list_head extent_list;//交换区的存储块链表头,链表元素为 swap_extent->list
        struct swap_extent *curr_swap_extent;//指向最近使用的存储块描述符的指针.用于加速搜索,因为存储块用链表关联,扫描一次耗时.每次新的搜索都从该实例开始,因为通常是对连续的槽位进行访问,所搜索的块通常会位于该区间或下一个区间.如果内核的搜索不能立即成功,则必须逐元素扫描整个区间链表,直至所需块所在区间被找到
        unsigned old_block_size;//存放交换区的磁盘分区自然块大小
        unsigned short * swap_map;//数组,其中包含的项数与交换区槽位数目相同。该数组用作一个访问计数器,每个数组项都表示共享对应换出页的进程的数目
        unsigned int lowest_bit;//管理搜索区域的下界,普通整数不是bit,解释为交换区中线性排布的各槽位的索引
        unsigned int highest_bit;//管理搜索区域的上界,普通整数不是bit,解释为交换区中线性排布的各槽位的索引
        unsigned int cluster_next;//指定了在交换区中接下来使用的槽位(在某个现存聚集中)的索引
        unsigned int cluster_nr;//表示当前聚集中仍然可用的槽位数,在消耗了这些空闲槽位之后,则必须建立一个新的聚集,否则(如果没有足够空闲槽位可用于建立新的聚集)就只能进行细粒度分配了(即不再按聚集分配槽位)。
        unsigned int pages;//保存了交换区中可用槽位的总数,每个槽位可容纳一个内存页
        unsigned int max;//保存了交换区当前包含的页数,不同于pages,该成员不仅计算可用的槽位,而且包括那些(例如,因为块设备故障)损坏或用于管理目的的槽位,max通常等于pages加1,二者差一个槽位有两个原因。首先,交换区的第一个槽位由内核用作标识(毕竟,不能将交换数据写出到硬盘上完全随机的某些部分)。其次,内核还使用第一个槽位来存储状态信息,例如交换区的长度和坏扇区的列表,该信息必须持久保留
        unsigned int inuse_pages;//交换区内已用页槽数
        int next;			/* next entry on swap list *//*内核使用了一种不怎么常见的方法,将交换数组中的各个数组项按优先级连接起来。因为表示各个交换区的数据位于一个线性数组的各数组项中,所以在固定的数组项位置之外,使用了next成员来建立一个相对的顺序。next实际上是下一个交换区在全局变量swap_info[]数组中的索引。这使得内核能够根据优先级来跟踪各个数组项*/
    };
    
    //include/linux/swap.h
    enum {
        SWP_USED	= (1 << 0),	/*交换区正在使用*//* is slot in swap_info[] used? */
        SWP_WRITEOK	= (1 << 1),	/*交换区可写*//* ok to write to this swap?	*/
        SWP_ACTIVE	= (SWP_USED | SWP_WRITEOK),//交换区正在使用,可写
                        /* add others here before... */
        SWP_SCANNING	= (1 << 8),	/* refcount in scan_swap_map */
    };
    

    交换区状态中一些主要的数据可以借助proc文件系统快速查询:

    wolfgang@meitner> cat /proc/swaps 
    Filename Type Size Used Priority 
    /dev/hda5 partition 136512 96164 1 
    /mnt/swap1 file 65556 6432 0 
    /tmp/swap2 file 65556 6432 0
    

    在上例中,使用了一个专用分区和两个文件来容纳交换区。交换分区的优先级最高,因而在内核中将优先使用。两个文件的优先级都是0,在优先级为1的分区上没有空间可用时,将交替使用这两个文件。(仍然可能发生这样的情况:虽然交换分区没有完全满,但交换文件中也有数据,从上述proc文件系统的输出即可看到这一点,下文将具体说明)。

    lowest_bit和highest_bit用于加速搜索空槽位
    cluster_next和cluster_nr用于聚集技术

  2. 用于实现非连续交换区的存储块关系
    内核使用extent_listcurr_swap_extent成员来实现存储块(extent),用于创建假定连续的交换区槽位与交换文件的磁盘块之间的映射。如果使用分区作为交换区,这是不必要的,因为内核可以依赖于一个事实,即分区中的块在磁盘上是线性排列的。因而槽位与磁盘块之间的映射会非常简单。从第一个磁盘块开始,加上所需的页数乘以一个常量得到的偏移量,即可获得所需地址,如下图所示。这种情况下,只需要一个swap_extent实例。(实际上,该实例也可以省去,但其存在使得内核的工作更容易进行,因为它缩小了交换分区与交换文件之间的差别。)
    在这里插入图片描述

    在使用文件作为交换区时,情况会更复杂,因为无法保证文件的所有块在磁盘上是连续的。因而,在槽位与磁盘块之间的映射更为复杂。上图通过例子说明了这一点。

    文件由多个部分组成,这些部分可能位于块设备的任意位置。(磁盘碎片的程度越轻,文件分成的部分就越少。毕竟,如果文件的各部分数据尽可能接近,才是最好的,这在第9章讨论过。)extent_list链表的任务是,将文件散布在块设备上各处的块,与线性排布的槽位关联起来。这样做时,应该确保两个要点:使用的内存空间尽可能少,将消耗的搜索时间保持在最低限度。

    没有必要将每个槽位都关联到块号。将一个连续块组的第一个块与对应槽位关联并标明块组中的块数,就可以非常紧凑地将文件的结构刻画出来。

    利用上例来说明这一过程。如上图所示,前三个连续的块组分别包含3个、10个和7个块。在内核想要读取第6个槽位的数据时,会发生什么样的操作呢?这些数据并不在第一个块组中,因为第一组只包含槽位0到2。搜索将在第2组成功结束,其中包含槽位3到12,当然包含槽位5。内核因而必须确定第2个块组的起始块(使用存储块链表)。将该块的起始地址,加上页长度的两倍作为偏移量,即可获得该组中第3个块的地址(对应于第6个槽位)。

    //include/linux/swap.h
    //存储块描述符
    struct swap_extent {
        struct list_head list;//链表元素,链表头为 swap_info_struct->extent_list
        pgoff_t start_page;//存储块中第一个页槽的编号
        pgoff_t nr_pages;//存储块中可容纳页的数目
        sector_t start_block;//存储块的第一块在硬盘上的块号
    };
    

18.3.2 创建交换区 (用应用层软件,软件工作流程说明)

新交换分区不是由内核直接创建的。这项任务委托给一个用户空间工具(mkswap),其源代码位于util-linux-ng工具集合中。因为在使用交换区之前,创建交换区是一个强制性的步骤,下面简要分析一下该实用程序的运作模式。

内核无须提供任何新系统调用来支持创建交换区,毕竟,内核也没有提供任何系统调用来创建普通的文件系统,这些显然都不是内核的问题。用于直接与块设备(或者,就交换文件而言,是块设备上的一个文件)通信的现存系统调用,已经足以按照内核的需求来组织交换区的内容。

mkswap只需要一个参数,即分区的设备文件的名称,或交换区所在文件的名称(还可以指定其他参数,如交换区的长度、页长度。但在大多数情况下,这是无意义的,因为这些数据可以自动而可靠地计算出来。mkswap的作者不建议用户自行指定这些参数,如其源代码所示:if (block_count) {/* 这个傻乎乎的用户显式指定了块数 */...})。该实用程序将执行下列操作。

  1. 将所需交换区的长度除以所述机器的页长度,以确定其中能够容纳的页数。
  2. 逐一检查交换区的各个磁盘块是否有读写错误,以确定有缺陷的区域。因为交换区的页长度将使用机器的页长度,因而一个坏块就意味着交换区的容量减少了一页。
  3. 将一个包含所有坏块地址的列表,写入到交换区的第一页。
  4. 为向内核标识此类交换区(如果管理员指定了无效交换区,这完全可能是一个包含了文件系统数据的普通分区,决不能无意覆盖其中的数据),在第一页末尾设置了SWAPSPACE2标记(内核早期版本使用的是另一种不同格式的交换区,标记为SWAP-SPACE。这种格式有某些不利之处,特别是其最大长度限制为128 MiB或512 MiB(取决于CPU类型),内核现在已经不再支持该格式。)。
  5. 可用槽位数目也存储在交换区头部。该值是通过从总的可用槽位数目中减去坏块数目而得到的。还必须从中减去1,因为第一页用于存储状态信息和坏块列表。

尽管在创建交换区时坏块的处理看似非常重要,但该操作是可以跳过的。在这种情况下,mkswap并不检查数据区的错误,因而也不在坏块列表中写入任何数据。因为当今的硬件在块设备上已经基本不发生错误,通常不需要进行显式检查。

18.3.3 激活交换区 sys_swapon

为通知内核,已经用mkswap初始化了一个交换区,用于扩展物理内存,这需要与用户空间的交互。内核为此提供了swapon系统调用。照例,它实现在sys_swapon中,其代码位于mm/swapfile.c中。

尽管sys_swapon是内核中比较长的函数之一,但并不特别复杂。它执行以下操作。

  1. 第一步,内核在swap_info数组中查找一个空闲数组项,并向该项指定初始值。如果将一个块设备分区用作交换区,则用bd_claim获取相关的block_device实例。回想6.5.2节的内容,该函数为特定的持有者(这里是页交换子系统)获取块设备,并通知内核的其他部分,该设备已经关联到该持有者
  2. 在已经打开交换文件(或交换分区)之后,读入第一页包含的坏块信息和交换区的长度。
  3. setup_swap_extents初始化区间链表。下文将详细讲述该函数。
  4. 最后一步,根据新交换区的优先级,将其添加到交换区的列表。如前文所述,交换区列表是使用swap_info_struct的next成员定义的。还需要更新如下两个全局变量。

如果在调用该系统调用时没有为新交换区显式指定优先级,则内核将现存最低优先级减1作为该交换区的优先级。根据这种方案,除非管理员人工干预,否则新交换区将以递降的优先级加入。

  1. 读取交换区特征信息 swap_header
    交换区的特征信息保存在第一个槽位中。内核使用下列结构来解释该数据

    //include/linux/swap.h
    //交换分区中第一页的数据,用于管理整个交换分区
    union swap_header {
        struct {
            char reserved[PAGE_SIZE - 10];
            char magic[10];			/* SWAP-SPACE or SWAPSPACE2 */
        } magic;
        struct {
            char		bootbits[1024];	/* Space for disklabel etc. *//*前1 024字节是空闲的,为启动装载程序腾出空间,因为在某些体系结构上启动装载程序必须位于硬盘上指定的位置。这种做法,使得交换区可以位于磁盘的起始处,尽管在这样的体系结构上,启动装载程序代码也位于该处*/
            __u32		version;//交换区版本号
            __u32		last_page;//最后一页的编号
            __u32		nr_badpages;//坏页数
            unsigned char	sws_uuid[16];//sws_uuid 和 sws_volume 用于将一个标签和UUID与一个交换分区关联起来,内核并不使用这些字段,但有些用户层工具需要使用(手册页 blkid(8)提供了这些标识符背后原理相关的更多信息)。
            unsigned char	sws_volume[16];
            __u32		padding[117];//填充项,在交换区格式发生变化时用于表示附加信息
            __u32		badpages[1];//一共637个数字,用来指定有缺陷页槽的位置,坏块块号的列表,尽管在数据结构中坏块列表只有一项,但实际的数组项数目是 nr_badpages
        } info;
    };
    

    之所以使用两个数据结构来分析该信息,一方面是出于历史原因(新的信息只会出现在旧格式不使用的区域中,即分区起始处保留的1 024字节到swap_header尾部的特征信息之间的区域),另一方面在一定程度上也是因为内核必须处理不同的页长度,如果使用不同的结构来表示,处理会比较简单。由于信息位于第一个交换槽位的开始和结束之间,其中的空间必须填充一定数量的填充数据,至少从该数据结构的角度来看,应该如此处理。但如果从页长度(在所有体系结构上都通过PAGE_SIZE指定)减去交换区特征的长度(10个字符)来计算填充的空间长度,那么对页尾部的交换区特征信息的访问会更容易,可以直接得到交换区特征字符串所在的位置。在访问结构上半部的成员时,只需要指定上半部的定义。从该数据结构的角度来看,对接下来的数据是不感兴趣的,因为其中仅包含了坏块列表,该数组的地址很容易计算出来。

    在这里插入图片描述

  2. 创建存储块链表 setup_swap_extents
    setup_swap_extents用于创建区间链表。下图给出了相关的代码流程图
    在这里插入图片描述

    //mm/swapfile.c
    //创建交换区存储块链表
    static int setup_swap_extents(struct swap_info_struct *sis, sector_t *span)
    
    • setup_swap_extents 创建交换区存储块链表,使用块设备时

      • add_swap_extent 将整个块设备作为单个存储块创建
    • setup_swap_extents 创建交换区存储块链表,使用文件时

      • 逐个扫描文件的各个块
      • 为连续的块创建一个存储块加入交换区结构
      • 每个不连续的块创建一个存储块加入交换区结构

18.4 交换缓存

前面已经根据页交换子系统的数据结构描述了其布局,以下几节详细讲述内核将页从内存写入交换区及将页从交换区读回内存所采用的技术。

内核利用了另一个缓存,称为交换缓存(swap cache),该缓存在选择换出页的操作和实际执行页交换的机制之间,充当协调者。初看起来,这似乎有点古怪。使用一个额外的交换缓存干什么,需要缓存什么东西呢?下文将给出回答。

下图说明了交换缓存与页交换子系统其他组件的交互。

在这里插入图片描述

在页面选择策略和用于在内存和交换区之间传输数据的机制之间,交换缓存充当代理人的角色。这两个部分通过交换缓存交互。一方的输入会触发另一方的相应操作。不过请注意,对无须换出但可以同步的页来说,策略例程是可以直接与回写例程交互的。

哪些数据保存在交换缓存中?由于交换缓存只是使用第3章讨论的结构确立的另一个页缓存,因此答案很简单,就是内存页。系统中其他的页缓存,是出于性能考虑将页保持在物理内存中,而相关的数据总可以从块设备存储介质获取,但交换缓存并非如此(否则将与页交换的原理相悖)。相反,交换缓存用于以下目的,具体取决于页交换请求的“方向”(读入或写出):

  1. 在换出页时,页面选择逻辑首先选择一个适当的、很少使用的页帧。该页帧缓冲在页缓存中,然后将其转移到交换缓存。
  2. 如果换出页由几个进程在同时使用,内核必须设置进程页目录中的对应页表项,使之指向交换文件中相关的位置。在其中某个进程访问该页的数据时,该页将再次换入,该进程对应此页的页表项将设置为该页当前的内存地址。但是,这会导致一个问题。其他进程的对应页表项仍然指向交换文件中的位置,因为尽管可以确定共享一页的进程数目,却不可能确定具体是哪些进程在共享该页。

因而在换入共享页时,它们将停留在交换缓存中,直至所有进程都已经从交换区请求该页,并都知道了该页在内存中新的位置为止。这种情况如下图所示。

在这里插入图片描述

没有交换缓存的帮助,内核不能确定一个共享的内存页是否已经换入内存,将不可避免地导致对数据的冗余读取。

从读入/写出两个方向来看,交换缓存的重要性并不相同。在页换入时,交换缓存的重要性远远高于页换出时。这种不对称性出现在内核版本2.5开发期间,此间引入了第4章描述的逆向映射方案(rmap)。回想前文,可知rmap机制用于查找共享一页的所有进程(在更早的内核版本中,共享内存页只能使用交换缓存换出。在该页从一个进程的页表移除之后,内核必须一直等到所有其他共享该页的进程也从页表删除了该页,才能将页的数据从内存移除,这需要系统地扫描所有系统页表。与此同时,相关的页保存在交换缓存中。)

在换出共享页时,rmap查找引用该页数据的所有进程。因而,引用该页的所有进程中的相关页表项都可以更新,指向交换区中对应的位置。这意味着,该页的数据可以立即换出,而无须在交换缓存中保持很长一段时间。

18.4.1 标识换出页

在第4章讨论过,根据内存页的虚拟地址,需要使用一整套页表,才能找到相关页帧在物理内存中的地址。仅当数据实际存在于内存中时,该机制才是有效的。否则,没有对应的页表项。内核还必须能够正确标识换出页,换言之,必须能够根据给定的虚拟地址,找到内存页在交换区中的地址。

换出页在页表中通过一种专门的页表项来标记,其格式取决于所用的处理器体系结构。每个系统都使用了特定的编码,以满足特定的需求。

在换出页的页表项中,所有CPU都会存储下列信息

  1. 一个标志,表明页已经换出
  2. 该页所在交换区的编号
  3. 对应槽位的偏移量,用于在交换区中查找该页所在的槽位

内核定义了一种体系结构无关的格式,可用于在交换区中确定页所在的位置,该格式可以(通过特定于处理器的代码)从体系结构相关的数据得出。该方法的优点是显然的,它使得所有页交换算法的实现都与硬件无关,无须对每种处理器类型重写。与实际硬件的唯一接口,就是用于转换体系结构相关和无关两种数据表示的函数

在体系结构无关的表示中,内核必须存储交换分区的标识(也称为类型)和该交换区内部的偏移量,以便唯一确定一页。该信息保存在一个专门的数据类型中,称为swap_entry_t,定义如下:

//include/linux/swap.h
/* val由两部分组成,
 * 对64bit来说:0-59->交换区内部偏移量     59-64->交换区标识符
 * 对32bit来说:0-27->交换区内部偏移量     27-32->交换区标识符
 * 一个变量分为两部分的原因:该变量也作为一个搜索的键值,
 * 用于检索列出所有交换缓存页的基数树。由于交换缓存仅仅
 * 是一个使用 long 作为键值的页缓存,换出页可以用这种方法唯一标识.
 * 该结构体未来可能改变所以不直接使用long而是用结构体,且使用函数访问
 * 访问宏 SWP_TYPE_SHIFT(e) SWP_OFFSET_MASK(e)
 */
typedef struct {
	unsigned long val;
} swp_entry_t;

在这里插入图片描述

为什么形式上只使用一个unsigned long变量来存储两个信息项呢?首先,目前为止,内核到支持的所有系统对以这种方式提供的信息都还能凑合着用。其次,该成员变量中的值,也作为一个搜索的键值,用于检索列出所有交换缓存页的基数树。由于交换缓存仅仅是一个使用long作为键值的页缓存,换出页可以用这种方法唯一标识。

由于这种情形在未来可能发生改变,因而没有直接使用unsigned long值,而是将其隐藏到一个结构中。因为swap_entry_t值的内容只能通过专用函数访问,即使未来的内核版本修改页表项的内部表示,也无须重写页交换的实现。

为确保对swap_entry_t中两个信息项的访问,内核对上图中比特位的布局定义了两个常数:

//include/linux/swapops.h
//用于获取swp_entry_t中的两个信息
#define SWP_TYPE_SHIFT(e)	(sizeof(e.val) * 8 - MAX_SWAPFILES_SHIFT)
#define SWP_OFFSET_MASK(e)	((1UL << SWP_TYPE_SHIFT(e)) - 1)

//根据交换区类型(标识符)和交换区内部偏移产生一个 swp_entry_t
static inline swp_entry_t swp_entry(unsigned long type, pgoff_t offset)
//获取交换区标识符(long类型的低5位表示)
static inline unsigned swp_type(swp_entry_t entry)
//获取交换区内部偏移(long类型的高27(59)位表示)
static inline pgoff_t swp_offset(swp_entry_t entry)

//体系结构无关和相关的两种页表项表示之间进行切换
static inline swp_entry_t pte_to_swp_entry(pte_t pte)

pte_to_swp_entry 转换分两步进行。输入参数是一个页表项,如第4章所述,其类型为pte_t,该函数需要将pte_t实例转换为一个体系结构无关的swap_entry_t实例。

对于页表项来说,即使在特定于处理器的表示和体系结构无关的表示中使用了同样的数据类型,两种表示中划分比特位的方法也是不同的。

__pte_to_swp_entry是一个体系结构相关的函数,定义在特定于CPU的头文件<asm-arch/pgtable.h>中。该函数向内核提供了一个时机,可以将页表中特定于处理器的信息抽取出来。在许多体系结构上,这可以通过简单的类型转换完成,并不改变页表项的内容,只是改变一下类型。即使在比较反常的Sparc处理器上,也不需要什么专门的处理。

在第二步中,包含在新创建的swap_entry_t实例中的信息将转换为体系结构无关的格式,其中通常有若干比特位用于管理,例如,将该标识符标记为交换数据项,与普通的页表项不同。这里,内核仍然需要依赖处理器相关代码的帮助。所有系统都必须提供__swp_type和__swp_offset函数(请注意,在体系结构无关的版本中,没有开头的两个下划线)从处理器相关的格式中提取类型和偏移量,并按体系结构无关的通用格式返回,接下来相关的信息由swp_entry合并,以创建一个新的swap_entry_t。

在体系结构无关的格式中,用于标识交换区的比特位数目,通常比体系结构相关的格式所用的位数要多。因为体系结构不需要以常数方式定义交换区偏移量所用的比特位数,内核需要采用一点技巧,来查找可寻址的最大交换区偏移量。

maxpages = swp_offset(pte_to_swp_entry(swp_entry_to_pte(swp_entry(0,~0UL)))) -1;

swp_entry(0, ~0UL)指定了一个所有比特位均置位的交换区偏移量。上述转换首先将一个体系结构无关格式转换为一个体系结构相关格式,再将其转换回体系结构无关格式,确保仅在当前体系结构下有效的比特位会保存下来。而后,从结果取得交换区偏移量,即为最大可寻址的交换区偏移量。

18.4.2 交换缓存的结构实例

就数据结构而言,交换缓存无非是一个页缓存,如第16章所述。其实现的核心是swapper_space对象,该对象中聚集了与交换缓存相关的内部函数和数据结构。

//mm/swap_state.c
//交换区使用的交换缓存
struct address_space swapper_space = {
	.page_tree	= RADIX_TREE_INIT(GFP_ATOMIC|__GFP_NOWARN),
	.tree_lock	= __RW_LOCK_UNLOCKED(swapper_space.tree_lock),
	.a_ops		= &swap_aops,
	.i_mmap_nonlinear = LIST_HEAD_INIT(swapper_space.i_mmap_nonlinear),
	.backing_dev_info = &swap_backing_dev_info,
};

尽管每个系统都可能有几个交换区,但页交换子系统以外的内核代码只通过一个变量来访问交换缓存。在数据实际回写之前,页并不按交换区进行组织。从确定换出页那部分内核代码的角度来看,只需要向一个交换缓存发送适当的指令,该缓存由上文提到的swapper_space对象表示。

由于大多数字段都是链表,这些字段将所有适当的宏初始化为基本设置(空)。各数据项的语义已经在第4章讨论过。

内核提供了一组交换缓存访问函数,可以由任何涉及内存管理的内核代码使用。例如,这些函数可用于向交换缓存添加页,或查找交换缓存中的页。这些函数构成了交换缓存和页面替换逻辑之间的接口,因而可用于发出换入/换出页的命令,而无须关注此后数据如何传输的技术细节。

内核还提供了一组函数,来处理通过交换缓存提供的地址空间。与地址空间和页缓存类似,这些函数聚集在一个address_space_operations实例中,通过aops成员关联到swapper_space。这些函数构成了交换缓存“向下”的接口,换言之,在交换缓存下是在系统的交换区和物理内存之间传输数据的实现部分,这些函数是交换缓存与数据传输部分之间的接口。与稍早提到的函数集不同,这些例程并不关注换出/换入哪些页,而负责对选定页进行数据传输的技术细节。

//mm/swap_state.c
//交换区的交换缓存实现的函数接口
static const struct address_space_operations swap_aops = {
	.writepage	= swap_writepage,//将脏页与底层块设备同步,将页从交换缓存移除,将其数据传输到交换区
	.sync_page	= block_sync_page,//负责拔出对应的块设备队列。就交换缓存而言,这意味着接下来将执行发送给块层的所有数据传输请求
	.set_page_dirty	= __set_page_dirty_nobuffers,//将页标记为脏,它设置PG_dirty标志但并不创建缓冲区.页必须在交换缓存中标记为“脏”,而决不能分配新的内存,因为在使用换出机制时,内存资源肯定已经匮乏到一定程度了。在第16章讨论过,一种将页标记为脏的可能做法是创建缓冲区,使数据逐块回写。但这种做法需要额外的内存来保存buffer_head实例(其中包括所需的管理数据)。因为交换缓存中只回写整页,所以这是无意义的
	.migratepage	= migrate_page,
};

在讨论实际的操作中如何使用这些之前,会简要介绍届时将会遇到的函数,事实上有很多此类函数。下图给出了这些函数中最重要的一些,并描述了它们是如何关联的。
在这里插入图片描述

18.4.3 向交换缓存添加新页(换出页或换入页时都会添加)

向交换缓存添加新页是一个非常简单的操作,因为只需要使用适当的页缓存机制。标准方法就是调用第16章描述的add_to_page_cache函数,这减少了必要的工作量。该函数将一个给定页的数据结构插入到swapper_space地址空间中对应的链表和树中。

但这并非任务的全部内容。该页不仅需要添加到交换缓存,还需要在某个交换区中分配空间。尽管数据不会在此时复制到硬盘,但内核仍然必须考虑为该页选择的交换区和对应的槽位。该决策必须保存到交换缓存的数据结构中。

下面两个内核方法可以向交换缓存添加页,但作用不同:

(1). 在内核想要主动换出一页时,会调用add_to_swap,即当策略算法确定可用内存不足时。该例程不仅将页添加到交换缓存(在页数据写出到磁盘之前,会一直停留在其中),还在某个交换区中为该页分配一个槽位。
(2). 当从交换区读入由几个进程共享的一页(可以根据交换区中的使用计数器判定)时,该页将同时保持在交换区和交换缓存中,直至被再次换出,或被所有共享该页的进程换入。内核通过add_to_swap_cache函数实现该行为,该函数将一页添加到交换缓存,而不对交换区进行操作。

  1. 分配槽位 get_swap_page

    • get_swap_page 在交换区中找空页槽,失败返回0
      • scan_swap_map 扫描各交换区的槽位位图,利用聚集技术,找到交换区中可用页槽位置
      • swp_entry 找到空页后组成一项

    一个 聚集 由SWAPFILE_CLUSTER个连续项组成,内存页可以顺序写入这些项

    • scan_swap_map
      • 在交换区有空页的上下限中查找 可用聚集页
      • 如果找到了,则使用聚集中的页
      • 如果没找到可用聚集页,则重新从下限开始找,使用第一个空闲页
  2. 换出页,将页加入交换区缓存 add_to_swap
    在这里插入图片描述

    • add_to_swap 将页添加到交换缓存,在某个交换区中为该页分配一个槽位
      • get_swap_page 在交换区中找空页槽,失败返回0
      • __add_to_swap_cache 将页移动到交换缓存的基数树中,将对page实例设置PG_swapcache标志并将交换标识符swp_entry_t保存在page的private成员中
      • 设置页 更新标志和脏标志

    但实际上呢,工作就是这些。在换出页时,对策略例程没有其他的要求。剩余的工作,特别是将数据从内存传输到交换区的任务,是由与swapper_space关联的特定于地址空间的操作完成的。这些相关例程的实现将在下文讨论。就策略例程来说,只需要知道:内核会负责将页数据实际写出到交换区,因而在调用add_to_swap之后,就释放了一页。更多的细节,将在下文对shrink_page_list函数的讨论中阐明

  3. 换入页,将页加入交换区缓存 add_to_swap_cache
    与add_to_swap不同,add_to_swap_cache将一页添加到交换缓存,但要求已经为该页分配了一个槽位。

    如果一页已经有了对应的槽位,为什么将其添加到交换缓存呢?在换入页时,这是需要的。假定已经换出了由许多进程共享的一页。在该页再次换入时,在第一个进程将其换入后,必须将其数据保持在交换缓存中,直至所有进程都完成了对该页的换入。只有到这时,才能将该页从交换缓存移除,因为此时所有相关用户进程都已经得知该页在内存中新的位置。当对交换页进行预读时,也会以这种方式来使用交换缓存。在这种情况下,读入的页尚未因缺页异常而被请求,但很可能在稍后被请求。
    在这里插入图片描述

    • add_to_swap_cache 换入页,将页加入交换区缓存
      • swap_duplicate 确保该页已经有了一个对应的交换数据项,还会将对应槽位的交换映射计数加1,这表明该页在多处被换出
      • __add_to_swap_cache 将页移动到交换缓存的基数树中,将对page实例设置PG_swapcache标志并将交换标识符swp_entry_t保存在page的private成员中

    add_to_swap和add_to_swap_cache的主要区别在于,后者没有设置PG_uptodate或PG_dirty这两个标志。本质上,这意味着内核并不需要将该页写入到交换区,即二者的内容当前是同步的(请注意,在内核版本2.6.25中,将会对这里提到的函数名做一些调整。add_to_swap_cache将合并到其唯一的调用者read_swap_cache_async中,不会继续存在。而__add_to_swap_cache将替换前者,重命名为add_to_swap_cache。这两个函数的调用者也会相应进行更新。)

18.4.4 搜索交换缓存中的页 lookup_swap_cache

  • lookup_swap_cache 搜索交换缓存中的页
    • find_get_page 从交换缓存基数树中查找页
      • radix_tree_lookup

18.5 交换区数据回写 swap_writepage

页交换实现的另一部分是其“向下”的接口,用于将页数据写入到交换区中选定的位置(或确切地说,向块层发出适当的请求)。这是在交换缓存中使用地址空间操作writepage完成的,该函数指针指向swap_writepage。下图给出了swap_writepage函数的代码流程图,该函数定义在mm/page_io.c中。

在这里插入图片描述

因为大部分工作已经由上文描述的机制完成,所以swap_writepage需要做的工作很少。

  • swap_writepage 将页数据写入到交换区中选定的位置
    • remove_exclusive_swap_page 检查相关页是否只由交换缓存使用,而内核其他部分都已经不再使用。如果是这样,该页不再需要,可以从内存移除
    • get_swap_bio 创建bio请求
      • map_swap_page 搜索交换区存储块链表找到扇区号
    • set_page_writeback 对页设置回写标志
    • submit_bio 将写请求发送给块层,可能直接读取

请注意,将页的内容写入到交换区中对应的槽位后,换出页的工作还没有完全结束。这时还需要更新页表,然后才能认为该页已经完全从物理内存移除。一方面,页表项需要指定该页不在内存中,另一方面,页表项还需要指向对应槽位在交换区中的位置。因为这项修改必须对所有使用该页的进程进行,所以这是一项牵涉颇多的任务,将在18.6.7节讨论。

18.6 页面回收

到现在为止,已经解释了回写的技术细节,下面把注意力转向页交换子系统的第二个主要的方面,即交换策略,该策略用于确定哪些页可以从物理内存换出而同时又不会严重降低内核性能。因为该策略可以释放页帧,使得有新的内存可用于紧急需求,所以该技术也称为页面回收(page reclaim)。

与前几节关注交换地址空间中的页相比,本节关注的是任何地址空间中的页。交换策略的原理可以适用于所有没有后备存储器的页,无论其数据读取自文件,还是动态生成的。唯一的差别在于,内核决定将相关的页从内存移除时,页数据所写入的位置。而这个问题对页是否换出没有影响。有些页有持久后备存储器,页数据可以写出到后备存储器,而其他页则必须放入到交换区中(18.1.1节有对可换出页的更精细的描述)。

交换策略算法的实现是内核中比较复杂的内容。这不仅是因为要使交换速度最大化,而更主要的是各种必须解决的特殊情况。下面的例子专注于构成页交换子系统主要工作的那些最常见的情况。为简明起见,这里不会讨论比较罕见的情况,这些可能是因SMP系统上各处理器间交互而导致的,或是单处理器系统上出现的随机巧合情况。与各交换操作的细枝末节相比,对页交换所涉及的各个组件间的交互给出一般性的概述要更重要。

18.6.1 概述

以下各节将主要讲述各个交换策略函数和过程的交互,并详细描述其实现。图18-11给出了一个代码流程图,列出了最重要的各个方法并说明了它们是如何彼此关联的。

图18-11是图18-7中概述的进一步细化。页面回收在两个地方触发,如图18-11所示。

在这里插入图片描述

  1. 如果内核检测到在某个操作期间内存严重不足,将调用try_to_free_pages。该函数检查当前内存域中所有页,并释放最不常用的那些。
  2. 后台守护进程 kswapd,会定期检查内存使用情况,并检测即将发生的内存不足。可使用该守护进程换出页,作为预防措施,以防内核在执行其他操作期间发现内存不足。

在NUMA机器上,所有处理器对内存的共享并不是一致的(参见第3章),对每个NUMA结点来说,都有一个单独的kswapd守护进程。每个守护进程负责一个NUMA结点上所有的内存域。

在非NUMA系统上,只有一个kswapd实例,负责系统中所有的内存域。例如,IA-32可以有最多3个内存域:ISA-DMA内存域、普通内存域、高端内存域。

上述两条代码路径,很快在shrink_zone函数中合并。对页面回收子系统的这两条代码路径来说,剩余的代码是相同的。

在try_to_free_pages中处理系统内存严重不足时,以及在kswap守护进程中定期检查内存使用时,在使用为此设计的算法确定为向系统提供新的空闲内存所需换出的页数以后,内核还需要确定具体换出哪些页(并最终将相关信息从策略代码部分传递到负责将页写回到后备存储器的例程,以及负责修改页表项的例程)。

回想3.2.1节,内核将页分类到两个LRU链表中:一个用于活动页,另一个用于不活动页。这些链表是按内存域管理的(即每个内存域两个链表):

//include/linux/mmzone.h
//内存域数据结构
struct zone {
    ...
	/* 活动页的集合( page 实例) */
	struct list_head	active_list;
	/* 不活动页的集合( page 实例) */
	struct list_head	inactive_list;
    ...
};

判断给定页属于哪一类是内核的一项必要工作,本章的很大一部分内容都在回答该问题。有关回收页的数目、具体回收哪些页的决策,是按照下列步骤作出的。

  1. shrink_zone是从内存移除很少使用的页的入口点,在周期性的kswapd机制中调用。该方法负责两件事:通过在活动链表和惰性链表之间移动页(使用shrink_active_list),试图在一个内存域中维护活动页和不活动页的数目的均衡;还通过shrink_cache,控制了选择换出页的过程。在确定内存域中换出页数的逻辑和具体换出哪些页的决策之间,shrink_zone充当了一个中间人。
  2. shrink_active_list内核使用该函数在活动页和不活动页的两个链表之间移动页。该函数会被告知需要在两个链表之间转移的页数,而后该函数试图选择使用最少的页。
    因而在本质上,shrink_active_list负责决定随后将换出哪些页,保留哪些页。换言之,该函数实现了页面选择的策略部分。
  3. shrink_inactive_list从给定内存域的惰性链表移除选定数目的不活动页,将其传送到shrink_page_list函数,后者将向各个对应的后备存储器发出回写数据的请求,以便在物理内存中释放空间,回收所选定的页。

如果由于任何原因,不能回写页(有些程序可能明确地阻止回写),shrink_inactive_list必须将不能回写的页放回活动链表或惰性链表。

18.6.2 数据结构

本节先讨论内核使用的几个数据结构。其中页向量借助于一个数组来保存特定数目的页,可以对这些页执行同样的操作。最好以“批处理模式”执行,这比分别对每个页执行同样的操作要快得多。然后,本节讲述用于将页置于内存域的活动链表或惰性链表上的LRU缓存机制。

  1. 页向量 pagevec
    内核定义了以下结构,用于将几个页群集到一个小的数组中,对一组页进行操作比对单个页操作快:

    //include/linux/pagevec.h
    //页向量结构体
    struct pagevec {
        unsigned long nr;//pages数组中的数目
        unsigned long cold;//区分热页(hot page)和冷页(cold page)。如果页的数据保存在某个CPU的高速缓存中,称为热页
        struct page *pages[PAGEVEC_SIZE];
    };
    

    操作函数:

    1. pagevec_release将向量中所有页的使用计数器减1。如果某些页的使用计数器归0,即不再使用,则自动返回到伙伴系统。如果页在系统的某个LRU链表上,则从该链表移除,无论其使用计数器为何值。
    2. pagevec_free将一组页占用的内存空间返还给伙伴系统。调用者负责确认页的使用计数器为0(表明页在其他地方没有使用),且未包含在任何LRU链表中。
    3. pagevec_release_nonlru是另一个用于释放页的函数,它将一个给定页向量中所有页的使用计数器减1。在计数器归0时,对应页占用的内存将返还给伙伴系统。与pagevec_release不同,该函数假定向量中所有的页都不在任何LRU链表上

    所有这些函数都需要一个pagevec结构作为参数,其中包含了需要处理的页。如果向量为空,则所有这些函数都会立即返回到调用者。

    这些函数还有另一种带有两个下划线的版本(例如__pagevec_release)。这些函数并不测试向量是否包含页。

    向页向量添加页的函数:

    //include/linux/pagevec.h
    static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page)
    

    pagevec_add 将一个新页添加到一个给出的页向量pvec。

  2. LRU缓存
    内核提供了另一个缓存,称为LRU缓存,以加速向系统的LRU链表添加页的操作。它利用页向量来收集page实例,将其逐组置于系统的活动链表或惰性链表上。这两个链表在内核中是一个热点,但必须通过自旋锁保护。为降低锁竞争的几率,新页不会立即添加到链表,而是首先缓冲到一个各CPU列表上:

    //mm/swap.c
    //LRU缓存,为降低锁竞争的几率,新页不会立即添加到活动或惰性链表,而是首先缓存到这个列表上
    static DEFINE_PER_CPU(struct pagevec, lru_add_pvecs) = { 0, };
    

    通过该缓冲区添加新页的函数是lru_cache_add。它提供了一种方法,可以延迟将页添加到系统的LRU链表,直至已经积累了PAGEVEC_SIZE个页:

    • lru_cache_add 向cpu lru页缓存(lru_add_pvecs)添加页

      • pagevec_add 将页添加到lru页缓存的页向量数组中
      • 如果lru页缓存满了则调用 __pagevec_lru_add 将缓存中的所有页加入内存域惰性链表中
    • add_to_page_cache_lru 将页插入到页缓存和LRU链表中

      • add_to_page_cache 将新页添加到页缓存
      • lru_cache_add 将该页添加到系统的LRU缓存

    lru_cache_add只在mm/filemap.c中的add_to_page_cache_lru中需要,用于将一页同时添加到页缓存和LRU缓存。但这是将一页同时添加到页缓存和LRU链表的标准函数。最重要的是,mpage_readpagesdo_generic_mapping_read会使用该函数,在从文件或映射读取数据时,块层结束于这两个标准函数。

    通常都认为内存页最初是不活动的,在确定其价值之后,才能认为是活动的。但有些例程对其使用的页有高度评价,会调用lru_cache_add_active直接将页放置到内存页的活动链表上(NUMA系统的页面迁移代码,也使用了该函数),如下:

    1. mm/swap_state.c中的read_swap_cache_async,该函数从交换缓存读取页
    2. 缺页异常处理程序__do_faultdo_anonymous_pagedo_wp_pagedo_no_page,这些实现在mm/memory.c

    不活动页如何提升为活动页是下一节的主题。这与将页从活动链表移动到惰性链表的操作直接相关,反之亦然。在可以执行这些操作之前,内核必须将所有页从各CPU LRU缓存传送到全局的链表;否则,移动页的逻辑可能漏掉一些页。内核提供的辅助函数lru_add_drain即用于此目的。

    图18-12综述了页在不同链表之间的移动:
    在这里插入图片描述

18.6.3 确定页的活动程度

为评估一页的重要性,内核不仅要跟踪该页是否由一个或多个进程使用,还需要跟踪其被访问的频繁程度。因为只有很少的体系结构对内存页支持直接的访问计数器,内核必须借助其他手段,因而引入了两个页标志,称为referenced和active。对应的标志位值分别是PG_referenced和PG_active,用于设置和获取状态的宏已经在3.2.2节讨论过。回想前文,例如PageReferenced检查PG_referenced标志位,而SetPageActive会设置PG_active标志位。

为什么对页状态使用这两个标志?假定只使用一个标志来确定页的活动程度,PG_active也会工作得相当好。在该页访问时,会设置相应标志,但何时将标志清除呢?如果内核不自动清除该标志,该页将一直处于活动状态,即使使用很少或根本不再使用,也是如此。为在一定的超时时间之后自动清除该标志,将需要大量的内核定时器,因为并非Linux支持的所有CPU都对此提供了适当的硬件支持。考虑到通常系统上可能存在的大量内存页,这种(基于定时器的)方法注定是要失败的。

使用两个标志,可以实现一种更精巧的方法,来判断页的活动程度。核心思想是,一个标志表示当前活动程度,而另一个标志表示页是否在最近被引用过。这两个标志位的设置需要密切协调。图18-13说明了相应的算法。基本上需要执行以下步骤。

在这里插入图片描述

  1. 如果页被认为是活动的,则设置PG_active标志;否则不设置。该标志是否设置,直接对应于页所在的LRU链表,即(特定于内存域的)活动链表或惰性链表。
  2. 每次访问该页时,都设置PG_referenced标志。负责该工作的是mark_page_accessed函数,内核必须确保适当地调用该函数。
  3. PG_referenced标志以及由逆向映射提供的信息用于确定页的活动程度。关键在于,每次清除PG_referenced标志时,都会检测页的活动程度。page_referenced函数实现了该行为。
  4. 再次进入mark_page_accessed。在它检查内存页时,如果发现PG_referenced标志位已经设置,这意味着page_referenced没有执行检查。因而对mark_page_accessed的调用必定比page_referenced更频繁,这意味着该页经常被访问。如果该页当前处于惰性链表上,则将其移动到活动链表。此外,还会设置PG_active标志位,清除PG_referenced标志位。
  5. 反向的转移也是可能的。如果页位于活动链表上,受到很多关注,那么通常会设置PG_referenced标志位。在页的活动减少时,如果要将其转入惰性链表,则需要两次page_referenced调用,而其中不能插入mark_page_accessed调用。

如果对内存页的访问是稳定的,那么对mark_page_accessed和page_referenced的调用在本质上将是均衡的,因而该页将保持在当前的LRU链表上

如果一个不经常访问的页(因而是不活动的)的PG_active和PG_referenced标志位均未设置。这意味着,接下来需要两次mark_page_accessed调用(其中不能夹杂page_referenced调用),才能将其从惰性链表移动到活动链表。反之亦然:一个高度活动的页,同时设置了PG_active和PG_referenced标志位,也需要两次page_referenced调用(其间不能插入mark_page_accessed调用)才能从活动链表移动到惰性链表。

总而言之,该解决方案确保了内存页不会在活动链表和惰性链表之间过快地跳跃,如果出现过快的跳跃,显然不利于对页的活动程度作出一个可靠的判断。该方法是本章开头讨论的“第二次机会”(second chance)方法的一种变体:高度活动的页在转换为不活动页之前,会获得第二次机会,而高度不活动的页在转换为活动页之前,也需要二次证明。这与“最近最少使用”(least recently used,LRU)方法(或至少是LRU的近似,因为内存页没有精确的使用计数)结合起来,来实现页面回收策略

注意,虽然图18-13说明了最重要的状态和链表迁移,但还有一些其他的可能性。一方面,这是由本书所未涵盖的代码(例如,页面迁移代码)导致的。另一方面,为处理一些特例,例如,集中式页面回收(lumpy page reclaim)技术,需要做出一些改动。这些例外情况将在本章讨论。

内核提供了几个辅助函数,支持在两个LRU链表之间移动页:

//include/linux/mm_inline.h
static inline void
add_page_to_active_list(struct zone *zone, struct page *page)
static inline void
add_page_to_inactive_list(struct zone *zone, struct page *page)
static inline void
del_page_from_active_list(struct zone *zone, struct page *page)
static inline void
del_page_from_inactive_list(struct zone *zone, struct page *page)
static inline void
del_page_from_lru(struct zone *zone, struct page *page)

如果调用者不知道页当前所在的LRU链表,则必须使用del_page_from_lru

将页从活动链表移动到惰性链表,不仅仅需要处理链表项。将不活动页提升到活动链表,activate_page就足够了。还要去掉锁定和统计量的处理,代码如下所示:

  • activate_page 将不活动页从不活动链表移动到活动链表
    • del_page_from_inactive_list 从惰性链表删除页
    • SetPageActive 设置页活动状态
    • add_page_to_active_list 将页加入内存域活动链表

将页从活动链表移动到惰性链表的处理隐藏在一个更大的函数内部,即shrink_active_list,该函数所处的上下文更为广泛,它还负责缓存的收缩,在18.6.6节讨论。在内部,该函数依赖于page_referenced,计算页最近的引用次数。除了按上述方法处理PG_referenced标志位置位,该函数还负责查询从页表引用该页的频繁程度。这主要应用了逆向映射机制。page_referenced需要参数is_locked,该参数表明所述页是否已经由调用者锁定。

mark_page_accessed函数用于提高页的活动程度(活动程度向上提一级),实现了如图18-13所示的状态迁移。表18-1综述了这些状态迁移。

在这里插入图片描述

18.6.4 收缩内存域 shrink_zone

内核的其他部分需要向负责收缩内存域的例程提供下列信息。

  1. NUMA结点和其中包含的将要处理的内存域。
  2. 需要换出的页数。
  3. 在放弃操作之前,可能检查(检查是否适合换出)的最大页数。
  4. 对释放页的请求所指定的优先级。这不是传统的UNIX意义上的进程优先级,且进程优先级在核心态也没什么意义,这里所谓的优先级只是一个整数,指定了内核需要新内存的急切程度。例如,当在后台换出页以预防内存不足时,需求的急切程度就不如内核直接检测到严重的内存不足时,在后一种情况下,内核急需新的内存来执行或完成操作。

页面选择开始于shrink_zone。但在讨论其代码之前,还需要介绍一些基础设施。

  1. 扫描控制结构体 scan_control
    下面的数据结构保存了用于控制扫描操作的参数。请注意,该结构不仅用于从高层函数向低层函数传递指令,而且也用于反向传递结果。可以通知调用者操作是否成功:

    //页扫描操作控制结构体
    struct scan_control {
        /* Incremented by the number of inactive pages that were scanned */
        unsigned long nr_scanned;//已经扫描的不活动页数
    
        /* This context's GFP mask */
        gfp_t gfp_mask;//指定了在调用页面回收函数的上下文环境下有效的页面分配标志。这很重要,因为有时候在页面回收期间必须分配新的内存。如果发起页面回收的上下文环境不允许睡眠,该约束当然必须转给所有调用的函数;这也恰好是设计使用gfp_mask的目的
    
        int may_writepage;//能否进行回写操作,内核运行于膝上模式时,有时候需要禁用写出操作
    
        /* Can pages be swapped as part of reclaim? */
        int may_swap;//确定了页面回收处理过程中是否允许页交换.只有在两种情况下会禁用页交换:软件挂起(software suspend)机制(大体上相当于Windows的休眠,将内存写到交换分区)在执行页面回收,或NUMA内存域显式禁用了页交换。
    
        int swap_cluster_max;//阈值,表示一次页面回收步骤中,在各CPU列表中扫描的内存页数目的最小值。通常设置为SWAP_CLUSTER_MAX,该宏默认定为32.该值为交换区一个聚集中页的总数
    
        int swappiness;//控制内核换出页的积极程度,该值的范围在0到100之间。默认情况下,将使用vm_swappiness。后者的标准设置为60,但可以通过/proc/sys/vm/swappiness调整
    
        int all_unreclaimable;//用于报告,所有内存域中的内存当前都是完全不可回收的,如,在所有页都被mlock系统调用钉住时,就可能发生这种情况
    
        int order;//内存回收的分配阶,即要回收2^order个连续页
    };
    
    //include/linux/mmzone.h
    struct zone {
        ...
        /* 指定在回收内存时需要扫描的活动页的数目 */
        unsigned long		nr_scan_active;
        /* 指定在回收内存时需要扫描的不活动页的数目 */
        unsigned long		nr_scan_inactive;
        /* 前一遍回收时扫描的页数 */
        unsigned long		pages_scanned;	   /* since last reclaim */
        ...
        /* Zone statistics */
        /* 维护了大量有关该内存域的统计信息 .如当前活动和不活动页的数目
        * 函数 zone_page_state 用来读取 vm_stat 中的信息
        * */
        atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
        ...    
    };
    

    内核需要扫描活动列表和惰性列表来查找可以在二者之间移动的页,或从惰性列表回收的页。但完整的链表不可能一遍扫描完成,每次只能扫描活动链表上的nr_scan_active个和惰性链表上的nr_scan_inactive个链表元素。由于内核使用了LRU方案,这个数目是从链表尾部开始计算的。pages_scanned记录的是前一遍回收时扫描的页数,而vm_stat提供了关于当前内存域的统计信息,例如当前活动和不活动页的数目。回想前文,可知用于统计的成员vm_stat可以用辅助函数zone_page_state访问。

  2. 收缩内存域实现 shrink_zone
    在介绍了所需的辅助数据结构之后,我们来讨论如何发起缩减内存域的操作。shrink_zone需要一个scan_control实例作为参数。该实例必须由调用者填充适当的值。最初,该函数需要确定要扫描的活动和不活动页的数目,这可以根据所处理内存域的当前状态以及传递进来的scan_control实例确定:

    • shrink_zone 缩小内存域,当扫描的页数大于聚集才回收
      • 计算要扫描的活动和不活动页数,当扫描的活动或不活动页数大于一个交换区聚集能容纳的页数才进行扫描操作
      • 活动页扫描调用 shrink_active_list 缩小内存域活动链表
      • 不活动页扫描调用 shrink_inactive_list 从内存域惰性链表回收所需数目的页。返回值是实际上成功回收的页数

    在shrink_active_list和shrink_inactive_list中缩减LRU链表时,需要一种方法从链表中选择页,因而在讨论这两个函数之前,必须先引入一个执行选择页工作的辅助函数。

18.6.5 隔离LRU页和集中回收 isolate_lru_pages

内存域中,保存在链表上的活动和不活动页都需要由一个自旋锁保护,确切地说是zone->lru_lock。为简化讨论,我们一直忽略了这个锁,因为就我们的目的而言,它不是本质性的。不过现在需要考虑它。在操作LRU链表时,需要锁定链表,这会出现一个问题:在内核中,对很多工作负荷而言,页面回收代码都属于最热、最重要的代码路径之一,因而锁竞争的几率相当高。因此,内核需要尽可能在锁的外部工作。

一种优化是这样:将shrink_active_listshrink_inactive_list中将要分析的所有页都放置到一个局部链表上,放弃对全局LRU链表的锁,然后继续在局部链表上处理这些页。因为这些页不再出现于任何全局的内存域相关链表上,所以除了当前执行函数之外,内核的任何其他部分都不能访问它们,这些页不会受到对内存域LRU链表后续操作的影响。因而在处理局部链表时,也不需要获取内存域中的自旋锁了。

isolate_lru_pages函数负责从活动链表或惰性链表选择给定数目的页。这并不很困难:从链表末尾开始(这一点很重要,因为LRU算法中必须先扫描最陈旧的页),通过循环遍历链表,每步获取一页,将其移动到局部链表,直至所需页数达到为止。需要为每一页清除PG_lru标志位,因为该页现在已经不在LRU链表上了。(此外,该函数还需要获得该页的一个引用,并确保该页此前的引用计数为0。通常,引用计数为零的页位于伙伴系统中,这在3.5节讨论过。 但是,并发性允许引用计数为0的页在LRU链表上存在比较短的一段时间。)

到目前为止,我们讨论的是最简单的情形。但实际的情况会稍微复杂一些,因为isolate_lru_pages也实现了所谓的集中回收算法。集中回收的目的是什么呢?高阶分配需要一段由多个页组成的连续物理内存,这种分配请求很难满足,请求的页数越多,面临的问题就越困难。系统运行一段时间以后,物理内存会变得越来越支离破碎。这个问题如何解决呢?图18-14说明了内核采取的方法(虽然集中回收不是计算机科学乐于讲授的内容,但它在实际工作中表现良好,特别是非常简单,与在纸上的良好效果相比,有时候对内核来说要重要得多。)

在这里插入图片描述

假定内核需要连续的4个页帧。遗憾的是,可分配的页帧所属的页当前在LRU链表上,这些页散布在内存中,最大的连续区域是两页。为避免这种情况,集中回收只是从LRU链表上某个页(称为标记页,tag page)对应的页帧前后来获取页帧。不仅标记页本身,还有与该页相邻的页帧,都会被选中进行回收。这样,就可以试图释放4个连续的页帧了。但这并不能保证分配一个有4个空闲页的内存块,因为选中的页帧很可能是无法回收的。但我们确实进行了尝试,而且在使用集中回收的情况下,与不使用该技术对比,回收到连续的高阶内存区域的几率大大增加。

  • isolate_lru_pages 从内存域的活动链表选择给定数目的页复制到一个局部的临时链表
    • 遍历活动链表,将页移动到目标链表中,如果指定了分配阶,则还会尝试将与分配阶页数相等的相邻页(且是在lru缓存中的页)移动到目标链表中

18.6.6 收缩活动页链表 shrink_active_list

将页从活动链表移动到惰性链表是页面回收的策略算法实现中的关键操作之一,因为此时需要评估系统中(或更确切地说,是在所述内存域中)各个页的重要性。因此,不出意料,shrink_active_list是内核中较长的函数之一。它执行的主要步骤如下。

  1. 使用isolate_lru_pages将所需数目(由nr_pages定义)的页从活动链表复制到一个局部的临时链表。
  2. 根据这些页的活动程度,将其分配到活动链表和惰性链表。
  3. 集中释放不重要的页

图18-15给出了shrink_active_list的第一个步骤的代码流程图

在这里插入图片描述

  • shrink_active_list 缩小内存域活动链表
    • 计算参数,定义页面回收算法的积极程度和行为
    • lru_add_drain 将当前保存在LRU缓存中的页释放到系统的per_cpu_pageset链表,链表满时会释放到伙伴系统
    • isolate_lru_pages 从内存域的活动链表选择给定数目的页复制到一个局部的临时链表
    • 遍历局部链表,根据链表中页的活动程度,将页分别放入临时活动链表和临时不活动链表
    • 遍历临时不活动链表,将页加入内存域的不活动链表
    • 遍历临时活动链表,将页加入内存域的活动链表

计算了4个参数(这里的各个公式是通过启发式方法得出的,目的是保证在不同情况下系统能具备良好的性能)

  1. distress是关键的标志,表示内核需要新内存的急切程度。该值是将固定值100右移 prev_priority位计算而来(实际上右移的位数是zone->prev_priority和priority中的较小者)。prev_priority指定了上一次try_to_free_pages运行期间扫描内存域的优先级。请注意,prev_priority的值越低,相应的优先级越高。移位操作生产以下distress值,对应的优先级如下所示。
    在这里插入图片描述

    所有大于7的优先级数值都对应distress 0。distress为0表示内核基本上不需要新内存,而100表示内核有很大的麻烦,急需新内存。

  2. mapped_ratio表示总的可用内存中已映射内存页(不仅用于缓存数据,而且由进程明确地请求用于存储数据)的比例。该比例是通过将当前映射页数目除以系统启动时可用内存页的总数计算出来的。然后将结果乘以100,放大为百分比值。

  3. mapped_ratio只用于计算另一个值,称作swap_tendency。顾名思义,它表示系统的页交换趋势。到此,读者已经熟悉了前两个变量的计算。sc_swappiness是另一个内核参数,通常基于/proc/sys/vm/swappiness中的设置。

  4. 如果活动链表和惰性链表的长度之间存在较大的不平衡,内核将允许更容易地进行页交换和页面回收,以便平衡二者的长度。但内核也做了一些工作,以便在swappiness值较低时,避免两个链表的长度差距造成太大的影响。

  5. 内核现在将所有计算出的信息归结为一个布尔值,来回答下述问题:是否需要换出映射页?如果swap_tendency大于或等于100,将会换出映射页,而reclaim_mapped设置为1。否则该变量保持其默认值0,因而只从页缓存回收页
    因为会将vm_swappiness加到swap_tendency,管理员可以在任何时间启用映射页的换出,只需要将vm_swappiness指定为100,就无须考虑其他系统参数的设置。

在参数计算之后将调用lru_add_drain过程,该函数将当前保存在LRU缓存中的数据分配到系统的LRU链表。与18.6.2节我们接触到的lru_cache_add相反,lru_add_drain在LRU缓存至少包含一个元素即执行复制操作,不会等到LRU缓存满。

最终,shrink_active_list的任务是将内存域的活动链表中特定数目的页,转移到惰性链表或移回活动链表。其中创建了3个局部链表,用于缓冲page实例,以便进行扫描。

  1. l_active和l_inactive分别保存在函数结束时将放回内存域的活动链表或惰性链表的页。
  2. l_hold保存仍然有待扫描的页,这些页在扫描之后才能确定其归宿。

18.6.7 回收不活动页 shrink_inactive_list

到目前为止,内存域中的页已经在LRU链表上进行重新分配,以找到适合回收的候选页。但其内存空间尚未释放。释放内存的最终步骤由shrink_inactive_list和shrink_page_list函数执行,二者彼此协作来执行该任务。shrink_inactive_lists将zone->inactive_list中的页群集为块,这有利于交换聚集,而shrink_page_list将结果链表上的成员向下传递并将页发送给相关的后备存储器(这意味着页被同步、换出或丢弃)。但这个看起来很简单的任务会导致几个问题,读者在下面会看到。

除了页的链表以及通常的收缩控制参数,shrink_page_list还需要另一个参数,以控制两种运作模式的选择:PAGEOUT_IO_ASYNC指定异步写出,而PAGEOUT_IO_SYNC指定同步写出。在第一种情况下,写请求传递给块层后不需要进一步的工作,在第二种情况下,内核发出写请求之后需要等待写操作完成

  1. 收缩惰性链表 shrink_inactive_list
    因为shrink_inactive_list只负责从zone->inactive_list逐块移除页,其实现不是特别复杂,如图18-16的代码流程图所示。
    在这里插入图片描述

    • shrink_inactive_list 缩小内存域不活动链表
      • lru_add_drain 将当前保存在LRU缓存中的页释放到系统的per_cpu_pageset链表,链表满时会释放到伙伴系统
      • while: 直至扫描页的数目达到了最大允许值,或已经回写了所需数目的页
      • isolate_lru_pages 从内存域的不活动链表选择给定数目的页(从链表尾部开始选,尾部为最不活动的页)复制到一个局部的临时链表
      • shrink_page_list 该函数将发起对链表中的页的异步回写操作
      • 如果有些页无法回写,回收的页要用于高阶分配
        • congestion_wait 等待块设备上的拥塞解除
        • shrink_page_list 对页同步回写
      • 将回收失败的页放回内存域的活动或不活动链表
  2. 执行页面回收 shrink_page_list
    shrink_page_list从参数取得一组选中回收的页(一个链表),试图将各页写回到对应的后备存储器。这是策略算法执行的最后一个步骤,所有其他的一切都是页交换的机制部分的职责。shrink_page_list函数形成了内核的两个子系统之间的接口。相关的代码流程图在图18-17给出。该函数需要处理许多边界情形,图18-17中忽略了其中的一些,以避免无关紧要的的细节妨碍考察本质性的操作原则。
    在这里插入图片描述

    • shrink_page_list 页面回收
      • while 遍历要回收的页链表
      • 如果页被其他部分锁定,则不回收,否则锁定该页
      • 如果是没有交换页槽的匿名页,则调用 add_to_swap 分配页槽,并加入交换缓存中
      • 如果页被映射了,则调用 try_to_unmap 尝试解除映射,并将回写的位置信息保存到页表项中
      • 如果页是脏的,调用 pageout 将页回写
      • 如果页有私有数据,即有块设备的页缓存(对包含了文件系统元数据的页来说,通常是这样),则调用 try_to_release_page 释放页
      • remove_mapping 将页与其地址空间分离(将页从缓存中删除)
      • pagevec_add 将页放入页向量
      • __pagevec_release_nonlru 释放页向量中的所有页,放会伙伴系统
        在这里插入图片描述

18.7 交换令牌

避免页颠簸的一种方法是交换令牌,在18.1.2节简要地讨论过。该方法简单但有效。在多个进程并发进行页交换时,很可能发生这样的情况:大多数时间都花费在将页写出到磁盘和再次读入内存,读入之后很短一段时间又需要换出。这样,大部分可用时间都花费在内存和硬盘之间来回传输页数据,而真正的工作几乎无法进行。显然这是一种罕见的情况,但如果用户坐在椅子上,只能干巴巴地观察硬盘的活动,而无法进行实际的工作,那是相当令人泄气的事情。

为防止这种情况,内核向某个当前换入页的进程颁发一枚所谓的交换令牌,且整个系统内只颁发一枚。交换令牌的好处在于,持有交换令牌的进程,其内存页不会被回收,或至少可以尽可能免遭回收。这使得该进程换入的页都可以保留在内存中,增加了完成工作的可能性

本质上,交换令牌对换入页的进程实现了一种“上位调度”。(但是,这根本不会改变CPU调度器的结果!)类似于每一个调度器,它必须保证在各个进程之间的公平性,因此,内核保证进程在获得交换令牌一段时间后就会失去,令牌将传递到下一个进程。原始的交换令牌建议方案(参见附录F)使用了一个超时定时器,定时器触发时会将令牌传递到下一个进程,在内核版本2.6.9最初集成了交换令牌方法时,就采用了这种策略。在内核版本2.6.20开发期间,引入了一种新的方案来抢占交换令牌,其工作机制将在下文讨论。令人感兴趣的是,交换令牌的实现非常简单,大约只包括100行代码,这再次证明了,好主意不见得是复杂的。

交换令牌通过一个全局指针实现,该指针指向当前拥有令牌的进程的mm_struct实例(实际上,内存区可能在几个进程间共享,而交换令牌是关联到某个特定内存区的,并非某个具体的进程。在这种意义上讲,交换令牌可能同时属于多个进程。实际上,它属于特定的内存区。但为简化阐述,这里假定只有一个进程关联到交换令牌所属的内存区):

//mm/thrash.c
struct mm_struct *swap_token_mm;/*页交换令牌全局变量*/
static unsigned int global_faults;/*计算调用do_swap_page的次数。每次换入一页时,都调用该函数,并对该计数器加1。这提供了一种可以判断进程获取交换令牌的频繁程度的可能性(与系统中其他进程相比)*/

//include/linux/mm_types.h
/*进程的内存管理描述符*/
struct mm_struct {
    ...
    unsigned int faultstamp;//保存了内核上一次试图获取令牌时全局变量 global_faults的 值
	unsigned int token_priority;//交换令牌相关的调度优先级,用于控制对交换令牌的访问
	unsigned int last_interval;//表示该进程等待交换令牌的时间间隔的长度
    ...
}

交换令牌通过调用grab_swap_token获取

  • do_swap_page 换入页
    • 如果页不在交换缓存中则调用 grab_swap_token 获取页交换令牌
      • 如果交换令牌尚未分配给任何进程,给当前进程交换令牌,函数结束
      • 如果交换令牌当前由其他进程持有
        • 如果在当前进程的等待时间已经不少于其上一次等待时间时,增加当前进程令牌优先级
        • 否则减小当前进程令牌优先级
        • 如果当前进程令牌优先级大于令牌持有者的优先级,当前优先级+2,抢占令牌持有者的令牌
      • 否则当前令牌优先级+2

当不再需要当前交换令牌的mm_struct时,必须使用put_swap_token来释放当前进程的交换令牌。disable_token则会强制性地剥夺令牌。在实际上必须换出页时,这是有必要的,读者在下文会看到这样的情况

交换令牌实现的关键在于,内核在何处检查当前进程是否是交换令牌的所有者,而这会对持有令牌的进程有何种影响。has_swap_token测试进程是否有交换令牌。但该检查只在内核中一处执行,即在内核检查一页是否已经被引用时(回想前文可知,这是判断一页是否将要被回收的基本要素之一,而page_referenced_onepage_referenced的一个子函数,只在那里调用):

  • page_referenced 计算页最近引用次数,降低页的活动程度,连续调用两次将把页从活动链表移动到惰性链表,计算了最近引用该页的次数,与 mark_page_accessed 相反
    • page_referenced_anon 计算了匿名页的使用者数目
      • page_referenced_one 计算使用该页的次数
        • has_swap_token 检测进程是否有交换令牌
    • page_referenced_file 计算了文件页的使用者数目
      • page_referenced_one 计算使用该页的次数
        • has_swap_token 检测进程是否有交换令牌

区分如下两种情形

  1. 所述页所在的内存区属于当前运行进程,而该进程持有交换令牌。由于交换令牌的所有者可以对拥有的页进行任意操作,page_referenced_one忽略了交换令牌的效果。
    这意味着,交换令牌的当前持有者不会阻止页的回收——如果它想要这样做,那么该页实际上是不必要的,回收该页不会妨碍该进程的工作。
  2. 当前运行进程不持有交换令牌,但操作的某页属于交换令牌持有者的地址空间。在这种情况下,该页标记为被引用,不会移动到惰性链表,因而也不会被回收。

但还需要考虑一件事情:虽然交换令牌对高负荷的系统具有有益的效用,但它对页交换很少的工作负荷具有不利的影响。因而内核在标记页的引用之前增加了另一项检查,即是否持有某个信号量。原始的交换令牌建议方案要求在处理缺页异常时强制施行交换令牌的效应。由于在内核中这个时机并不容易检测,因而可以通过检查是否持有mmap_sem信号量来近似。虽然这可能因几种原因而发生,但它也发生于缺页异常代码中,作为近似来说,这种做法是足够的。

在系统很少或不需要页交换时,发生缺页异常的概率是非常低的。但如果页交换的压力变大,那么发生缺页异常的概率也会相应增加。总而言之,这意味着,随着系统中缺页异常发生得越来越多,交换令牌机制发生作用的机会也相应增加。这消除了交换令牌在页交换活动很少的系统上的负面效应,而又保持了高负荷系统上交换令牌的正面效果。

18.8 处理交换缺页异常

虽然换出物理内存页是应该相对复杂的行为,但换入页要简单得多。按第4章的说法,当试图访问进程虚拟地址空间中注册的一页时,如果该页未映射到物理内存中,则处理器触发一个缺页异常。这并不一定意味着访问了一个换出页。举例来说,也可能是应用程序访问了一个并未分配给该进程的地址,或涉及了一个非线性映射。因而内核首先必须查明是否需要实际换入一页,如4.11节所讲述的,内核会调用体系结构相关的函数handle_pte_fault来检查内存管理数据结构,以完成这一任务。

尽管无论页的后备存储器如何,内核回收所有页的方式都是相同的,但这一点反过来是不成立的。这里描述的方法,只适用于从系统某个交换区读取的匿名映射数据。当缺页异常发生在属于某个文件映射的页上时,由第8章讨论的机制负责提供数据。

18.8.1 换入页

访问换出页导致的缺页异常,由mm/memory.c中的do_swap_page处理。如图18-19给出的代码流程图所示,换入一页比换出要容易得多,但其中涉及的仍然不只是一个简单的读操作。

内核不仅要检查所请求的页是否仍然或已经在交换缓存中,它还使用了一种简单的预读方法,一次性从交换区读入几页,预防未来可能出现的缺页异常。

在18.4.1节讨论过,换出页所在的交换区和槽位信息保持在页表项中(实际的表示因具体的体系结构而有所不同)。为获得通用值,内核首先对页表项调用我们熟悉的pte_to_swp_entry函数,获得一个swp_entry_t实例,其中用独立于机器的值唯一标识了换出页。

在这里插入图片描述

  • do_swap_page 换入页
    • pte_to_swp_entry 获取页表项中保存的页换出位置信息
    • lookup_swap_cache 检查所需的页是否在交换缓存中
    • 如果页不在交换缓存中,内核不仅必须要读取该页,还必须发起一个预读操作
      • grab_swap_token 尝试获取交换令牌
      • swapin_readahead 预读,对所需页对应槽位和相邻槽位发出读请求
      • read_swap_cache_async 异步读页
    • page_add_anon_rmap 将该页加入到逆向映射机制中
    • swap_free 释放交换区中该页的页槽位信息
    • do_wp_page 创建该页的一个副本,并将其添加到导致异常的进程的页表中,且将原始页的使用计数器减1。

18.8.2 读取交换区的数据

有两个函数可以从交换区将数据读入物理内存。read_swap_cache_async创建必要的先决条件并执行额外的管理任务,而 swap_readpage 负责将实际的读请求提交对块层。图18-20给出了read_swap_cache_async的代码流程图(假定在页分配期间没有发生错误,在读入换出页时也没有因竞态条件而导致错误)。
在这里插入图片描述

  • read_swap_cache_async 异步读页
    • find_get_page 从交换缓存中找该页
    • alloc_page_vma 分配新的内存页用来存读入的页数据
    • add_to_swap_cache 将页加入交换缓存中
    • lru_cache_add_active 将页加入全局lru活动链表
    • swap_readpage 将页数据从交换区读入该页

18.8.3 交换预读

类似于文件的读取,内核在从交换区读取数据时也使用了一种预读机制。这确保了数据可以预先读入内存,使得未来的换入页请求可以迅速完成,因而提高了系统性能。与比较复杂的文件预读方法相比,页交换子系统的预读机制相对简单,如下列代码所示:

  • swapin_readahead 交换区的预读
    • valid_swaphandles 计算预读页的数目。通常将预读2^page_cluster页
    • read_swap_cache_async 异步读页

18.9 发起内存回收

18.1节阐述过,到目前为止讨论过的页面选择和换出例程都由一个抽象层控制,该层会决定在何时回收多少页内存。该决策会重定向到两个地方:首先是kswapd守护进程,该守护进程试图在没有内存密集型应用程序运行时,维护系统中内存使用的均衡;其次是一种应急机制,在内核认为出现严重的内存不足时启用。

18.9.1 用kswapd进行周期性内存回收

kswapd是一个内核守护进程,每当系统启动时由kswap_init激活。只要计算机在运行,该守护进程将一直执行:

  • kswapd_init 初始化kswapd守护进程

    • for_each_node_state(nid, N_HIGH_MEMORY) 对每个NUMA内存域,都会激活一个独立的kswapd实例,非NUMA系统只使用一个kswapd
    • kswapd_run 启动kswapd守护进程
      • kthread_run(kswapd, pgdat, “kswapd%d”, nid); 启动kswapd守护进程
  • kswapd kswapd守护进程执行函数

    • while:
    • prepare_to_wait 将进程置于一个与NUMA内存域相关的等待队列上
    • 如果kswapd_max_order指定的当前分配阶大于上一次执行均衡操作的分配阶则什么也不做
    • 否则调度执行其他进程 schedule
    • 调用 balance_pgdat 来均衡该结点内存域

在这里插入图片描述

  • balance_pgdat 均衡结点内存域
    • for优先级数值从大到小(实际优先级从低到高)
      • if优先级数值为0,即最高优先级,则停用交换令牌,调用 disable_swap_token ,因为在非常需要内存时,阻止内存页换出不可取
      • for内存结点中的每个内存域,找到有内存页可以回收的的内存域
      • for每个内存域,确定内存域中需要进行扫描的所有LRU页
      • for每个内存域
        • 调用 shrink_zone 来回收内存域中的页
        • 调用 shrink_slab 来回收slab系统为其他数据结构分配的缓存
        • congestion_wait 等待几个刷出操作成功完成,防止块层的拥塞

18.9.2 在严重内存不足时换出页 try_to_free_pages

try_to_free_pages 例程用于紧急、非预期的内存回收操作。图18-22给出了该函数的代码流程图。

在这里插入图片描述

  • try_to_free_pages 用于紧急、非预期的内存回收操作,在严重内存不足时换出页
    • for内存域数组,计算所有内存域中lru链表中页的数目
    • for优先级从低到高(数值高,优先级低),如果内核以最高优先级运作,将禁用交换令牌
      • shrink_zones 回收内存域数组中的页
      • shrink_slab 收缩slab缓存
      • wakeup_pdflush 唤醒周期回写进程
      • congestion_wait 等待几个刷出操作成功完成,防止块层的拥塞

18.10 收缩其他缓存(slab缓存)

除了页缓存之外,内核还管理着其他缓存,这些缓存通常基于第3章讨论的slab(或slub/slob,但在下文中将统一使用术语slab指代)机制。

slab管理着常用的数据结构,以确保伙伴系统中按页管理的内存能够得到更有效的使用,并通过缓存,来更快速而容易地分配数据类型的实例。

使用此类缓存的内核子系统可以向内核动态地注册“收缩器”函数。这些函数在可用内存较低时调用,释放一些已用的内存空间(从技术上看,收缩器函数与slab没有什么固定的关联,但当前没有其他使用收缩器的缓存类型)。

除了注册和删除收缩器函数的例程之外,内核还必须提供发起缓存收缩的方法。这些将在以下各节中仔细考察。

18.10.1 数据结构

内核定义了用于描述收缩器函数特征的数据结构:

//include/linux/mm.h
//收缩器结构体,用于管理除了页缓存之外的其他缓存,这些缓存通常基于slab
struct shrinker {
	int (*shrink)(int nr_to_scan, gfp_t gfp_mask);//指向用于收缩缓存的函数,参数为所检查的内存页的数目和内存类型,返回值是一个整数,表示有多少个对象仍然在缓存中.如果返回-1,表示该函数不能进行任何收缩。在内核想要查询缓存中对象的数目时,可以将nr_to_scan参数传递0值。
	int seeks;	/* seeks to recreate an obj *//*用于调整缓存相对于页缓存的权重*/

	/* These are for internal use */
	struct list_head list;//链表元素,链表头为 shrinker_list。所有注册的收缩器保存在它上面。
	long nr;	/* objs pending delete *//*由收缩器函数释放的对象数目。内核使用该值来启用对象的批处理,以提高性能。*/
};

18.10.2 注册和删除slab收缩器

register_shrinker用于注册一个新的收缩器:

//mm/vmscan.c
//注册一个新的收缩器,其中shrinker的seek和shrink应该已经设置好适当的值,该函数只将收缩器注册到全局链表 shrinker_list 中
void register_shrinker(struct shrinker *shrinker)

目前,内核中只有少量收缩器,如以下几个。

  1. shrink_icache_memory 收缩第8章中讨论的inode缓存,并管理struct Inode对象。
  2. shrink_dcache_memory 负责第8章讨论的dentry缓存。
  3. mb_cache_shrink_fn 收缩一个用于文件系统元数据的通用缓存(当前用于实现Ext2和Ext3文件系统中的增强属性)。

unregister_shrinker 函数根据shrinker实例,从全局链表删除对应的收缩器:

//mm/vmscan.c
//从全局链表删除对应的收缩器
void unregister_shrinker(struct shrinker *shrinker)

18.10.3 收缩缓存 shrink_slab

shrink_slab用于收缩所有注册为可收缩的缓存。其参数包括指定内存类型的分配掩码和页面回收期间扫描页的数目。本质上,它会遍历shrinker_list中所有的收缩器:

  • shrink_slab 调用slab收缩器链表中的所有注册的收缩函数
    • for收缩器链表shrinker_list中所有收缩器
      • (*shrinker->shrink)(0, gfp_mask); 调用注册的收缩器函数,查询缓存中对象的数目
      • 计算要释放的缓存对象数目
      • while total_scan >= SHRINK_BATCH 当可释放的对象数目超过 SHRINK_BATCH 时(缓存中的对象128个一组释放)
        • 调用 (*shrinker->shrink)(this_scan, gfp_mask); 释放缓存中的对象
        • cond_resched 调度,在两次释放间调度,确保系统阻塞时间不会太长

总结

交换区可以使用一个分区或文件,可以设置优先级.交换区细分为多个槽,每个槽为一页.用聚集的构造方式加快访问交换区.内存中用数据结构记录交换区的信息

kswapd内核守护进程周期检查所有内存结点中所有内存域的内存使用情况,回收页

内核用内存域中两个LRU链表(活动链表和惰性链表)来选择哪些页可以换出

内核用缓存缩减框架作为最后获取内存的方式(内核使用的缓存不大)

在内存和交换区之间传输数据之间,用交换缓存当作媒介

CPU中还有一个 LRU页缓存 ,在新页被加入各内存域活动或惰性链表前会先被加入该缓存,缓存满了以后才会被移动到惰性链表中

lru缓存会先释放给 cpu冷热页缓存 per_cpu_pageset,如果 cpu冷热页缓存 满了会释放给伙伴系统

Linux内核中的LRU缓存和per_cpu_pageset机制是用于管理内存的两种重要机制。

  1. LRU缓存:Linux内核中的LRU缓存是一种数据结构,用于存储最近使用的页面。当系统需要访问一个页面时,它会首先检查该页面是否在LRU缓存中。如果在缓存中,则直接返回该页面;如果不在缓存中,则需要从磁盘或其他存储设备中读取该页面,并将其添加到LRU缓存中。通过使用LRU缓存,可以减少对磁盘或其他存储设备的访问次数,从而提高系统的性能。

  2. per_cpu_pageset机制:per_cpu_pageset是一种针对每个CPU的页面集,用于存储每个CPU的页面缓存。在多核处理器系统中,每个CPU都有自己的缓存,因此需要为每个CPU分配一个独立的页面集。per_cpu_pageset机制可以确保每个CPU的页面缓存不会相互干扰,从而提高系统的性能。此外,per_cpu_pageset机制还可以实现CPU亲和性,即将某些进程或线程绑定到特定的CPU上运行,以提高这些进程或线程的性能。

在内核中,LRU缓存和per_cpu_pageset机制通常是由内存管理系统(如页分配器)来管理的。当系统需要分配一个新的页面时,内存管理系统会首先检查LRU缓存中是否有可用的页面。如果有,则直接使用该页面;如果没有,则需要从磁盘或其他存储设备中读取一个新的页面,并将其添加到LRU缓存中。同时,还需要将该页面添加到当前CPU的per_cpu_pageset中,以便对该页面进行管理和访问控制。

当系统需要释放一个页面时,内存管理系统会从LRU缓存和per_cpu_pageset中同时删除该页面。如果该页面只在一个CPU的per_cpu_pageset中存在,则只需要从该per_cpu_pageset中删除即可。如果该页面在多个CPU的per_cpu_pageset中都存在,则需要在所有相关的per_cpu_pageset中都删除该页面。

交换令牌

为了避免页颠簸,即某页短时间内被经常换入换出,内存增加了交换令牌机制
即全局变量记录进程的 内存管理结构体 该进程的内存就不会被其他机制回收。该令牌根据优先级抢占的方式来在不同进程间传递

在换入不在交换缓存中的页时尝试获取交换令牌

slab收缩器

全局 shrinker_list 链表存着所有注册的slab缓存收缩器

shrink_slab 函数用于遍历链表释放缓存对象,缓存对象每128个一组释放,两次释放之间进行一次进程调度防止阻塞时间过长

=========================================

涉及的命令和配置:
mkswap,命令用于格式化交换分区/文件
swapon,命令用于启用交换区

全局数组 swap_info 存储了各交换区的信息,可以是交换分区或交换文件

交换分区信息 cat /proc/swaps

全局变量 swap_list 存储全局数组swap_info中第一个使用的交换区(优先级最高的交换区)

全局变量 nr_swap_pages,存储可用的交换区空闲页槽的总数
全局变量 total_swap_pages,交换区槽位总数,而不考虑是否为空闲槽位

全局变量 vm_swappiness,表示内核换出页的积极程度对应 /proc/sys/vm/swappiness

全局变量 swap_token_mm,表示页交换令牌
全局变量 global_faults,计算调用do_swap_page的次数。每次换入一页时,都调用该函数,并对该计数器加1。这提供了一种可以判断进程获取交换令牌的频繁程度的可能性(与系统中其他进程相比)

全局变量 page_cluster,表示交换区预读页的阶数,用户层为/proc/sys/vm/page-cluster,可将它设为0来禁止交换区的预读

全局变量 shrinker_list,slab缓存收缩器全局链表头,存着所有slab收缩器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值