Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,90 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/active_mm.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
=========
Active MM
=========
注意,在配置了 CONFIG_MMU_LAZY_TLB_REFCOUNT=n 的内核中,mm_count 引用计数
可能不再包括“懒惰”用户(运行任务中 ->active_mm == mm && ->mm == NULL)。
获取和释放这些懒惰引用必须使用 mmgrab_lazy_tlb() 和 mmdrop_lazy_tlb() 这
两个辅助函数,它们抽象了这个配置选项。
这是一封linux之父回复开发者的一封邮件,所以翻译时我尽量保持邮件格式的完整。
::
List: linux-kernel
Subject: Re: active_mm
From: Linus Torvalds <torvalds () transmeta ! com>
Date: 1999-07-30 21:36:24
因为我并不经常写解释,所以已经抄送到linux-kernel邮件列表,而当我做这些,
且更多的人在阅读它们时,我觉得棒极了。
1999年7月30日 星期五, David Mosberger 写道:
>
> 是否有一个简短的描述,说明task_struct中的
> "mm" 和 "active_mm"应该如何使用? (如果
> 这个问题在邮件列表中讨论过,我表示歉意--我刚
> 刚度假回来,有一段时间没能关注linux-kernel了)。
基本上,新的设定是:
- 我们有“真实地址空间”和“匿名地址空间”。区别在于,匿名地址空间根本不关心用
户级页表,所以当我们做上下文切换到匿名地址空间时,我们只是让以前的地址空间
处于活动状态。
一个“匿名地址空间”的明显用途是任何不需要任何用户映射的线程--所有的内核线
程基本上都属于这一类,但即使是“真正的”线程也可以暂时说在一定时间内它们不
会对用户空间感兴趣,调度器不妨试着避免在切换VM状态上浪费时间。目前只有老
式的bdflush sync能做到这一点。
- “tsk->mm” 指向 “真实地址空间”。对于一个匿名进程来说,tsk->mm将是NULL,
其逻辑原因是匿名进程实际上根本就 “没有” 真正的地址空间。
- 然而,我们显然需要跟踪我们为这样的匿名用户“偷用”了哪个地址空间。为此,我们
有 “tsk->active_mm”,它显示了当前活动的地址空间是什么。
规则是,对于一个有真实地址空间的进程(即tsk->mm是 non-NULL),active_mm
显然必须与真实的mm相同。
对于一个匿名进程,tsk->mm == NULL,而tsk->active_mm是匿名进程运行时
“借用”的mm。当匿名进程被调度走时,借用的地址空间被返回并清除。
为了支持所有这些,“struct mm_struct”现在有两个计数器:一个是 “mm_users”
计数器,即有多少 “真正的地址空间用户”,另一个是 “mm_count”计数器,即 “lazy”
用户(即匿名用户)的数量,如果有任何真正的用户,则加1。
通常情况下,至少有一个真正的用户,但也可能是真正的用户在另一个CPU上退出,而
一个lazy的用户仍在活动,所以你实际上得到的情况是,你有一个地址空间 **只**
被lazy的用户使用。这通常是一个短暂的生命周期状态,因为一旦这个线程被安排给一
个真正的线程,这个 “僵尸” mm就会被释放,因为 “mm_count”变成了零。
另外,一个新的规则是,**没有人** 再把 “init_mm” 作为一个真正的MM了。
“init_mm”应该被认为只是一个 “没有其他上下文时的lazy上下文”,事实上,它主
要是在启动时使用,当时还没有真正的VM被创建。因此,用来检查的代码
if (current->mm == &init_mm)
一般来说,应该用
if (!current->mm)
取代上面的写法(这更有意义--测试基本上是 “我们是否有一个用户环境”,并且通常
由缺页异常处理程序和类似的东西来完成)。
总之,我刚才在ftp.kernel.org上放了一个pre-patch-2.3.13-1,因为它稍微改
变了接口以适配alpha(谁会想到呢,但alpha体系结构上下文切换代码实际上最终是
最丑陋的之一--不像其他架构的MM和寄存器状态是分开的,alpha的PALcode将两者
连接起来,你需要同时切换两者)。
(文档来源 https://lore.kernel.org/lkml/Pine.LNX.4.10.9907301410280.752-100000@penguin.transmeta.com/)
@@ -0,0 +1,81 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/balance.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
内存平衡
========
2000年1月开始,作者:Kanoj Sarcar <kanoj@sgi.com>
对于 !__GFP_HIGH 和 !__GFP_KSWAPD_RECLAIM 以及非 __GFP_IO 的分配,需要进行
内存平衡。
调用者避免回收的第一个原因是调用者由于持有自旋锁或处于中断环境中而无法睡眠。第二个
原因可能是,调用者愿意在不产生页面回收开销的情况下分配失败。这可能发生在有0阶回退
选项的机会主义高阶分配请求中。在这种情况下,调用者可能也希望避免唤醒kswapd。
__GFP_IO分配请求是为了防止文件系统死锁。
在没有非睡眠分配请求的情况下,做平衡似乎是有害的。页面回收可以被懒散地启动,也就是
说,只有在需要的时候(也就是区域的空闲内存为0),而不是让它成为一个主动的过程。
也就是说,内核应该尝试从直接映射池中满足对直接映射页的请求,而不是回退到dma池中,
这样就可以保持dma池为dma请求(不管是不是原子的)所填充。类似的争论也适用于高内存
和直接映射的页面。相反,如果有很多空闲的dma页,最好是通过从dma池中分配一个来满足
常规的内存请求,而不是产生常规区域平衡的开销。
在2.2中,只有当空闲页总数低于总内存的1/64时,才会启动内存平衡/页面回收。如果dma
和常规内存的比例合适,即使dma区完全空了,也很可能不会进行平衡。2.2已经在不同内存
大小的生产机器上运行,即使有这个问题存在,似乎也做得不错。在2.3中,由于HIGHMEM的
存在,这个问题变得更加严重。
在2.3中,区域平衡可以用两种方式之一来完成:根据区域的大小(可能是低级区域的大小),
我们可以在初始化阶段决定在平衡任何区域时应该争取多少空闲页。好的方面是,在平衡的时
候,我们不需要看低级区的大小,坏的方面是,我们可能会因为忽略低级区可能较低的使用率
而做过于频繁的平衡。另外,只要对分配程序稍作修改,就有可能将memclass()宏简化为一
个简单的等式。
另一个可能的解决方案是,我们只在一个区 **和** 其所有低级区的空闲内存低于该区及其
低级区总内存的1/64时进行平衡。这就解决了2.2的平衡问题,并尽可能地保持了与2.2行为
的接近。另外,平衡算法在各种架构上的工作方式也是一样的,这些架构有不同数量和类型的
内存区。如果我们想变得更花哨一点,我们可以在未来为不同区域的自由页面分配不同的权重。
请注意,如果普通区的大小与dma区相比是巨大的,那么在决定是否平衡普通区的时候,考虑
空闲的dma页就变得不那么重要了。那么第一个解决方案就变得更有吸引力。
所附的补丁实现了第二个解决方案。它还 “修复”了两个问题:首先,在低内存条件下,kswapd
被唤醒,就像2.2中的非睡眠分配。第二,HIGHMEM区也被平衡了,以便给replace_with_highmem()
一个争取获得HIGHMEM页的机会,同时确保HIGHMEM分配不会落回普通区。这也确保了HIGHMEM
页不会被泄露(例如,在一个HIGHMEM页在交换缓存中但没有被任何人使用的情况下)。
kswapd还需要知道它应该平衡哪些区。kswapd主要是在无法进行平衡的情况下需要的,可能
是因为所有的分配请求都来自中断上下文,而所有的进程上下文都在睡眠。对于2.3,
kswapd并不真正需要平衡高内存区,因为中断上下文并不请求高内存页。kswapd看zone
结构体中的zone_wake_kswapd字段来决定一个区是否需要平衡。
如果从进程内存和shm中偷取页面可以减轻该页面节点中任何区的内存压力,而该区的内存压力
已经低于其水位,则会进行偷取。
watermark[WMARK_MIN/WMARK_LOW/WMARK_HIGH]/low_on_memory/zone_wake_kswapd:
这些是每个区的字段,用于确定一个区何时需要平衡。当页面数低于水位[WMARK_MIN]时,
hysteric 的字段low_on_memory被设置。这个字段会一直被设置,直到空闲页数变成水位
[WMARK_HIGH]。当low_on_memory被设置时,页面分配请求将尝试释放该区域的一些页面(如果
请求中设置了GFP_WAIT)。与此相反的是,决定唤醒kswapd以释放一些区的页。这个决定不是基于
hysteresis 的,而是当空闲页的数量低于watermark[WMARK_LOW]时就会进行;在这种情况下,
zone_wake_kswapd也被设置。
我所听到的(超棒的)想法:
1. 动态经历应该影响平衡:可以跟踪一个区的失败请求的数量,并反馈到平衡方案中(jalvo@mbay.net)。
2. 实现一个类似于replace_with_highmem()的replace_with_regular(),以保留dma页面。
(lkd@tantalophile.demon.co.uk)
@@ -0,0 +1,32 @@
.. SPDX-License-Identifier: GPL-2.0
:Original: Documentation/mm/damon/api.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
=======
API参考
=======
内核空间的程序可以使用下面的API来使用DAMON的每个功能。你所需要做的就是引用 ``damon.h`` ,
它位于源代码树的include/linux/。
结构体
======
该API在以下内核代码中:
include/linux/damon.h
函数
====
该API在以下内核代码中:
mm/damon/core.c
@@ -0,0 +1,140 @@
.. SPDX-License-Identifier: GPL-2.0
:Original: Documentation/mm/damon/design.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
====
设计
====
可配置的层
==========
DAMON提供了数据访问监控功能,同时使其准确性和开销可控。基本的访问监控需要依赖于目标地址空间
并为之优化的基元。另一方面,作为DAMON的核心,准确性和开销的权衡机制是在纯逻辑空间中。DAMON
将这两部分分离在不同的层中,并定义了它的接口,以允许各种低层次的基元实现与核心逻辑的配置。
由于这种分离的设计和可配置的接口,用户可以通过配置核心逻辑和适当的低级基元实现来扩展DAMON的
任何地址空间。如果没有提供合适的,用户可以自己实现基元。
例如,物理内存、虚拟内存、交换空间、那些特定的进程、NUMA节点、文件和支持的内存设备将被支持。
另外,如果某些架构或设备支持特殊的优化访问检查基元,这些基元将很容易被配置。
特定地址空间基元的参考实现
==========================
基本访问监测的低级基元被定义为两部分。:
1. 确定地址空间的监测目标地址范围
2. 目标空间中特定地址范围的访问检查。
DAMON目前为物理和虚拟地址空间提供了基元的实现。下面两个小节描述了这些工作的方式。
基于VMA的目标地址范围构造
-------------------------
这仅仅是针对虚拟地址空间基元的实现。对于物理地址空间,只是要求用户手动设置监控目标地址范围。
在进程的超级巨大的虚拟地址空间中,只有小部分被映射到物理内存并被访问。因此,跟踪未映射的地
址区域只是一种浪费。然而,由于DAMON可以使用自适应区域调整机制来处理一定程度的噪声,所以严
格来说,跟踪每一个映射并不是必须的,但在某些情况下甚至会产生很高的开销。也就是说,监测目标
内部过于巨大的未映射区域应该被移除,以不占用自适应机制的时间。
出于这个原因,这个实现将复杂的映射转换为三个不同的区域,覆盖地址空间的每个映射区域。这三个
区域之间的两个空隙是给定地址空间中两个最大的未映射区域。这两个最大的未映射区域是堆和最上面
的mmap()区域之间的间隙,以及在大多数情况下最下面的mmap()区域和堆之间的间隙。因为这些间隙
在通常的地址空间中是异常巨大的,排除这些间隙就足以做出合理的权衡。下面详细说明了这一点::
<heap>
<BIG UNMAPPED REGION 1>
<uppermost mmap()-ed region>
(small mmap()-ed regions and munmap()-ed regions)
<lowermost mmap()-ed region>
<BIG UNMAPPED REGION 2>
<stack>
基于PTE访问位的访问检查
-----------------------
物理和虚拟地址空间的实现都使用PTE Accessed-bit进行基本访问检查。唯一的区别在于从地址中
找到相关的PTE访问位的方式。虚拟地址的实现是为该地址的目标任务查找页表,而物理地址的实现则
是查找与该地址有映射关系的每一个页表。通过这种方式,实现者找到并清除下一个采样目标地址的位,
并检查该位是否在一个采样周期后再次设置。这可能会干扰其他使用访问位的内核子系统,即空闲页跟
踪和回收逻辑。为了避免这种干扰,DAMON使其与空闲页面跟踪相互排斥,并使用 ``PG_idle`` 和
``PG_young`` 页面标志来解决与回收逻辑的冲突,就像空闲页面跟踪那样。
独立于地址空间的核心机制
========================
下面四个部分分别描述了DAMON的核心机制和五个监测属性,即 ``采样间隔`` 、 ``聚集间隔`` 、
``更新间隔`` 、 ``最小区域数`` 和 ``最大区域数`` 。
访问频率监测
------------
DAMON的输出显示了在给定的时间内哪些页面的访问频率是多少。访问频率的分辨率是通过设置
``采样间隔`` 和 ``聚集间隔`` 来控制的。详细地说,DAMON检查每个 ``采样间隔`` 对每
个页面的访问,并将结果汇总。换句话说,计算每个页面的访问次数。在每个 ``聚合间隔`` 过
去后,DAMON调用先前由用户注册的回调函数,以便用户可以阅读聚合的结果,然后再清除这些结
果。这可以用以下简单的伪代码来描述::
while monitoring_on:
for page in monitoring_target:
if accessed(page):
nr_accesses[page] += 1
if time() % aggregation_interval == 0:
for callback in user_registered_callbacks:
callback(monitoring_target, nr_accesses)
for page in monitoring_target:
nr_accesses[page] = 0
sleep(sampling interval)
这种机制的监测开销将随着目标工作负载规模的增长而任意增加。
基于区域的抽样调查
------------------
为了避免开销的无限制增加,DAMON将假定具有相同访问频率的相邻页面归入一个区域。只要保持
这个假设(一个区域内的页面具有相同的访问频率),该区域内就只需要检查一个页面。因此,对
于每个 ``采样间隔`` ,DAMON在每个区域中随机挑选一个页面,等待一个 ``采样间隔`` ,检
查该页面是否同时被访问,如果被访问则增加该区域的访问频率。因此,监测开销是可以通过设置
区域的数量来控制的。DAMON允许用户设置最小和最大的区域数量来进行权衡。
然而,如果假设没有得到保证,这个方案就不能保持输出的质量。
适应性区域调整
--------------
即使最初的监测目标区域被很好地构建以满足假设(同一区域内的页面具有相似的访问频率),数
据访问模式也会被动态地改变。这将导致监测质量下降。为了尽可能地保持假设,DAMON根据每个
区域的访问频率自适应地进行合并和拆分。
对于每个 ``聚集区间`` ,它比较相邻区域的访问频率,如果频率差异较小,就合并这些区域。
然后,在它报告并清除每个区域的聚合接入频率后,如果区域总数不超过用户指定的最大区域数,
它将每个区域拆分为两个或三个区域。
通过这种方式,DAMON提供了其最佳的质量和最小的开销,同时保持了用户为其权衡设定的界限。
动态目标空间更新处理
--------------------
监测目标地址范围可以动态改变。例如,虚拟内存可以动态地被映射和解映射。物理内存可以被
热插拔。
由于在某些情况下变化可能相当频繁,DAMON允许监控操作检查动态变化,包括内存映射变化,
并仅在用户指定的时间间隔( ``更新间隔`` )中的每个时间段,将其应用于监控操作相关的
数据结构,如抽象的监控目标内存区。
@@ -0,0 +1,31 @@
.. SPDX-License-Identifier: GPL-2.0
:Original: Documentation/mm/damon/faq.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
常见问题
========
DAMON是否只支持虚拟内存?
=========================
不,DAMON的核心是独立于地址空间的。用户可以在DAMON核心上实现和配置特定地址空间的低级原始
部分,包括监测目标区域的构造和实际的访问检查。通过这种方式,DAMON用户可以用任何访问检查技
术来监测任何地址空间。
尽管如此,DAMON默认为虚拟内存和物理内存提供了基于vma/rmap跟踪和PTE访问位检查的地址空间
相关功能的实现,以供参考和方便使用。
我可以简单地监测页面的粒度吗?
==============================
是的,你可以通过设置 ``min_nr_regions`` 属性高于工作集大小除以页面大小的值来实现。
因为监视目标区域的大小被强制为 ``>=page size`` ,所以区域分割不会产生任何影响。
@@ -0,0 +1,32 @@
.. SPDX-License-Identifier: GPL-2.0
:Original: Documentation/mm/damon/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
==========================
DAMON:数据访问监视器
==========================
DAMON是Linux内核的一个数据访问监控框架子系统。DAMON的核心机制使其成为
(该核心机制详见(Documentation/translations/zh_CN/mm/damon/design.rst))
- *准确度* (监测输出对DRAM级别的内存管理足够有用;但可能不适合CPU Cache级别),
- *轻量级* (监控开销低到可以在线应用),以及
- *可扩展* (无论目标工作负载的大小,开销的上限值都在恒定范围内)。
因此,利用这个框架,内核的内存管理机制可以做出高级决策。会导致高数据访问监控开销的实
验性内存管理优化工作可以再次进行。同时,在用户空间,有一些特殊工作负载的用户可以编写
个性化的应用程序,以便更好地了解和优化他们的工作负载和系统。
.. toctree::
:maxdepth: 2
faq
design
api
@@ -0,0 +1,38 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/free_page_reporting.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
==========
空闲页报告
==========
空闲页报告是一个API,设备可以通过它来注册接收系统当前未使用的页面列表。这在虚拟
化的情况下是很有用的,客户机能够使用这些数据来通知管理器它不再使用内存中的某些页
面。
对于驱动,通常是气球驱动要使用这个功能,它将分配和初始化一个page_reporting_dev_info
结构体。它要填充的结构体中的字段是用于处理散点列表的 "report" 函数指针。它还必
须保证每次调用该函数时能处理至少相当于PAGE_REPORTING_CAPACITY的散点列表条目。
假设没有其他页面报告设备已经注册, 对page_reporting_register的调用将向报告框
架注册页面报告接口。
一旦注册,页面报告API将开始向驱动报告成批的页面。API将在接口被注册后2秒开始报告
页面,并在任何足够高的页面被释放之后2秒继续报告。
报告的页面将被存储在传递给报告函数的散列表中,最后一个条目的结束位被设置在条目
nent-1中。 当页面被报告函数处理时,分配器将无法访问它们。一旦报告函数完成,这些
页将被返回到它们所获得的自由区域。
在移除使用空闲页报告的驱动之前,有必要调用page_reporting_unregister,以移除
目前被空闲页报告使用的page_reporting_dev_info结构体。这样做将阻止进一步的报
告通过该接口发出。如果另一个驱动或同一驱动被注册,它就有可能恢复前一个驱动在报告
空闲页方面的工作。
Alexander Duyck, 2019年12月04日
@@ -0,0 +1,151 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/highmem.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
==========
高内存处理
==========
作者: Peter Zijlstra <a.p.zijlstra@chello.nl>
.. contents:: :local:
高内存是什么?
==============
当物理内存的大小接近或超过虚拟内存的最大大小时,就会使用高内存(highmem)。在这一点上,内
核不可能在任何时候都保持所有可用的物理内存的映射。这意味着内核需要开始使用它想访问的物理内
存的临时映射。
没有被永久映射覆盖的那部分(物理)内存就是我们所说的 "高内存"。对于这个边界的确切位置,有
各种架构上的限制。
例如,在i386架构中,我们选择将内核映射到每个进程的虚拟空间,这样我们就不必为内核的进入/退
出付出全部的TLB作废代价。这意味着可用的虚拟内存空间(i386上为4GiB)必须在用户和内核空间之
间进行划分。
使用这种方法的架构的传统分配方式是3:1,3GiB用于用户空间,顶部的1GiB用于内核空间。::
+--------+ 0xffffffff
| Kernel |
+--------+ 0xc0000000
| |
| User |
| |
+--------+ 0x00000000
这意味着内核在任何时候最多可以映射1GiB的物理内存,但是由于我们需要虚拟地址空间来做其他事
情--包括访问其余物理内存的临时映射--实际的直接映射通常会更少(通常在~896MiB左右)。
其他有mm上下文标签的TLB的架构可以有独立的内核和用户映射。然而,一些硬件(如一些ARM)在使
用mm上下文标签时,其虚拟空间有限。
临时虚拟映射
============
内核包含几种创建临时映射的方法。下面的列表按照使用的优先顺序显示了它们。
* kmap_local_page()。这个函数是用来要求短期映射的。它可以从任何上下文(包括中断)中调用,
但是映射只能在获取它们的上下文中使用。
在可行的情况下,这个函数应该比其他所有的函数优先使用。
这些映射是线程本地和CPU本地的,这意味着映射只能从这个线程中访问,并且当映射处于活跃状
态时,线程被绑定到CPU上。尽管这个函数从来没有禁用过抢占,但在映射被处理之前,CPU不能
通过CPU-hotplug从系统中拔出。
在本地的kmap区域中采取pagefaults是有效的,除非获取本地映射的上下文由于其他原因不允许
这样做。
如前所述,缺页异常和抢占从未被禁用。没有必要禁用抢占,因为当上下文切换到一个不同的任务
时,离开的任务的映射被保存,而进入的任务的映射被恢复。
kmap_local_page()总是返回一个有效的虚拟地址,并且假定kunmap_local()不会失败。
在CONFIG_HIGHMEM=n的内核中,对于低内存页,它返回直接映射的虚拟地址。只有真正的高内
存页面才会被临时映射。因此,用户可以为那些已知不是来自ZONE_HIGHMEM的页面调用普通的
page_address()。然而,使用kmap_local_page() / kunmap_local()总是安全的。
虽然它比kmap()快得多,但在高内存的情况下,它对指针的有效性有限制。与kmap()映射相反,
本地映射只在调用者的上下文中有效,不能传递给其他上下文。这意味着用户必须绝对保证返回
地址的使用只限于映射它的线程。
大多数代码可以被设计成使用线程本地映射。因此,用户在设计他们的代码时,应该尽量避免使用
kmap(),将页面映射到将被使用的同一线程中,并优先使用kmap_local_page()。
嵌套kmap_local_page()和kmap_atomic()映射在一定程度上是允许的(最多到KMAP_TYPE_NR),
但是它们的调用必须严格排序,因为映射的实现是基于堆栈的。关于如何管理嵌套映射的细节,
请参见kmap_local_page() kdocs(包含在 "函数 "部分)。
* kmap_atomic(). 这允许对单个页面进行非常短的时间映射。由于映射被限制在发布它的CPU上,
它表现得很好,但发布的任务因此被要求留在该CPU上直到它完成,以免其他任务取代它的映射。
kmap_atomic()也可以被中断上下文使用,因为它不睡眠,调用者也可能在调用kunmap_atomic()
后才睡眠。
内核中对kmap_atomic()的每次调用都会创建一个不可抢占的段,并禁用缺页异常。这可能是
未预期延迟的来源之一。因此用户应该选择kmap_local_page()而不是kmap_atomic()。
假设k[un]map_atomic()不会失败。
* kmap()。这应该被用来对单个页面进行短时间的映射,对抢占或迁移没有限制。它会带来开销,
因为映射空间是受限制的,并且受到全局锁的保护,以实现同步。当不再需要映射时,必须用
kunmap()释放该页被映射的地址。
映射变化必须广播到所有CPU(核)上,kmap()还需要在kmap的池被回绕(TLB项用光了,需要从第
一项复用)时进行全局TLB无效化,当映射空间被完全利用时,它可能会阻塞,直到有一个可用的
槽出现。因此,kmap()只能从可抢占的上下文中调用。
如果一个映射必须持续相对较长的时间,上述所有的工作都是必要的,但是内核中大部分的
高内存映射都是短暂的,而且只在一个地方使用。这意味着在这种情况下,kmap()的成本大
多被浪费了。kmap()并不是为长期映射而设计的,但是它已经朝着这个方向发展了,在较新
的代码中强烈不鼓励使用它,前面的函数集应该是首选。
在64位系统中,调用kmap_local_page()、kmap_atomic()和kmap()没有实际作用,因为64位
地址空间足以永久映射所有物理内存页面。
* vmap()。这可以用来将多个物理页长期映射到一个连续的虚拟空间。它需要全局同步来解除
映射。
临时映射的成本
==============
创建临时映射的代价可能相当高。体系架构必须操作内核的页表、数据TLB和/或MMU的寄存器。
如果CONFIG_HIGHMEM没有被设置,那么内核会尝试用一点计算来创建映射,将页面结构地址转换成
指向页面内容的指针,而不是去捣鼓映射。在这种情况下,解映射操作可能是一个空操作。
如果CONFIG_MMU没有被设置,那么就不可能有临时映射和高内存。在这种情况下,也将使用计算方法。
i386 PAE
========
在某些情况下,i386 架构将允许你在 32 位机器上安装多达 64GiB 的内存。但这有一些后果:
* Linux需要为系统中的每个页面建立一个页帧结构,而且页帧需要驻在永久映射中,这意味着:
* 你最多可以有896M/sizeof(struct page)页帧;由于页结构体是32字节的,所以最终会有
112G的页;然而,内核需要在内存中存储更多的页帧......
* PAE使你的页表变大--这使系统变慢,因为更多的数据需要在TLB填充等方面被访问。一个好处
是,PAE有更多的PTE位,可以提供像NX和PAT这样的高级功能。
一般的建议是,你不要在32位机器上使用超过8GiB的空间--尽管更多的空间可能对你和你的工作
量有用,但你几乎是靠你自己--不要指望内核开发者真的会很关心事情的进展情况。
函数
====
该API在以下内核代码中:
include/linux/highmem.h
include/linux/highmem-internal.h
@@ -0,0 +1,355 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/hmm.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
==================
异构内存管理 (HMM)
==================
提供基础设施和帮助程序以将非常规内存(设备内存,如板上 GPU 内存)集成到常规内核路径中,其
基石是此类内存的专用struct page(请参阅本文档的第 5 至 7 节)。
HMM 还为 SVM(共享虚拟内存)提供了可选的帮助程序,即允许设备透明地访问与 CPU 一致的程序
地址,这意味着 CPU 上的任何有效指针也是该设备的有效指针。这对于简化高级异构计算的使用变得
必不可少,其中 GPU、DSP 或 FPGA 用于代表进程执行各种计算。
本文档分为以下部分:在第一部分中,我揭示了与使用特定于设备的内存分配器相关的问题。在第二
部分中,我揭示了许多平台固有的硬件限制。第三部分概述了 HMM 设计。第四部分解释了 CPU 页
表镜像的工作原理以及 HMM 在这种情况下的目的。第五部分处理内核中如何表示设备内存。最后,
最后一节介绍了一个新的迁移助手,它允许利用设备 DMA 引擎。
.. contents:: :local:
使用特定于设备的内存分配器的问题
================================
具有大量板载内存(几 GB)的设备(如 GPU)历来通过专用驱动程序特定 API 管理其内存。这会
造成设备驱动程序分配和管理的内存与常规应用程序内存(私有匿名、共享内存或常规文件支持内存)
之间的隔断。从这里开始,我将把这个方面称为分割的地址空间。我使用共享地址空间来指代相反的情况:
即,设备可以透明地使用任何应用程序内存区域。
分割的地址空间的发生是因为设备只能访问通过设备特定 API 分配的内存。这意味着从设备的角度来
看,程序中的所有内存对象并不平等,这使得依赖于广泛的库的大型程序变得复杂。
具体来说,这意味着想要利用像 GPU 这样的设备的代码需要在通用分配的内存(malloc、mmap
私有、mmap 共享)和通过设备驱动程序 API 分配的内存之间复制对象(这仍然以 mmap 结束,
但是是设备文件)。
对于平面数据集(数组、网格、图像……),这并不难实现,但对于复杂数据集(列表、树……),
很难做到正确。复制一个复杂的数据集需要重新映射其每个元素之间的所有指针关系。这很容易出错,
而且由于数据集和地址的重复,程序更难调试。
分割地址空间也意味着库不能透明地使用它们从核心程序或另一个库中获得的数据,因此每个库可能
不得不使用设备特定的内存分配器来重复其输入数据集。大型项目会因此受到影响,并因为各种内存
拷贝而浪费资源。
复制每个库的API以接受每个设备特定分配器分配的内存作为输入或输出,并不是一个可行的选择。
这将导致库入口点的组合爆炸。
最后,随着高级语言结构(在 C++ 中,当然也在其他语言中)的进步,编译器现在有可能在没有程
序员干预的情况下利用 GPU 和其他设备。某些编译器识别的模式仅适用于共享地址空间。对所有
其他模式,使用共享地址空间也更合理。
I/O 总线、设备内存特性
======================
由于一些限制,I/O 总线削弱了共享地址空间。大多数 I/O 总线只允许从设备到主内存的基本
内存访问;甚至缓存一致性通常是可选的。从 CPU 访问设备内存甚至更加有限。通常情况下,它
不是缓存一致的。
如果我们只考虑 PCIE 总线,那么设备可以访问主内存(通常通过 IOMMU)并与 CPU 缓存一
致。但是,它只允许设备对主存储器进行一组有限的原子操作。这在另一个方向上更糟:CPU
只能访问有限范围的设备内存,而不能对其执行原子操作。因此,从内核的角度来看,设备内存不
能被视为与常规内存等同。
另一个严重的因素是带宽有限(约 32GBytes/s,PCIE 4.0 和 16 通道)。这比最快的 GPU
内存 (1 TBytes/s) 慢 33 倍。最后一个限制是延迟。从设备访问主内存的延迟比设备访问自
己的内存时高一个数量级。
一些平台正在开发新的 I/O 总线或对 PCIE 的添加/修改以解决其中一些限制
(OpenCAPI、CCIX)。它们主要允许 CPU 和设备之间的双向缓存一致性,并允许架构支持的所
有原子操作。遗憾的是,并非所有平台都遵循这一趋势,并且一些主要架构没有针对这些问题的硬
件解决方案。
因此,为了使共享地址空间有意义,我们不仅必须允许设备访问任何内存,而且还必须允许任何内
存在设备使用时迁移到设备内存(在迁移时阻止 CPU 访问)。
共享地址空间和迁移
==================
HMM 打算提供两个主要功能。第一个是通过复制cpu页表到设备页表中来共享地址空间,因此对
于进程地址空间中的任何有效主内存地址,相同的地址指向相同的物理内存。
为了实现这一点,HMM 提供了一组帮助程序来填充设备页表,同时跟踪 CPU 页表更新。设备页表
更新不像 CPU 页表更新那么容易。要更新设备页表,您必须分配一个缓冲区(或使用预先分配的
缓冲区池)并在其中写入 GPU 特定命令以执行更新(取消映射、缓存失效和刷新等)。这不能通
过所有设备的通用代码来完成。因此,为什么HMM提供了帮助器,在把硬件的具体细节留给设备驱
动程序的同时,把一切可以考虑的因素都考虑进去了。
HMM 提供的第二种机制是一种新的 ZONE_DEVICE 内存,它允许为设备内存的每个页面分配一个
struct page。这些页面很特殊,因为 CPU 无法映射它们。然而,它们允许使用现有的迁移机
制将主内存迁移到设备内存,从 CPU 的角度来看,一切看起来都像是换出到磁盘的页面。使用
struct page可以与现有的 mm 机制进行最简单、最干净的集成。再次,HMM 仅提供帮助程序,
首先为设备内存热插拔新的 ZONE_DEVICE 内存,然后执行迁移。迁移内容和时间的策略决定留
给设备驱动程序。
请注意,任何 CPU 对设备页面的访问都会触发缺页异常并迁移回主内存。例如,当支持给定CPU
地址 A 的页面从主内存页面迁移到设备页面时,对地址 A 的任何 CPU 访问都会触发缺页异常
并启动向主内存的迁移。
凭借这两个特性,HMM 不仅允许设备镜像进程地址空间并保持 CPU 和设备页表同步,而且还通
过迁移设备正在使用的数据集部分来利用设备内存。
地址空间镜像实现和API
=====================
地址空间镜像的主要目标是允许将一定范围的 CPU 页表复制到一个设备页表中;HMM 有助于
保持两者同步。想要镜像进程地址空间的设备驱动程序必须从注册 mmu_interval_notifier
开始::
int mmu_interval_notifier_insert(struct mmu_interval_notifier *interval_sub,
struct mm_struct *mm, unsigned long start,
unsigned long length,
const struct mmu_interval_notifier_ops *ops);
在 ops->invalidate() 回调期间,设备驱动程序必须对范围执行更新操作(将范围标记为只
读,或完全取消映射等)。设备必须在驱动程序回调返回之前完成更新。
当设备驱动程序想要填充一个虚拟地址范围时,它可以使用::
int hmm_range_fault(struct hmm_range *range);
如果请求写访问,它将在丢失或只读条目上触发缺页异常(见下文)。缺页异常使用通用的 mm 缺
页异常代码路径,就像 CPU 缺页异常一样。使用模式是::
int driver_populate_range(...)
{
struct hmm_range range;
...
range.notifier = &interval_sub;
range.start = ...;
range.end = ...;
range.hmm_pfns = ...;
if (!mmget_not_zero(interval_sub->notifier.mm))
return -EFAULT;
again:
range.notifier_seq = mmu_interval_read_begin(&interval_sub);
mmap_read_lock(mm);
ret = hmm_range_fault(&range);
if (ret) {
mmap_read_unlock(mm);
if (ret == -EBUSY)
goto again;
return ret;
}
mmap_read_unlock(mm);
take_lock(driver->update);
if (mmu_interval_read_retry(&ni, range.notifier_seq) {
release_lock(driver->update);
goto again;
}
/* Use pfns array content to update device page table,
* under the update lock */
release_lock(driver->update);
return 0;
}
driver->update 锁与驱动程序在其 invalidate() 回调中使用的锁相同。该锁必须在调用
mmu_interval_read_retry() 之前保持,以避免与并发 CPU 页表更新发生任何竞争。
利用 default_flags 和 pfn_flags_mask
====================================
hmm_range 结构有 2 个字段,default_flags 和 pfn_flags_mask,它们指定整个范围
的故障或快照策略,而不必为 pfns 数组中的每个条目设置它们。
例如,如果设备驱动程序需要至少具有读取权限的范围的页面,它会设置::
range->default_flags = HMM_PFN_REQ_FAULT;
range->pfn_flags_mask = 0;
并如上所述调用 hmm_range_fault()。这将填充至少具有读取权限的范围内的所有页面。
现在假设驱动程序想要做同样的事情,除了它想要拥有写权限的范围内的一页。现在驱动程序设
置::
range->default_flags = HMM_PFN_REQ_FAULT;
range->pfn_flags_mask = HMM_PFN_REQ_WRITE;
range->pfns[index_of_write] = HMM_PFN_REQ_WRITE;
有了这个,HMM 将在至少读取(即有效)的所有页面中异常,并且对于地址
== range->start + (index_of_write << PAGE_SHIFT) 它将异常写入权限,即,如果
CPU pte 没有设置写权限,那么HMM将调用handle_mm_fault()。
hmm_range_fault 完成后,标志位被设置为页表的当前状态,即 HMM_PFN_VALID | 如果页
面可写,将设置 HMM_PFN_WRITE。
从核心内核的角度表示和管理设备内存
==================================
尝试了几种不同的设计来支持设备内存。第一个使用特定于设备的数据结构来保存有关迁移内存
的信息,HMM 将自身挂接到 mm 代码的各个位置,以处理对设备内存支持的地址的任何访问。
事实证明,这最终复制了 struct page 的大部分字段,并且还需要更新许多内核代码路径才
能理解这种新的内存类型。
大多数内核代码路径从不尝试访问页面后面的内存,而只关心struct page的内容。正因为如此,
HMM 切换到直接使用 struct page 用于设备内存,这使得大多数内核代码路径不知道差异。
我们只需要确保没有人试图从 CPU 端映射这些页面。
移入和移出设备内存
==================
由于 CPU 无法直接访问设备内存,因此设备驱动程序必须使用硬件 DMA 或设备特定的加载/存
储指令来迁移数据。migrate_vma_setup()、migrate_vma_pages() 和
migrate_vma_finalize() 函数旨在使驱动程序更易于编写并集中跨驱动程序的通用代码。
在将页面迁移到设备私有内存之前,需要创建特殊的设备私有 ``struct page`` 。这些将用
作特殊的“交换”页表条目,以便 CPU 进程在尝试访问已迁移到设备专用内存的页面时会发生异常。
这些可以通过以下方式分配和释放::
struct resource *res;
struct dev_pagemap pagemap;
res = request_free_mem_region(&iomem_resource, /* number of bytes */,
"name of driver resource");
pagemap.type = MEMORY_DEVICE_PRIVATE;
pagemap.range.start = res->start;
pagemap.range.end = res->end;
pagemap.nr_range = 1;
pagemap.ops = &device_devmem_ops;
memremap_pages(&pagemap, numa_node_id());
memunmap_pages(&pagemap);
release_mem_region(pagemap.range.start, range_len(&pagemap.range));
还有devm_request_free_mem_region(), devm_memremap_pages(),
devm_memunmap_pages() 和 devm_release_mem_region() 当资源可以绑定到 ``struct device``.
整体迁移步骤类似于在系统内存中迁移 NUMA 页面(see Documentation/mm/page_migration.rst) ,
但这些步骤分为设备驱动程序特定代码和共享公共代码:
1. ``mmap_read_lock()``
设备驱动程序必须将 ``struct vm_area_struct`` 传递给migrate_vma_setup(),
因此需要在迁移期间保留 mmap_read_lock() 或 mmap_write_lock()。
2. ``migrate_vma_setup(struct migrate_vma *args)``
设备驱动初始化了 ``struct migrate_vma`` 的字段,并将该指针传递给
migrate_vma_setup()。``args->flags`` 字段是用来过滤哪些源页面应该被迁移。
例如,设置 ``MIGRATE_VMA_SELECT_SYSTEM`` 将只迁移系统内存,设置
``MIGRATE_VMA_SELECT_DEVICE_PRIVATE`` 将只迁移驻留在设备私有内存中的页
面。如果后者被设置, ``args->pgmap_owner`` 字段被用来识别驱动所拥有的设备
私有页。这就避免了试图迁移驻留在其他设备中的设备私有页。目前,只有匿名的私有VMA
范围可以被迁移到系统内存和设备私有内存。
migrate_vma_setup()所做的第一步是用 ``mmu_notifier_invalidate_range_start()``
和 ``mmu_notifier_invalidate_range_end()`` 调用来遍历设备周围的页表,使
其他设备的MMU无效,以便在 ``args->src`` 数组中填写要迁移的PFN。
``invalidate_range_start()`` 回调传递给一个``struct mmu_notifier_range`` ,
其 ``event`` 字段设置为MMU_NOTIFY_MIGRATE, ``owner`` 字段设置为传递给
migrate_vma_setup()的 ``args->pgmap_owner`` 字段。这允许设备驱动跳过无
效化回调,只无效化那些实际正在迁移的设备私有MMU映射。这一点将在下一节详细解释。
在遍历页表时,一个 ``pte_none()`` 或 ``is_zero_pfn()`` 条目导致一个有效
的 “zero” PFN 存储在 ``args->src`` 阵列中。这让驱动分配设备私有内存并清
除它,而不是复制一个零页。到系统内存或设备私有结构页的有效PTE条目将被
``lock_page()``锁定,与LRU隔离(如果系统内存和设备私有页不在LRU上),从进
程中取消映射,并插入一个特殊的迁移PTE来代替原来的PTE。 migrate_vma_setup()
还清除了 ``args->dst`` 数组。
3. 设备驱动程序分配目标页面并将源页面复制到目标页面。
驱动程序检查每个 ``src`` 条目以查看该 ``MIGRATE_PFN_MIGRATE`` 位是否已
设置并跳过未迁移的条目。设备驱动程序还可以通过不填充页面的 ``dst`` 数组来选
择跳过页面迁移。
然后,驱动程序分配一个设备私有 struct page 或一个系统内存页,用 ``lock_page()``
锁定该页,并将 ``dst`` 数组条目填入::
dst[i] = migrate_pfn(page_to_pfn(dpage));
现在驱动程序知道这个页面正在被迁移,它可以使设备私有 MMU 映射无效并将设备私有
内存复制到系统内存或另一个设备私有页面。由于核心 Linux 内核会处理 CPU 页表失
效,因此设备驱动程序只需使其自己的 MMU 映射失效。
驱动程序可以使用 ``migrate_pfn_to_page(src[i])`` 来获取源设备的
``struct page`` 面,并将源页面复制到目标设备上,如果指针为 ``NULL`` ,意
味着源页面没有被填充到系统内存中,则清除目标设备的私有内存。
4. ``migrate_vma_pages()``
这一步是实际“提交”迁移的地方。
如果源页是 ``pte_none()`` 或 ``is_zero_pfn()`` 页,这时新分配的页会被插
入到CPU的页表中。如果一个CPU线程在同一页面上发生异常,这可能会失败。然而,页
表被锁定,只有一个新页会被插入。如果它失去了竞争,设备驱动将看到
``MIGRATE_PFN_MIGRATE`` 位被清除。
如果源页被锁定、隔离等,源 ``struct page`` 信息现在被复制到目标
``struct page`` ,最终完成CPU端的迁移。
5. 设备驱动为仍在迁移的页面更新设备MMU页表,回滚未迁移的页面。
如果 ``src`` 条目仍然有 ``MIGRATE_PFN_MIGRATE`` 位被设置,设备驱动可以
更新设备MMU,如果 ``MIGRATE_PFN_WRITE`` 位被设置,则设置写启用位。
6. ``migrate_vma_finalize()``
这一步用新页的页表项替换特殊的迁移页表项,并释放对源和目的 ``struct page``
的引用。
7. ``mmap_read_unlock()``
现在可以释放锁了。
独占访问存储器
==============
一些设备具有诸如原子PTE位的功能,可以用来实现对系统内存的原子访问。为了支持对一
个共享的虚拟内存页的原子操作,这样的设备需要对该页的访问是排他的,而不是来自CPU
的任何用户空间访问。 ``make_device_exclusive()`` 函数可以用来使一
个内存范围不能从用户空间访问。
这将用特殊的交换条目替换给定范围内的所有页的映射。任何试图访问交换条目的行为都会
导致一个异常,该异常会通过用原始映射替换该条目而得到恢复。驱动程序会被通知映射已
经被MMU通知器改变,之后它将不再有对该页的独占访问。独占访问被保证持续到驱动程序
放弃页面锁和页面引用为止,这时页面上的任何CPU异常都可以按所述进行。
内存 cgroup (memcg) 和 rss 统计
===============================
目前,设备内存被视为 rss 计数器中的任何常规页面(如果设备页面用于匿名,则为匿名,
如果设备页面用于文件支持页面,则为文件,如果设备页面用于共享内存,则为 shmem)。
这是为了保持现有应用程序的故意选择,这些应用程序可能在不知情的情况下开始使用设备
内存,运行不受影响。
一个缺点是 OOM 杀手可能会杀死使用大量设备内存而不是大量常规系统内存的应用程序,
因此不会释放太多系统内存。在决定以不同方式计算设备内存之前,我们希望收集更多关
于应用程序和系统在存在设备内存的情况下在内存压力下如何反应的实际经验。
对内存 cgroup 做出了相同的决定。设备内存页面根据相同的内存 cgroup 计算,常规
页面将被计算在内。这确实简化了进出设备内存的迁移。这也意味着从设备内存迁移回常规
内存不会失败,因为它会超过内存 cgroup 限制。一旦我们对设备内存的使用方式及其对
内存资源控制的影响有了更多的了解,我们可能会在后面重新考虑这个选择。
请注意,设备内存永远不能由设备驱动程序或通过 GUP 固定,因此此类内存在进程退出时
总是被释放的。或者在共享内存或文件支持内存的情况下,当删除最后一个引用时。
@@ -0,0 +1,437 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/hugetlbfs_reserv.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
==============
Hugetlbfs 预留
==============
概述
====
Documentation/admin-guide/mm/hugetlbpage.rst
中描述的巨页通常是预先分配给应用程序使用的 。如果VMA指
示要使用巨页,这些巨页会在缺页异常时被实例化到任务的地址空间。如果在缺页异常
时没有巨页存在,任务就会被发送一个SIGBUS,并经常不高兴地死去。在加入巨页支
持后不久,人们决定,在mmap()时检测巨页的短缺情况会更好。这个想法是,如果
没有足够的巨页来覆盖映射,mmap()将失败。这首先是在mmap()时在代码中做一个
简单的检查,以确定是否有足够的空闲巨页来覆盖映射。就像内核中的大多数东西一
样,代码随着时间的推移而不断发展。然而,基本的想法是在mmap()时 “预留”
巨页,以确保巨页可以用于该映射中的缺页异常。下面的描述试图描述在v4.10内核
中是如何进行巨页预留处理的。
读者
====
这个描述主要是针对正在修改hugetlbfs代码的内核开发者。
数据结构
========
resv_huge_pages
这是一个全局的(per-hstate)预留的巨页的计数。预留的巨页只对预留它们的任
务可用。因此,一般可用的巨页的数量被计算为(``free_huge_pages - resv_huge_pages``)。
Reserve Map
预留映射由以下结构体描述::
struct resv_map {
struct kref refs;
spinlock_t lock;
struct list_head regions;
long adds_in_progress;
struct list_head region_cache;
long region_cache_count;
};
系统中每个巨页映射都有一个预留映射。resv_map中的regions列表描述了映射中的
区域。一个区域被描述为::
struct file_region {
struct list_head link;
long from;
long to;
};
file_region结构体的 ‘from’ 和 ‘to’ 字段是进入映射的巨页索引。根据映射的类型,在
reserv_map 中的一个区域可能表示该范围存在预留,或预留不存在。
Flags for MAP_PRIVATE Reservations
这些被存储在预留的映射指针的底部。
``#define HPAGE_RESV_OWNER (1UL << 0)``
表示该任务是与该映射相关的预留的所有者。
``#define HPAGE_RESV_UNMAPPED (1UL << 1)``
表示最初映射此范围(并创建储备)的任务由于COW失败而从该任务(子任务)中取消映
射了一个页面。
Page Flags
PagePrivate页面标志是用来指示在释放巨页时必须恢复巨页的预留。更多细节将在
“释放巨页” 一节中讨论。
预留映射位置(私有或共享)
==========================
一个巨页映射或段要么是私有的,要么是共享的。如果是私有的,它通常只对一个地址空间
(任务)可用。如果是共享的,它可以被映射到多个地址空间(任务)。对于这两种类型的映射,
预留映射的位置和语义是明显不同的。位置的差异是:
- 对于私有映射,预留映射挂在VMA结构体上。具体来说,就是vma->vm_private_data。这个保
留映射是在创建映射(mmap(MAP_PRIVATE))时创建的。
- 对于共享映射,预留映射挂在inode上。具体来说,就是inode->i_mapping->private_data。
由于共享映射总是由hugetlbfs文件系统中的文件支持,hugetlbfs代码确保每个节点包含一个预
留映射。因此,预留映射在创建节点时被分配。
创建预留
========
当创建一个巨大的有页面支持的共享内存段(shmget(SHM_HUGETLB))或通过mmap(MAP_HUGETLB)
创建一个映射时,就会创建预留。这些操作会导致对函数hugetlb_reserve_pages()的调用::
int hugetlb_reserve_pages(struct inode *inode,
long from, long to,
struct vm_area_struct *vma,
vm_flags_t vm_flags)
hugetlb_reserve_pages()做的第一件事是检查在调用shmget()或mmap()时是否指定了NORESERVE
标志。如果指定了NORESERVE,那么这个函数立即返回,因为不需要预留。
参数'from'和'to'是映射或基础文件的巨页索引。对于shmget(),'from'总是0,'to'对应于段/映射
的长度。对于mmap(),offset参数可以用来指定进入底层文件的偏移量。在这种情况下,'from'和'to'
参数已经被这个偏移量所调整。
PRIVATE和SHARED映射之间的一个很大的区别是预留在预留映射中的表示方式。
- 对于共享映射,预留映射中的条目表示对应页面的预留存在或曾经存在。当预留被消耗时,预留映射不被
修改。
- 对于私有映射,预留映射中没有条目表示相应页面存在预留。随着预留被消耗,条目被添加到预留映射中。
因此,预留映射也可用于确定哪些预留已被消耗。
对于私有映射,hugetlb_reserve_pages()创建预留映射并将其挂在VMA结构体上。此外,
HPAGE_RESV_OWNER标志被设置,以表明该VMA拥有预留。
预留映射被查阅以确定当前映射/段需要多少巨页预留。对于私有映射,这始终是一个值(to - from)。
然而,对于共享映射来说,一些预留可能已经存在于(to - from)的范围内。关于如何实现这一点的细节,
请参见 :ref:`预留映射的修改 <resv_map_modifications>` 一节。
该映射可能与一个子池(subpool)相关联。如果是这样,将查询子池以确保有足够的空间用于映射。子池
有可能已经预留了可用于映射的预留空间。更多细节请参见 :ref: `子池预留 <sub_pool_resv>`
一节。
在咨询了预留映射和子池之后,就知道了需要的新预留数量。hugetlb_acct_memory()函数被调用以检查
并获取所要求的预留数量。hugetlb_acct_memory()调用到可能分配和调整剩余页数的函数。然而,在这
些函数中,代码只是检查以确保有足够的空闲的巨页来容纳预留。如果有的话,全局预留计数resv_huge_pages
会被调整,如下所示::
if (resv_needed <= (resv_huge_pages - free_huge_pages))
resv_huge_pages += resv_needed;
注意,在检查和调整这些计数器时,全局锁hugetlb_lock会被预留。
如果有足够的空闲的巨页,并且全局计数resv_huge_pages被调整,那么与映射相关的预留映射被修改以
反映预留。在共享映射的情况下,将存在一个file_region,包括'from'-'to'范围。对于私有映射,
不对预留映射进行修改,因为没有条目表示存在预留。
如果hugetlb_reserve_pages()成功,全局预留数和与映射相关的预留映射将根据需要被修改,以确保
在'from'-'to'范围内存在预留。
消耗预留/分配一个巨页
===========================
当与预留相关的巨页在相应的映射中被分配和实例化时,预留就被消耗了。该分配是在函数alloc_hugetlb_folio()
中进行的::
struct folio *alloc_hugetlb_folio(struct vm_area_struct *vma,
unsigned long addr, int avoid_reserve)
alloc_hugetlb_folio被传递给一个VMA指针和一个虚拟地址,因此它可以查阅预留映射以确定是否存在预留。
此外,alloc_hugetlb_folio需要一个参数avoid_reserve,该参数表示即使看起来已经为指定的地址预留了
预留,也不应该使用预留。avoid_reserve参数最常被用于写时拷贝和页面迁移的情况下,即现有页面的额
外拷贝被分配。
调用辅助函数vma_needs_reservation()来确定是否存在对映射(vma)中地址的预留。关于这个函数的详
细内容,请参见 :ref:`预留映射帮助函数 <resv_map_helpers>` 一节。从
vma_needs_reservation()返回的值通常为0或1。如果该地址存在预留,则为0,如果不存在预留,则为1。
如果不存在预留,并且有一个与映射相关联的子池,则查询子池以确定它是否包含预留。如果子池包含预留,
则可将其中一个用于该分配。然而,在任何情况下,avoid_reserve参数都会优先考虑为分配使用预留。在
确定预留是否存在并可用于分配后,调用dequeue_huge_page_vma()函数。这个函数需要两个与预留有关
的参数:
- avoid_reserve,这是传递给alloc_hugetlb_folio()的同一个值/参数。
- chg,尽管这个参数的类型是long,但只有0或1的值被传递给dequeue_huge_page_vma。如果该值为0,
则表明存在预留(关于可能的问题,请参见 “预留和内存策略” 一节)。如果值
为1,则表示不存在预留,如果可能的话,必须从全局空闲池中取出该页。
与VMA的内存策略相关的空闲列表被搜索到一个空闲页。如果找到了一个页面,当该页面从空闲列表中移除时,
free_huge_pages的值被递减。如果有一个与该页相关的预留,将进行以下调整::
SetPagePrivate(page); /* 表示分配这个页面消耗了一个预留,
* 如果遇到错误,以至于必须释放这个页面,预留将被
* 恢复。 */
resv_huge_pages--; /* 减少全局预留计数 */
注意,如果找不到满足VMA内存策略的巨页,将尝试使用伙伴分配器分配一个。这就带来了超出预留范围
的剩余巨页和超额分配的问题。即使分配了一个多余的页面,也会进行与上面一样的基于预留的调整:
SetPagePrivate(page) 和 resv_huge_pages--.
在获得一个新的巨页后,(folio)->_hugetlb_subpool被设置为与该页面相关的子池的值,如果它存在的话。当页
面被释放时,这将被用于子池的计数。
然后调用函数vma_commit_reservation(),根据预留的消耗情况调整预留映射。一般来说,这涉及
到确保页面在区域映射的file_region结构体中被表示。对于预留存在的共享映射,预留映射中的条目
已经存在,所以不做任何改变。然而,如果共享映射中没有预留,或者这是一个私有映射,则必须创建一
个新的条目。
注意,如果找不到满足VMA内存策略的巨页,将尝试使用伙伴分配器分配一个。这就带来了超出预留范围
的剩余巨页和过度分配的问题。即使分配了一个多余的页面,也会进行与上面一样的基于预留的调整。
SetPagePrivate(page)和resv_huge_pages-。
在获得一个新的巨页后,(page)->private被设置为与该页面相关的子池的值,如果它存在的话。当页
面被释放时,这将被用于子池的计数。
然后调用函数vma_commit_reservation(),根据预留的消耗情况调整预留映射。一般来说,这涉及
到确保页面在区域映射的file_region结构体中被表示。对于预留存在的共享映射,预留映射中的条目
已经存在,所以不做任何改变。然而,如果共享映射中没有预留,或者这是一个私有映射,则必须创建
一个新的条目。
在alloc_hugetlb_folio()开始调用vma_needs_reservation()和页面分配后调用
vma_commit_reservation()之间,预留映射有可能被改变。如果hugetlb_reserve_pages在共
享映射中为同一页面被调用,这将是可能的。在这种情况下,预留计数和子池空闲页计数会有一个偏差。
这种罕见的情况可以通过比较vma_needs_reservation和vma_commit_reservation的返回值来
识别。如果检测到这种竞争,子池和全局预留计数将被调整以进行补偿。关于这些函数的更多信息,请
参见 :ref:`预留映射帮助函数 <resv_map_helpers>` 一节。
实例化巨页
==========
在巨页分配之后,页面通常被添加到分配任务的页表中。在此之前,共享映射中的页面被添加到页面缓
存中,私有映射中的页面被添加到匿名反向映射中。在这两种情况下,PagePrivate标志被清除。因此,
当一个已经实例化的巨页被释放时,不会对全局预留计数(resv_huge_pages)进行调整。
释放巨页
========
巨页释放是由函数free_huge_folio()执行的。这个函数是hugetlbfs复合页的析构器。因此,它只传
递一个指向页面结构体的指针。当一个巨页被释放时,可能需要进行预留计算。如果该页与包含保
留的子池相关联,或者该页在错误路径上被释放,必须恢复全局预留计数,就会出现这种情况。
page->private字段指向与该页相关的任何子池。如果PagePrivate标志被设置,它表明全局预留计数
应该被调整(关于如何设置这些标志的信息,请参见
:ref: `消耗预留/分配一个巨页 <consume_resv>` )。
该函数首先调用hugepage_subpool_put_pages()来处理该页。如果这个函数返回一个0的值(不等于
传递的1的值),它表明预留与子池相关联,这个新释放的页面必须被用来保持子池预留的数量超过最小值。
因此,在这种情况下,全局resv_huge_pages计数器被递增。
如果页面中设置了PagePrivate标志,那么全局resv_huge_pages计数器将永远被递增。
子池预留
========
有一个结构体hstate与每个巨页尺寸相关联。hstate跟踪所有指定大小的巨页。一个子池代表一
个hstate中的页面子集,它与一个已挂载的hugetlbfs文件系统相关
当一个hugetlbfs文件系统被挂载时,可以指定min_size选项,它表示文件系统所需的最小的巨页数量。
如果指定了这个选项,与min_size相对应的巨页的数量将被预留给文件系统使用。这个数字在结构体
hugepage_subpool的min_hpages字段中被跟踪。在挂载时,hugetlb_acct_memory(min_hpages)
被调用以预留指定数量的巨页。如果它们不能被预留,挂载就会失败。
当从子池中获取或释放页面时,会调用hugepage_subpool_get/put_pages()函数。
hugepage_subpool_get/put_pages被传递给巨页数量,以此来调整子池的 “已用页面” 计数
(get为下降,put为上升)。通常情况下,如果子池中没有足够的页面,它们会返回与传递的相同的值或
一个错误。
然而,如果预留与子池相关联,可能会返回一个小于传递值的返回值。这个返回值表示必须进行的额外全局
池调整的数量。例如,假设一个子池包含3个预留的巨页,有人要求5个。与子池相关的3个预留页可以用来
满足部分请求。但是,必须从全局池中获得2个页面。为了向调用者转达这一信息,将返回值2。然后,调用
者要负责从全局池中获取另外两个页面。
COW和预留
==========
由于共享映射都指向并使用相同的底层页面,COW最大的预留问题是私有映射。在这种情况下,两个任务可
以指向同一个先前分配的页面。一个任务试图写到该页,所以必须分配一个新的页,以便每个任务都指向它
自己的页。
当该页最初被分配时,该页的预留被消耗了。当由于COW而试图分配一个新的页面时,有可能没有空闲的巨
页,分配会失败。
当最初创建私有映射时,通过设置所有者的预留映射指针中的HPAGE_RESV_OWNER位来标记映射的所有者。
由于所有者创建了映射,所有者拥有与映射相关的所有预留。因此,当一个写异常发生并且没有可用的页面
时,对预留的所有者和非所有者采取不同的行动。
在发生异常的任务不是所有者的情况下,异常将失败,该任务通常会收到一个SIGBUS。
如果所有者是发生异常的任务,我们希望它能够成功,因为它拥有原始的预留。为了达到这个目的,该页被
从非所有者任务中解映射出来。这样一来,唯一的引用就是来自拥有者的任务。此外,HPAGE_RESV_UNMAPPED
位被设置在非拥有任务的预留映射指针中。如果非拥有者任务后来在一个不存在的页面上发生异常,它可能
会收到一个SIGBUS。但是,映射/预留的原始拥有者的行为将与预期一致。
预留映射的修改
==============
以下低级函数用于对预留映射进行修改。通常情况下,这些函数不会被直接调用。而是调用一个预留映射辅
助函数,该函数调用这些低级函数中的一个。这些低级函数在源代码(mm/hugetlb.c)中得到了相当好的
记录。这些函数是::
long region_chg(struct resv_map *resv, long f, long t);
long region_add(struct resv_map *resv, long f, long t);
void region_abort(struct resv_map *resv, long f, long t);
long region_count(struct resv_map *resv, long f, long t);
在预留映射上的操作通常涉及两个操作:
1) region_chg()被调用来检查预留映射,并确定在指定的范围[f, t]内有多少页目前没有被代表。
调用代码执行全局检查和分配,以确定是否有足够的巨页使操作成功。
2)
a) 如果操作能够成功,region_add()将被调用,以实际修改先前传递给region_chg()的相同范围
[f, t]的预留映射。
b) 如果操作不能成功,region_abort被调用,在相同的范围[f, t]内中止操作。
注意,这是一个两步的过程, region_add()和 region_abort()在事先调用 region_chg()后保证
成功。 region_chg()负责预先分配任何必要的数据结构以确保后续操作(特别是 region_add())的
成功。
如上所述,region_chg()确定该范围内当前没有在映射中表示的页面的数量。region_add()返回添加
到映射中的范围内的页数。在大多数情况下, region_add() 的返回值与 region_chg() 的返回值相
同。然而,在共享映射的情况下,有可能在调用 region_chg() 和 region_add() 之间对预留映射进
行更改。在这种情况下,region_add()的返回值将与region_chg()的返回值不符。在这种情况下,全局计数
和子池计数很可能是不正确的,需要调整。检查这种情况并进行适当的调整是调用者的责任。
函数region_del()被调用以从预留映射中移除区域。
它通常在以下情况下被调用:
- 当hugetlbfs文件系统中的一个文件被删除时,该节点将被释放,预留映射也被释放。在释放预留映射
之前,所有单独的file_region结构体必须被释放。在这种情况下,region_del的范围是[0, LONG_MAX]。
- 当一个hugetlbfs文件正在被截断时。在这种情况下,所有在新文件大小之后分配的页面必须被释放。
此外,预留映射中任何超过新文件大小的file_region条目必须被删除。在这种情况下,region_del
的范围是[new_end_of_file, LONG_MAX]。
- 当在一个hugetlbfs文件中打洞时。在这种情况下,巨页被一次次从文件的中间移除。当这些页被移除
时,region_del()被调用以从预留映射中移除相应的条目。在这种情况下,region_del被传递的范
围是[page_idx, page_idx + 1]。
在任何情况下,region_del()都会返回从预留映射中删除的页面数量。在非常罕见的情况下,region_del()
会失败。这只能发生在打洞的情况下,即它必须分割一个现有的file_region条目,而不能分配一个新的
结构体。在这种错误情况下,region_del()将返回-ENOMEM。这里的问题是,预留映射将显示对该页有
预留。然而,子池和全局预留计数将不反映该预留。为了处理这种情况,调用函数hugetlb_fix_reserve_counts()
来调整计数器,使其与不能被删除的预留映射条目相对应。
region_count()在解除私有巨页映射时被调用。在私有映射中,预留映射中没有条目表明存在一个预留。
因此,通过计算预留映射中的条目数,我们知道有多少预留被消耗了,有多少预留是未完成的
(Outstanding = (end - start) - region_count(resv, start, end))。由于映射正在消
失,子池和全局预留计数被未完成的预留数量所减去。
预留映射帮助函数
================
有几个辅助函数可以查询和修改预留映射。这些函数只对特定的巨页的预留感兴趣,所以它们只是传入一个
地址而不是一个范围。此外,它们还传入相关的VMA。从VMA中,可以确定映射的类型(私有或共享)和预留
映射的位置(inode或VMA)。这些函数只是调用 “预留映射的修改” 一节中描述的基础函数。然而,
它们确实考虑到了私有和共享映射的预留映射条目的 “相反” 含义,并向调用者隐藏了这个细节::
long vma_needs_reservation(struct hstate *h,
struct vm_area_struct *vma,
unsigned long addr)
该函数为指定的页面调用 region_chg()。如果不存在预留,则返回1。如果存在预留,则返回0::
long vma_commit_reservation(struct hstate *h,
struct vm_area_struct *vma,
unsigned long addr)
这将调用 region_add(),用于指定的页面。与region_chg和region_add的情况一样,该函数应在
先前调用的vma_needs_reservation后调用。它将为该页添加一个预留条目。如果预留被添加,它将
返回1,如果没有则返回0。返回值应与之前调用vma_needs_reservation的返回值进行比较。如果出
现意外的差异,说明在两次调用之间修改了预留映射::
void vma_end_reservation(struct hstate *h,
struct vm_area_struct *vma,
unsigned long addr)
这将调用指定页面的 region_abort()。与region_chg和region_abort的情况一样,该函数应在
先前调用的vma_needs_reservation后被调用。它将中止/结束正在进行的预留添加操作::
long vma_add_reservation(struct hstate *h,
struct vm_area_struct *vma,
unsigned long addr)
这是一个特殊的包装函数,有助于在错误路径上清理预留。它只从repare_reserve_on_error()函数
中调用。该函数与vma_needs_reservation一起使用,试图将一个预留添加到预留映射中。它考虑到
了私有和共享映射的不同预留映射语义。因此,region_add被调用用于共享映射(因为映射中的条目表
示预留),而region_del被调用用于私有映射(因为映射中没有条目表示预留)。关于在错误路径上需
要做什么的更多信息,请参见 “错误路径中的预留清理” 。
错误路径中的预留清理
====================
正如在:ref:`预留映射帮助函数<resv_map_helpers>` 一节中提到的,预留的修改分两步进行。首
先,在分配页面之前调用vma_needs_reservation。如果分配成功,则调用vma_commit_reservation。
如果不是,则调用vma_end_reservation。全局和子池的预留计数根据操作的成功或失败进行调整,
一切都很好。
此外,在一个巨页被实例化后,PagePrivate标志被清空,这样,当页面最终被释放时,计数是
正确的。
然而,有几种情况是,在一个巨页被分配后,但在它被实例化之前,就遇到了错误。在这种情况下,
页面分配已经消耗了预留,并进行了适当的子池、预留映射和全局计数调整。如果页面在这个时候被释放
(在实例化和清除PagePrivate之前),那么free_huge_folio将增加全局预留计数。然而,预留映射
显示报留被消耗了。这种不一致的状态将导致预留的巨页的 “泄漏” 。全局预留计数将比它原本的要高,
并阻止分配一个预先分配的页面。
函数 restore_reserve_on_error() 试图处理这种情况。它有相当完善的文档。这个函数的目的
是将预留映射恢复到页面分配前的状态。通过这种方式,预留映射的状态将与页面释放后的全局预留计
数相对应。
函数restore_reserve_on_error本身在试图恢复预留映射条目时可能会遇到错误。在这种情况下,
它将简单地清除该页的PagePrivate标志。这样一来,当页面被释放时,全局预留计数将不会被递增。
然而,预留映射将继续看起来像预留被消耗了一样。一个页面仍然可以被分配到该地址,但它不会像最
初设想的那样使用一个预留页。
有一些代码(最明显的是userfaultfd)不能调用restore_reserve_on_error。在这种情况下,
它简单地修改了PagePrivate,以便在释放巨页时不会泄露预留。
预留和内存策略
==============
当git第一次被用来管理Linux代码时,每个节点的巨页列表就存在于hstate结构中。预留的概念是
在一段时间后加入的。当预留被添加时,没有尝试将内存策略考虑在内。虽然cpusets与内存策略不
完全相同,但hugetlb_acct_memory中的这个注释总结了预留和cpusets/内存策略之间的相互作
用::
/*
* 当cpuset被配置时,它打破了严格的hugetlb页面预留,因为计数是在一个全局变量上完
* 成的。在有cpuset的情况下,这样的预留完全是垃圾,因为预留没有根据当前cpuset的
* 页面可用性来检查。在任务所在的cpuset中缺乏空闲的htlb页面时,应用程序仍然有可能
* 被内核OOM'ed。试图用cpuset来执行严格的计数几乎是不可能的(或者说太难看了),因
* 为cpuset太不稳定了,任务或内存节点可以在cpuset之间动态移动。与cpuset共享
* hugetlb映射的语义变化是不可取的。然而,为了预留一些语义,我们退回到检查当前空闲
* 页的可用性,作为一种最好的尝试,希望能将cpuset改变语义的影响降到最低。
*/
添加巨页预留是为了防止在缺页异常时出现意外的页面分配失败(OOM)。然而,如果一个应用
程序使用cpusets或内存策略,就不能保证在所需的节点上有巨页可用。即使有足够数量的全局
预留,也是如此。
Hugetlbfs回归测试
=================
最完整的hugetlb测试集在libhugetlbfs仓库。如果你修改了任何hugetlb相关的代码,请使用
libhugetlbfs测试套件来检查回归情况。此外,如果你添加了任何新的hugetlb功能,请在
libhugetlbfs中添加适当的测试。
--
Mike Kravetz,2017年4月7日
@@ -0,0 +1,166 @@
:Original: Documentation/mm/hwpoison.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
hwpoison
========
什么是hwpoison?
===============
即将推出的英特尔CPU支持从一些内存错误中恢复( ``MCA恢复`` )。这需要操作系统宣布
一个页面"poisoned",杀死与之相关的进程,并避免在未来使用它。
这个补丁包在虚拟机中实现了必要的(编程)框架。
引用概述中的评论::
高级机器的检查与处理。处理方法是损坏的页面被硬件报告,通常是由于2位ECC内
存或高速缓存故障。
这主要是针对在后台检测到的损坏的页面。当当前的CPU试图访问它时,当前运行的进程
可以直接被杀死。因为还没有访问损坏的页面, 如果错误由于某种原因不能被处理,就可
以安全地忽略它. 而不是用另外一个机器检查去处理它。
处理不同状态的页面缓存页。这里棘手的部分是,相对于其他虚拟内存用户, 我们可以异
步访问任何页面。因为内存故障可能随时随地发生,可能违反了他们的一些假设。这就是
为什么这段代码必须非常小心。一般来说,它试图使用正常的锁规则,如获得标准锁,即使
这意味着错误处理可能需要很长的时间。
这里的一些操作有点低效,并且具有非线性的算法复杂性,因为数据结构没有针对这种情
况进行优化。特别是从vma到进程的映射就是这种情况。由于这种情况大概率是罕见的,所
以我们希望我们可以摆脱这种情况。
该代码由mm/memory-failure.c中的高级处理程序、一个新的页面poison位和虚拟机中的
各种检查组成,用来处理poison的页面。
现在主要目标是KVM客户机,但它适用于所有类型的应用程序。支持KVM需要最近的qemu-kvm
版本。
对于KVM的使用,需要一个新的信号类型,这样KVM就可以用适当的地址将机器检查注入到客户
机中。这在理论上也允许其他应用程序处理内存故障。我们的期望是,所有的应用程序都不要这
样做,但一些非常专业的应用程序可能会这样做。
故障恢复模式
============
有两种(实际上是三种)模式的内存故障恢复可以在。
vm.memory_failure_recovery sysctl 置零:
所有的内存故障都会导致panic。请不要尝试恢复。
早期处理
(可以在全局和每个进程中控制) 一旦检测到错误,立即向应用程序发送SIGBUS这允许
应用程序以温和的方式处理内存错误(例如,放弃受影响的对象) 这是KVM qemu使用的
模式。
推迟处理
当应用程序运行到损坏的页面时,发送SIGBUS。这对不知道内存错误的应用程序来说是
最好的,默认情况下注意一些页面总是被当作late kill处理。
用户控制
========
vm.memory_failure_recovery
参阅 sysctl.txt
vm.memory_failure_early_kill
全局启用early kill
PR_MCE_KILL
设置early/late kill mode/revert 到系统默认值。
arg1: PR_MCE_KILL_CLEAR:
恢复到系统默认值
arg1: PR_MCE_KILL_SET:
arg2定义了线程特定模式
PR_MCE_KILL_EARLY:
Early kill
PR_MCE_KILL_LATE:
Late kill
PR_MCE_KILL_DEFAULT
使用系统全局默认值
注意,如果你想有一个专门的线程代表进程处理SIGBUS(BUS_MCEERR_AO),你应该在
指定线程上调用prctl(PR_MCE_KILL_EARLY)。否则,SIGBUS将被发送到主线程。
PR_MCE_KILL_GET
返回当前模式
测试
====
* madvise(MADV_HWPOISON, ....) (as root) - 在测试过程中Poison一个页面
* 通过debugfs ``/sys/kernel/debug/hwpoison/`` hwpoison-inject模块
corrupt-pfn
在PFN处注入hwpoison故障,并echoed到这个文件。这做了一些早期过滤,以避
免在测试套件中损坏非预期页面。
unpoison-pfn
在PFN的Software-unpoison页面对应到这个文件。这样,一个页面可以再次被
复用。这只对Linux注入的故障起作用,对真正的内存故障不起作用。
注意这些注入接口并不稳定,可能会在不同的内核版本中发生变化
corrupt-filter-dev-major, corrupt-filter-dev-minor
只处理与块设备major/minor定义的文件系统相关的页面的内存故障。-1U是通
配符值。这应该只用于人工注入的测试。
corrupt-filter-memcg
限制注入到memgroup拥有的页面。由memcg的inode号指定。
Example::
mkdir /sys/fs/cgroup/mem/hwpoison
usemem -m 100 -s 1000 &
echo `jobs -p` > /sys/fs/cgroup/mem/hwpoison/tasks
memcg_ino=$(ls -id /sys/fs/cgroup/mem/hwpoison | cut -f1 -d' ')
echo $memcg_ino > /debug/hwpoison/corrupt-filter-memcg
page-types -p `pidof init` --hwpoison # shall do nothing
page-types -p `pidof usemem` --hwpoison # poison its pages
corrupt-filter-flags-mask, corrupt-filter-flags-value
当指定时,只有在((page_flags & mask) == value)的情况下才会poison页面。
这允许对许多种类的页面进行压力测试。page_flags与/proc/kpageflags中的相
同。这些标志位在include/linux/kernel-page-flags.h中定义,并在
Documentation/admin-guide/mm/pagemap.rst中记录。
* 架构特定的MCE注入器
x86 有 mce-inject, mce-test
在mce-test中的一些便携式hwpoison测试程序,见下文。
引用
====
http://halobates.de/mce-lc09-2.pdf
09年LinuxCon的概述演讲
git://git.kernel.org/pub/scm/utils/cpu/mce/mce-test.git
测试套件(在tsrc中的hwpoison特定可移植测试)。
git://git.kernel.org/pub/scm/utils/cpu/mce/mce-inject.git
x86特定的注入器
限制
====
- 不是所有的页面类型都被支持,而且永远不会。大多数内核内部对象不能被恢
复,目前只有LRU页。
---
Andi Kleen, 2009年10月
@@ -0,0 +1,69 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
=================
Linux内存管理文档
=================
这是一份关于了解Linux的内存管理子系统的指南。如果你正在寻找关于简单分配内存的
建议,请参阅内存分配指南
(Documentation/translations/zh_CN/core-api/memory-allocation.rst)。
关于控制和调整的指南,请看管理指南
(Documentation/translations/zh_CN/admin-guide/mm/index.rst)。
.. toctree::
:maxdepth: 1
highmem
该处剩余文档待原始文档有内容后翻译。
遗留文档
========
这是一个关于Linux内存管理(MM)子系统内部的旧文档的集合,其中有不同层次的细节,
包括注释和邮件列表的回复,用于阐述数据结构和算法的描述。它应该被很好地整合到上述
结构化的文档中,如果它已经完成了它的使命,可以删除。
.. toctree::
:maxdepth: 1
active_mm
balance
damon/index
free_page_reporting
ksm
hmm
hwpoison
hugetlbfs_reserv
memory-model
mmu_notifier
numa
overcommit-accounting
page_frags
page_migration
page_owner
page_table_check
page_tables
physical_memory
remap_file_pages
split_page_table_lock
vmalloced-kernel-stacks
zsmalloc
TODOLIST:
* arch_pgtable_helpers
* free_page_reporting
* hugetlbfs_reserv
* slub
* transhuge
* unevictable-lru
@@ -0,0 +1,70 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/ksm.rst
:翻译:
徐鑫 xu xin <xu.xin16@zte.com.cn>
============
内核同页合并
============
KSM 是一种节省内存的数据去重功能,由CONFIG_KSM=y启用,并在2.6.32版本时被添加
到Linux内核。详见 ``mm/ksm.c`` 的实现,以及http://lwn.net/Articles/306704和
https://lwn.net/Articles/330589
KSM的用户空间的接口在Documentation/translations/zh_CN/admin-guide/mm/ksm.rst
文档中有描述。
设计
====
概述
----
概述内容请见mm/ksm.c文档中的“DOC: Overview”
逆映射
------
KSM维护着稳定树中的KSM页的逆映射信息。
当KSM页面的共享数小于 ``max_page_sharing`` 的虚拟内存区域(VMAs)时,则代表了
KSM页的稳定树其中的节点指向了一个ksm_rmap_item结构体类型的列表。同时,这个KSM页
的 ``page->mapping`` 指向了该稳定树节点。
如果共享数超过了阈值,KSM将给稳定树添加第二个维度。稳定树就变成链接一个或多
个稳定树"副本"的"链"。每个副本都保留KSM页的逆映射信息,其中 ``page->mapping``
指向该"副本"。
每个链以及链接到该链中的所有"副本"强制不变的是,它们代表了相同的写保护内存
内容,尽管任中一个"副本"是由同一片内存区的不同的KSM复制页所指向的。
这样一来,相比与无限的逆映射链表,稳定树的查找计算复杂性不受影响。但在稳定树
本身中不能有重复的KSM页面内容仍然是强制要求。
由 ``max_page_sharing`` 强制决定的数据去重限制是必要的,以此来避免虚拟内存
rmap链表变得过大。rmap的遍历具有O(N)的复杂度,其中N是共享页面的rmap_项(即
虚拟映射)的数量,而这个共享页面的节点数量又被 ``max_page_sharing`` 所限制。
因此,这有效地将线性O(N)计算复杂度从rmap遍历中分散到不同的KSM页面上。ksmd进
程在稳定节点"链"上的遍历也是O(N),但这个N是稳定树"副本"的数量,而不是rmap项
的数量,因此它对ksmd性能没有显著影响。实际上,最佳稳定树"副本"的候选节点将
保留在"副本"列表的开头。
``max_page_sharing`` 的值设置得高了会促使更快的内存合并(因为将有更少的稳定
树副本排队进入稳定节点chain->hlist)和更高的数据去重系数,但代价是在交换、压
缩、NUMA平衡和页面迁移过程中可能导致KSM页的最大rmap遍历速度较慢。
``stable_node_dups/stable_node_chains`` 的比值还受 ``max_page_sharing`` 调控
的影响,高比值可能意味着稳定节点dup中存在碎片,这可以通过在ksmd中引入碎片算
法来解决,该算法将rmap项从一个稳定节点dup重定位到另一个稳定节点dup,以便释放
那些仅包含极少rmap项的稳定节点"dup",但这可能会增加ksmd进程的CPU使用率,并可
能会减慢应用程序在KSM页面上的只读计算。
KSM会定期扫描稳定节点"链"中链接的所有稳定树"副本",以便删减过时了的稳定节点。
这种扫描的频率由 ``stable_node_chains_prune_millisecs`` 这个sysfs 接口定义。
参考
====
内核代码请见mm/ksm.c。
涉及的函数(mm_slot ksm_scan stable_node rmap_item)。
@@ -0,0 +1,133 @@
.. SPDX-License-Identifier: GPL-2.0
:Original: Documentation/mm/memory-model.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
============
物理内存模型
============
系统中的物理内存可以用不同的方式进行寻址。最简单的情况是,物理内存从地址0开
始,跨越一个连续的范围,直到最大的地址。然而,这个范围可能包含CPU无法访问的
小孔隙。那么,在完全不同的地址可能有几个连续的范围。而且,别忘了NUMA,即不
同的内存库连接到不同的CPU。
Linux使用两种内存模型中的一种对这种多样性进行抽象。FLATMEM和SPARSEM。每
个架构都定义了它所支持的内存模型,默认的内存模型是什么,以及是否有可能手动
覆盖该默认值。
所有的内存模型都使用排列在一个或多个数组中的 `struct page` 来跟踪物理页
帧的状态。
无论选择哪种内存模型,物理页框号(PFN)和相应的 `struct page` 之间都存
在一对一的映射关系。
每个内存模型都定义了 :c:func:`pfn_to_page` 和 :c:func:`page_to_pfn`
帮助函数,允许从PFN到 `struct page` 的转换,反之亦然。
FLATMEM
=======
最简单的内存模型是FLATMEM。这个模型适用于非NUMA系统的连续或大部分连续的
物理内存。
在FLATMEM内存模型中,有一个全局的 `mem_map` 数组来映射整个物理内存。对
于大多数架构,孔隙在 `mem_map` 数组中都有条目。与孔洞相对应的 `struct page`
对象从未被完全初始化。
为了分配 `mem_map` 数组,架构特定的设置代码应该调用free_area_init()函数。
然而,在调用memblock_free_all()函数之前,映射数组是不能使用的,该函数
将所有的内存交给页分配器。
一个架构可能会释放 `mem_map` 数组中不包括实际物理页的部分。在这种情况下,特
定架构的 :c:func:`pfn_valid` 实现应该考虑到 `mem_map` 中的孔隙。
使用FLATMEM,PFN和 `struct page` 之间的转换是直接的。 `PFN - ARCH_PFN_OFFSET`
是 `mem_map` 数组的一个索引。
`ARCH_PFN_OFFSET` 定义了物理内存起始地址不同于0的系统的第一个页框号。
SPARSEMEM
=========
SPARSEMEM是Linux中最通用的内存模型,它是唯一支持若干高级功能的内存模型,
如物理内存的热插拔、非易失性内存设备的替代内存图和较大系统的内存图的延迟
初始化。
SPARSEMEM模型将物理内存显示为一个部分的集合。一个区段用mem_section结构
体表示,它包含 `section_mem_map` ,从逻辑上讲,它是一个指向 `struct page`
阵列的指针。然而,它被存储在一些其他的magic中,以帮助分区管理。区段的大小
和最大区段数是使用 `SECTION_SIZE_BITS` 和 `MAX_PHYSMEM_BITS` 常量
来指定的,这两个常量是由每个支持SPARSEMEM的架构定义的。 `MAX_PHYSMEM_BITS`
是一个架构所支持的物理地址的实际宽度,而 `SECTION_SIZE_BITS` 是一个任
意的值。
最大的段数表示为 `NR_MEM_SECTIONS` ,定义为
.. math::
NR\_MEM\_SECTIONS = 2 ^ {(MAX\_PHYSMEM\_BITS - SECTION\_SIZE\_BITS)}
`mem_section` 对象被安排在一个叫做 `mem_sections` 的二维数组中。这个数组的
大小和位置取决于 `CONFIG_SPARSEM_EXTREME` 和可能的最大段数:
* 当 `CONFIG_SPARSEMEM_EXTREME` 被禁用时, `mem_sections` 数组是静态的,有
`NR_MEM_SECTIONS` 行。每一行持有一个 `mem_section` 对象。
* 当 `CONFIG_SPARSEMEM_EXTREME` 被启用时, `mem_sections` 数组被动态分配。
每一行包含价值 `PAGE_SIZE` 的 `mem_section` 对象,行数的计算是为了适应所有的
内存区。
通过SPARSEMEM,有两种可能的方式将PFN转换为相应的 `struct page` --"classic sparse"和
"sparse vmemmap"。选择是在构建时进行的,它由 `CONFIG_SPARSEMEM_VMEMMAP` 的
值决定。
Classic sparse在page->flags中编码了一个页面的段号,并使用PFN的高位来访问映射该页
框的段。在一个区段内,PFN是指向页数组的索引。
Sparse vmemmapvmemmap使用虚拟映射的内存映射来优化pfn_to_page和page_to_pfn操
作。有一个全局的 `struct page *vmemmap` 指针,指向一个虚拟连续的 `struct page`
对象阵列。PFN是该数组的一个索引,`struct page` 从 `vmemmap` 的偏移量是该页的PFN。
为了使用vmemmap,一个架构必须保留一个虚拟地址的范围,以映射包含内存映射的物理页,并
确保 `vmemmap`指向该范围。此外,架构应该实现 :c:func:`vmemmap_populate` 方法,
它将分配物理内存并为虚拟内存映射创建页表。如果一个架构对vmemmap映射没有任何特殊要求,
它可以使用通用内存管理提供的默认 :c:func:`vmemmap_populate_basepages`。
虚拟映射的内存映射允许将持久性内存设备的 `struct page` 对象存储在这些设备上预先分
配的存储中。这种存储用vmem_altmap结构表示,最终通过一长串的函数调用传递给
vmemmap_populate()。vmemmap_populate()实现可以使用 `vmem_altmap` 和
:c:func:`vmemmap_alloc_block_buf` 助手来分配持久性内存设备上的内存映射。
ZONE_DEVICE
===========
`ZONE_DEVICE` 设施建立在 `SPARSEM_VMEMMAP` 之上,为设备驱动识别的物理地址范
围提供 `struct page` `mem_map` 服务。 `ZONE_DEVICE` 的 "设备" 方面与以下
事实有关:这些地址范围的页面对象从未被在线标记过,而且必须对设备进行引用,而不仅仅
是页面,以保持内存被“锁定”以便使用。 `ZONE_DEVICE` ,通过 :c:func:`devm_memremap_pages` ,
为给定的pfns范围执行足够的内存热插拔来开启 :c:func:`pfn_to_page`,
:c:func:`page_to_pfn`, ,和 :c:func:`get_user_pages` 服务。由于页面引
用计数永远不会低于1,所以页面永远不会被追踪为空闲内存,页面的 `struct list_head lru`
空间被重新利用,用于向映射该内存的主机设备/驱动程序进行反向引用。
虽然 `SPARSEMEM` 将内存作为一个区段的集合,可以选择收集并合成内存块,但
`ZONE_DEVICE` 用户需要更小的颗粒度来填充 `mem_map` 。鉴于 `ZONE_DEVICE`
内存从未被在线标记,因此它的内存范围从未通过sysfs内存热插拔api暴露在内存块边界
上。这个实现依赖于这种缺乏用户接口的约束,允许子段大小的内存范围被指定给
:c:func:`arch_add_memory` ,即内存热插拔的上半部分。子段支持允许2MB作为
:c:func:`devm_memremap_pages` 的跨架构通用对齐颗粒度。
`ZONE_DEVICE` 的用户是:
* pmem: 通过DAX映射将平台持久性内存作为直接I/O目标使用。
* hmm: 用 `->page_fault()` 和 `->page_free()` 事件回调扩展 `ZONE_DEVICE` ,
以允许设备驱动程序协调与设备内存相关的内存管理事件,通常是GPU内存。参见Documentation/mm/hmm.rst。
* p2pdma: 创建 `struct page` 对象,允许PCI/E拓扑结构中的peer设备协调它们之间的
直接DMA操作,即绕过主机内存。
@@ -0,0 +1,97 @@
:Original: Documentation/mm/mmu_notifier.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
什么时候需要页表锁内通知?
==========================
当清除一个pte/pmd时,我们可以选择通过在页表锁下(通知版的\*_clear_flush调用
mmu_notifier_invalidate_range)通知事件。但这种通知并不是在所有情况下都需要的。
对于二级TLB(非CPU TLB),如IOMMU TLB或设备TLB(当设备使用类似ATS/PASID的东西让
IOMMU走CPU页表来访问进程的虚拟地址空间)。只有两种情况需要在清除pte/pmd时在持有页
表锁的同时通知这些二级TLB:
A) 在mmu_notifier_invalidate_range_end()之前,支持页的地址被释放。
B) 一个页表项被更新以指向一个新的页面(COW,零页上的写异常,__replace_page(),...)。
情况A很明显,你不想冒风险让设备写到一个现在可能被一些完全不同的任务使用的页面。
情况B更加微妙。为了正确起见,它需要按照以下序列发生:
- 上页表锁
- 清除页表项并通知 ([pmd/pte]p_huge_clear_flush_notify())
- 设置页表项以指向新页
如果在设置新的pte/pmd值之前,清除页表项之后没有进行通知,那么你就会破坏设备的C11或
C++11等内存模型。
考虑以下情况(设备使用类似于ATS/PASID的功能)。
两个地址addrA和addrB,这样|addrA - addrB| >= PAGE_SIZE,我们假设它们是COW的
写保护(B的其他情况也适用)。
::
[Time N] --------------------------------------------------------------------
CPU-thread-0 {尝试写到addrA}
CPU-thread-1 {尝试写到addrB}
CPU-thread-2 {}
CPU-thread-3 {}
DEV-thread-0 {读取addrA并填充设备TLB}
DEV-thread-2 {读取addrB并填充设备TLB}
[Time N+1] ------------------------------------------------------------------
CPU-thread-0 {COW_step0: {mmu_notifier_invalidate_range_start(addrA)}}
CPU-thread-1 {COW_step0: {mmu_notifier_invalidate_range_start(addrB)}}
CPU-thread-2 {}
CPU-thread-3 {}
DEV-thread-0 {}
DEV-thread-2 {}
[Time N+2] ------------------------------------------------------------------
CPU-thread-0 {COW_step1: {更新页表以指向addrA的新页}}
CPU-thread-1 {COW_step1: {更新页表以指向addrB的新页}}
CPU-thread-2 {}
CPU-thread-3 {}
DEV-thread-0 {}
DEV-thread-2 {}
[Time N+3] ------------------------------------------------------------------
CPU-thread-0 {preempted}
CPU-thread-1 {preempted}
CPU-thread-2 {写入addrA,这是对新页面的写入}
CPU-thread-3 {}
DEV-thread-0 {}
DEV-thread-2 {}
[Time N+3] ------------------------------------------------------------------
CPU-thread-0 {preempted}
CPU-thread-1 {preempted}
CPU-thread-2 {}
CPU-thread-3 {写入addrB,这是一个写入新页的过程}
DEV-thread-0 {}
DEV-thread-2 {}
[Time N+4] ------------------------------------------------------------------
CPU-thread-0 {preempted}
CPU-thread-1 {COW_step3: {mmu_notifier_invalidate_range_end(addrB)}}
CPU-thread-2 {}
CPU-thread-3 {}
DEV-thread-0 {}
DEV-thread-2 {}
[Time N+5] ------------------------------------------------------------------
CPU-thread-0 {preempted}
CPU-thread-1 {}
CPU-thread-2 {}
CPU-thread-3 {}
DEV-thread-0 {从旧页中读取addrA}
DEV-thread-2 {从新页面读取addrB}
所以在这里,因为在N+2的时候,清空页表项没有和通知一起作废二级TLB,设备在看到addrA的新值之前
就看到了addrB的新值。这就破坏了设备的总内存序。
当改变一个pte的写保护或指向一个新的具有相同内容的写保护页(KSM)时,将mmu_notifier_invalidate_range
调用延迟到页表锁外的mmu_notifier_invalidate_range_end()是可以的。即使做页表更新的线程
在释放页表锁后但在调用mmu_notifier_invalidate_range_end()前被抢占,也是如此。
@@ -0,0 +1,101 @@
:Original: Documentation/mm/numa.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
始于1999年11月,作者: <kanoj@sgi.com>
==========================
何为非统一内存访问(NUMA)?
==========================
这个问题可以从几个视角来回答:硬件观点和Linux软件视角。
从硬件角度看,NUMA系统是一个由多个组件或装配组成的计算机平台,每个组件可能包含0个或更多的CPU、
本地内存和/或IO总线。为了简洁起见,并将这些物理组件/装配的硬件视角与软件抽象区分开来,我们在
本文中称这些组件/装配为“单元”。
每个“单元”都可以看作是系统的一个SMP[对称多处理器]子集——尽管独立的SMP系统所需的一些组件可能
不会在任何给定的单元上填充。NUMA系统的单元通过某种系统互连连接在一起——例如,交叉开关或点对点
链接是NUMA系统互连的常见类型。这两种类型的互连都可以聚合起来,以创建NUMA平台,其中的单元与其
他单元有多个距离。
对于Linux,感兴趣的NUMA平台主要是所谓的缓存相干NUMA--简称ccNUMA系统系统。在ccNUMA系统中,
所有的内存都是可见的,并且可以从连接到任何单元的任何CPU中访问,缓存一致性是由处理器缓存和/或
系统互连在硬件中处理。
内存访问时间和有效的内存带宽取决于包含CPU的单元或进行内存访问的IO总线距离包含目标内存的单元
有多远。例如,连接到同一单元的CPU对内存的访问将比访问其他远程单元的内存经历更快的访问时间和
更高的带宽。 NUMA平台可以在任何给定单元上访问多种远程距离的(其他)单元。
平台供应商建立NUMA系统并不只是为了让软件开发人员的生活变得有趣。相反,这种架构是提供可扩展
内存带宽的一种手段。然而,为了实现可扩展的内存带宽,系统和应用软件必须安排大部分的内存引用
[cache misses]到“本地”内存——同一单元的内存,如果有的话——或者到最近的有内存的单元。
这就自然而然有了Linux软件对NUMA系统的视角:
Linux将系统的硬件资源划分为多个软件抽象,称为“节点”。Linux将节点映射到硬件平台的物理单元
上,对一些架构的细节进行了抽象。与物理单元一样,软件节点可能包含0或更多的CPU、内存和/或IO
总线。同样,对“较近”节点的内存访问——映射到较近单元的节点——通常会比对较远单元的访问经历更快
的访问时间和更高的有效带宽。
对于一些架构,如x86,Linux将“隐藏”任何代表没有内存连接的物理单元的节点,并将连接到该单元
的任何CPU重新分配到代表有内存的单元的节点上。因此,在这些架构上,我们不能假设Linux将所有
的CPU与一个给定的节点相关联,会看到相同的本地内存访问时间和带宽。
此外,对于某些架构,同样以x86为例,Linux支持对额外节点的仿真。对于NUMA仿真,Linux会将现
有的节点或者非NUMA平台的系统内存分割成多个节点。每个模拟的节点将管理底层单元物理内存的一部
分。NUMA仿真对于在非NUMA平台上测试NUMA内核和应用功能是非常有用的,当与cpusets一起使用时,
可以作为一种内存资源管理机制。[见 Documentation/admin-guide/cgroup-v1/cpusets.rst]
对于每个有内存的节点,Linux构建了一个独立的内存管理子系统,有自己的空闲页列表、使用中页列表、
使用统计和锁来调解访问。此外,Linux为每个内存区[DMA、DMA32、NORMAL、HIGH_MEMORY、MOVABLE
中的一个或多个]构建了一个有序的“区列表”。zonelist指定了当一个选定的区/节点不能满足分配请求
时要访问的区/节点。当一个区没有可用的内存来满足请求时,这种情况被称为“overflow 溢出”或
“fallback 回退”。
由于一些节点包含多个包含不同类型内存的区,Linux必须决定是否对区列表进行排序,使分配回退到不同
节点上的相同区类型,或同一节点上的不同区类型。这是一个重要的考虑因素,因为有些区,如DMA或DMA32,
代表了相对稀缺的资源。Linux选择了一个默认的Node ordered zonelist。这意味着在使用按NUMA距
离排序的远程节点之前,它会尝试回退到同一节点的其他分区。
默认情况下,Linux会尝试从执行请求的CPU被分配到的节点中满足内存分配请求。具体来说,Linux将试
图从请求来源的节点的适当分区列表中的第一个节点进行分配。这被称为“本地分配”。如果“本地”节点不能
满足请求,内核将检查所选分区列表中其他节点的区域,寻找列表中第一个能满足请求的区域。
本地分配将倾向于保持对分配的内存的后续访问 “本地”的底层物理资源和系统互连——只要内核代表其分配
一些内存的任务后来不从该内存迁移。Linux调度器知道平台的NUMA拓扑结构——体现在“调度域”数据结构
中[见 Documentation/scheduler/sched-domains.rst]——并且调度器试图尽量减少任务迁移到遥
远的调度域中。然而,调度器并没有直接考虑到任务的NUMA足迹。因此,在充分不平衡的情况下,任务可
以在节点之间迁移,远离其初始节点和内核数据结构。
系统管理员和应用程序设计者可以使用各种CPU亲和命令行接口,如taskset(1)和numactl(1),以及程
序接口,如sched_setaffinity(2),来限制任务的迁移,以改善NUMA定位。此外,人们可以使用
Linux NUMA内存策略修改内核的默认本地分配行为。 [见
Documentation/admin-guide/mm/numa_memory_policy.rst].
系统管理员可以使用控制组和CPUsets限制非特权用户在调度或NUMA命令和功能中可以指定的CPU和节点
的内存。 [见 Documentation/admin-guide/cgroup-v1/cpusets.rst]
在不隐藏无内存节点的架构上,Linux会在分区列表中只包括有内存的区域[节点]。这意味着对于一个无
内存的节点,“本地内存节点”——CPU节点的分区列表中的第一个区域的节点——将不是节点本身。相反,它
将是内核在建立分区列表时选择的离它最近的有内存的节点。所以,默认情况下,本地分配将由内核提供
最近的可用内存来完成。这是同一机制的结果,该机制允许这种分配在一个包含内存的节点溢出时回退到
其他附近的节点。
一些内核分配不希望或不能容忍这种分配回退行为。相反,他们想确保他们从指定的节点获得内存,或者
得到通知说该节点没有空闲内存。例如,当一个子系统分配每个CPU的内存资源时,通常是这种情况。
一个典型的分配模式是使用内核的numa_node_id()或CPU_to_node()函数获得“当前CPU”所在节点的
节点ID,然后只从返回的节点ID请求内存。当这样的分配失败时,请求的子系统可以恢复到它自己的回退
路径。板块内核内存分配器就是这样的一个例子。或者,子系统可以选择在分配失败时禁用或不启用自己。
内核分析子系统就是这样的一个例子。
如果架构支持——不隐藏无内存节点,那么连接到无内存节点的CPU将总是产生回退路径的开销,或者一些
子系统如果试图完全从无内存的节点分配内存,将无法初始化。为了透明地支持这种架构,内核子系统可
以使用numa_mem_id()或cpu_to_mem()函数来定位调用或指定CPU的“本地内存节点”。同样,这是同
一个节点,默认的本地页分配将从这个节点开始尝试。
@@ -0,0 +1,85 @@
:Original: Documentation/mm/overcommit-accounting.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
==============
超量使用审计
==============
Linux内核支持下列超量使用处理模式
0
启发式超量使用处理。拒绝明显的地址空间超量使用。用于一个典型的系统。
它确保严重的疯狂分配失败,同时允许超量使用以减少swap的使用。这是默认的。
1
总是超量使用。适用于一些科学应用。经典的例子是使用稀疏数组的代码,只是依赖
几乎完全由零页组成的虚拟内存
2
不超量使用。系统提交的总地址空间不允许超过swap+一个可配置的物理RAM的数量
(默认为50%)。根据你使用的数量,在大多数情况下,这意味着一个进程在访问页面时
不会被杀死,但会在内存分配上收到相应的错误。
对于那些想保证他们的内存分配在未来可用而又不需要初始化每一个页面的应用程序来说
是很有用的。
超量使用策略是通过sysctl `vm.overcommit_memory` 设置的。
可以通过 `vm.overcommit_ratio` (百分比)或 `vm.overcommit_kbytes` (绝对值)
来设置超限数量。这些只有在 `vm.overcommit_memory` 被设置为2时才有效果。
在 ``/proc/meminfo`` 中可以分别以CommitLimit和Committed_AS的形式查看当前
的超量使用和提交量。
陷阱
====
C语言的堆栈增长是一个隐含的mremap。如果你想得到绝对的保证,并在接近边缘的地方运行,
你 **必须** 为你认为你需要的最大尺寸的堆栈进行mmap。对于典型的堆栈使用来说,这并
不重要,但如果你真的非常关心的话,这就是一个值得关注的案例。
在模式2中,MAP_NORESERVE标志被忽略。
它是如何工作的
==============
超量使用是基于以下规则
对于文件映射
| SHARED or READ-only - 0 cost (该文件是映射而不是交换)
| PRIVATE WRITABLE - 每个实例的映射大小
对于匿名或者 ``/dev/zero`` 映射
| SHARED - 映射的大小
| PRIVATE READ-only - 0 cost (但作用不大)
| PRIVATE WRITABLE - 每个实例的映射大小
额外的计数
| 通过mmap制作可写副本的页面
| 从同一池中提取的shmfs内存
状态
====
* 我们核算mmap内存映射
* 我们核算mprotect在提交中的变化
* 我们核算mremap的大小变化
* 我们的审计 brk
* 审计munmap
* 我们在/proc中报告commit 状态
* 核对并检查分叉的情况
* 审查堆栈处理/执行中的构建
* 叙述SHMfs的情况
* 实现实际限制的执行
待续
====
* ptrace 页计数(这很难)。
@@ -0,0 +1,38 @@
:Original: Documentation/mm/page_frags.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
页面片段
========
一个页面片段是一个任意长度的任意偏移的内存区域,它位于一个0或更高阶的复合页面中。
该页中的多个碎片在该页的引用计数器中被单独计算。
page_frag函数,page_frag_alloc和page_frag_free,为页面片段提供了一个简单
的分配框架。这被网络堆栈和网络设备驱动使用,以提供一个内存的支持区域,作为
sk_buff->head使用,或者用于skb_shared_info的 “frags” 部分。
为了使用页面片段API,需要一个支持页面片段的缓冲区。这为碎片分配提供了一个中心点,
并允许多个调用使用一个缓存的页面。这样做的好处是可以避免对get_page的多次调用,
这在分配时开销可能会很大。然而,由于这种缓存的性质,要求任何对缓存的调用都要受到每
个CPU的限制,或者每个CPU的限制,并在执行碎片分配时强制禁止中断。
网络堆栈在每个CPU使用两个独立的缓存来处理碎片分配。netdev_alloc_cache被使用
netdev_alloc_frag和__netdev_alloc_skb调用的调用者使用。napi_alloc_cache
被调用__napi_alloc_frag和napi_alloc_skb的调用者使用。这两个调用的主要区别是
它们可能被调用的环境。“netdev” 前缀的函数可以在任何上下文中使用,因为这些函数
将禁用中断,而 ”napi“ 前缀的函数只可以在softirq上下文中使用。
许多网络设备驱动程序使用类似的方法来分配页面片段,但页面片段是在环或描述符级别上
缓存的。为了实现这些情况,有必要提供一种拆解页面缓存的通用方法。出于这个原因,
__page_frag_cache_drain被实现了。它允许通过一次调用从一个页面释放多个引用。
这样做的好处是,它允许清理被添加到一个页面的多个引用,以避免每次分配都调用
get_page。
Alexander Duyck,2016年11月29日。
@@ -0,0 +1,228 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/page_migration.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
页面迁移
========
页面迁移允许在进程运行时在NUMA系统的节点之间移动页面的物理位置。这意味着进程所看到的虚拟地
址并没有改变。然而,系统会重新安排这些页面的物理位置。
也可以参见 :ref: `<异构内存管理 (HMM)>` 以了解将页面迁移到设备私有内存或从设备私有内存中迁移。
页面迁移的主要目的是通过将页面移到访问该内存的进程所运行的处理器附近来减少内存访问的延迟。
页面迁移允许进程通过MF_MOVE和MF_MOVE_ALL选项手动重新定位其页面所在的节点,同时通过
mbind()设置一个新的内存策略。一个进程的页面也可以通过sys_migrate_pages()函数调用从另
一个进程重新定位。migrate_pages()函数调用接收两组节点,并将一个进程位于旧节点上的页面移
动到目标节点上。页面迁移功能由Andi Kleen的numactl包提供(需要0.9.3以上的版本,其仓库
地址https://github.com/numactl/numactl.git)。numactl提供了libnuma,它为页面迁移
提供了与其他NUMA功能类似的接口。执行 cat ``/proc/<pid>/numa_maps`` 允许轻松查看进
程的页面位置。参见proc(5)手册中的numa_maps文档。
如果调度程序将一个进程重新安置到一个遥远的节点上的处理器,手动迁移是很有用的。批量调度程序
或管理员可以检测到这种情况,并将进程的页面移到新处理器附近。内核本身只提供手动的页迁移支持。
自动的页面迁移可以通过用户空间的进程移动页面来实现。一个特殊的函数调用 "move_pages" 允许
在一个进程中移动单个页面。例如,NUMA分析器可以获得一个显示频繁的节点外访问的日志,并可以使
用这个结果将页面移动到更有利的位置。
较大型的设备通常使用cpusets将系统分割成若干个节点。Paul Jackson为cpusets配备了当任务被
转移到另一个cpuset时移动页面的能力(见:ref:`CPUSETS <cpusets>`)。Cpusets允许进程定
位的自动化。如果一个任务被移到一个新的cpuset上,那么它的所有页面也会随之移动,这样进程的
性能就不会急剧下降。如果cpuset允许的内存节点发生变化,cpuset中的进程页也会被移动。
页面迁移允许为所有迁移技术保留一组节点中页面的相对位置,这将保留生成的特定内存分配模式即使
进程已被迁移。为了保留内存延迟,这一点是必要的。迁移后的进程将以类似的性能运行。
页面迁移分几个步骤进行。首先为那些试图从内核中使用migrate_pages()的进程做一个高层次的
描述(对于用户空间的使用,可以参考上面提到的Andi Kleen的numactl包),然后对低水平的细
节工作做一个低水平描述。
在内核中使用 migrate_pages()
============================
1. 从LRU中移除页面。
要迁移的页面列表是通过扫描页面并把它们移到列表中来生成的。这是通过调用 folio_isolate_lru()
来完成的。调用folio_isolate_lru()增加了对该页的引用,这样在页面迁移发生时它就不会
消失。它还可以防止交换器或其他扫描器遇到该页。
2. 我们需要有一个new_folio_t类型的函数,可以传递给migrate_pages()。这个函数应该计算
出如何在给定的旧页面中分配正确的新页面。
3. migrate_pages()函数被调用,它试图进行迁移。它将调用该函数为每个被考虑迁移的页面分
配新的页面。
migrate_pages()如何工作
=======================
migrate_pages()对它的页面列表进行了多次处理。如果当时对一个页面的所有引用都可以被移除,
那么这个页面就会被移动。该页已经通过folio_isolate_lru()从LRU中移除,并且refcount被
增加,以便在页面迁移发生时不释放该页。
步骤:
1. 锁定要迁移的页面。
2. 确保回写已经完成。
3. 锁定我们要迁移到的新页面。锁定它是为了在迁移过程中立即阻止对这个(尚未更新的)页面的
访问。
4. 所有对该页的页表引用都被转换为迁移条目。这就减少了一个页面的mapcount。如果产生的
mapcount不是零,那么我们就不迁移该页。所有试图访问该页的用户空间进程现在将等待页
面锁或者等待迁移页表项被移除。
5. i_pages的锁被持有。这将导致所有试图通过映射访问该页的进程在自旋锁上阻塞。
6. 检查该页的Refcount,如果还有引用,我们就退出。否则,我们知道我们是唯一引用这个页
面的人。
7. 检查基数树,如果它不包含指向这个页面的指针,那么我们就退出,因为其他人修改了基数树。
8. 新的页面要用旧的页面的一些设置进行预处理,这样访问新的页面就会发现一个具有正确设置
的页面。
9. 基数树被改变以指向新的页面。
10. 旧页的引用计数被删除,因为地址空间的引用已经消失。对新页的引用被建立,因为新页被
地址空间引用。
11. i_pages锁被放弃。这样一来,在映射中的查找又变得可能了。进程将从在锁上自旋到在
被锁的新页上睡眠。
12. 页面内容被复制到新的页面上。
13. 剩余的页面标志被复制到新的页面上。
14. 旧的页面标志被清除,以表明该页面不再提供任何信息。
15. 新页面上的回写队列被触发了。
16. 如果迁移条目被插入到页表中,那么就用真正的ptes替换它们。这样做将使那些尚未等待页
锁的用户空间进程能够访问。
17. 页面锁从新旧页面上被撤销。等待页锁的进程将重做他们的缺页异常,并将到达新的页面。
18. 新的页面被移到LRU中,可以被交换器等再次扫描。
非LRU页面迁移
=============
尽管迁移最初的目的是为了减少NUMA的内存访问延迟,但压缩也使用迁移来创建高阶页面。
目前实现的问题是,它被设计为只迁移*LRU*页。然而,有一些潜在的非LRU页面可以在驱动中
被迁移,例如,zsmalloc,virtio-balloon页面。
对于virtio-balloon页面,迁移代码路径的某些部分已经被钩住,并添加了virtio-balloon
的特定函数来拦截迁移逻辑。这对一个驱动来说太特殊了,所以其他想让自己的页面可移动的驱
动就必须在迁移路径中添加自己的特定钩子。
为了克服这个问题,VM支持非LRU页面迁移,它为非LRU可移动页面提供了通用函数,而在迁移
路径中没有特定的驱动程序钩子。
如果一个驱动程序想让它的页面可移动,它应该定义三个函数,这些函数是
struct address_space_operations的函数指针。
1. ``bool (*isolate_page) (struct page *page, isolate_mode_t mode);``
VM对驱动的isolate_page()函数的期望是,如果驱动成功隔离了该页,则返回*true*。
返回true后,VM会将该页标记为PG_isolated,这样多个CPU的并发隔离就会跳过该
页进行隔离。如果驱动程序不能隔离该页,它应该返回*false*。
一旦页面被成功隔离,VM就会使用page.lru字段,因此驱动程序不应期望保留这些字段的值。
2. ``int (*migratepage) (struct address_space *mapping,``
| ``struct page *newpage, struct page *oldpage, enum migrate_mode);``
隔离后,虚拟机用隔离的页面调用驱动的migratepage()。migratepage()的功能是将旧页
的内容移动到新页,并设置struct page newpage的字段。请记住,如果你成功迁移了旧页
并返回MIGRATEPAGE_SUCCESS,你应该通过page_lock下的__ClearPageMovable()向虚
拟机表明旧页不再可移动。如果驱动暂时不能迁移该页,驱动可以返回-EAGAIN。在-EAGAIN
时,VM会在短时间内重试页面迁移,因为VM将-EAGAIN理解为 "临时迁移失败"。在返回除
-EAGAIN以外的任何错误时,VM将放弃页面迁移而不重试。
在migratepage()函数中,驱动程序不应该接触page.lru字段。
3. ``void (*putback_page)(struct page *);``
如果在隔离页上迁移失败,VM应该将隔离页返回给驱动,因此VM用隔离页调用驱动的
putback_page()。在这个函数中,驱动应该把隔离页放回自己的数据结构中。
非LRU可移动页标志
有两个页面标志用于支持非LRU可移动页面。
* PG_movable
驱动应该使用下面的函数来使页面在page_lock下可移动。::
void __SetPageMovable(struct page *page, struct address_space *mapping)
它需要address_space的参数来注册将被VM调用的migration family函数。确切地说,
PG_movable不是struct page的一个真正的标志。相反,VM复用了page->mapping的低
位来表示它::
#define PAGE_MAPPING_MOVABLE 0x2
page->mapping = page->mapping | PAGE_MAPPING_MOVABLE;
所以驱动不应该直接访问page->mapping。相反,驱动应该使用page_mapping(),它可
以在页面锁下屏蔽掉page->mapping的低2位,从而获得正确的struct address_space。
对于非LRU可移动页面的测试,VM支持__PageMovable()函数。然而,它并不能保证识别
非LRU可移动页面,因为page->mapping字段与struct page中的其他变量是统一的。如
果驱动程序在被虚拟机隔离后释放了页面,尽管page->mapping设置了PAGE_MAPPING_MOVABLE,
但它并没有一个稳定的值(看看__ClearPageMovable)。但是__PageMovable()在页
面被隔离后,无论页面是LRU还是非LRU可移动的,调用它开销都很低,因为LRU页面在
page->mapping中不可能有PAGE_MAPPING_MOVABLE设置。在用pfn扫描中的lock_page()
进行更大开销的检查来选择受害者之前,它也很适合只是瞥一眼来测试非LRU可移动的页面。
为了保证非LRU的可移动页面,VM提供了PageMovable()函数。与__PageMovable()不
同,PageMovable()在lock_page()下验证page->mapping和
mapping->a_ops->isolate_page。lock_page()可以防止突然破坏page->mapping。
使用__SetPageMovable()的驱动应该在释放页面之前通过page_lock()下的
__ClearMovablePage()清除该标志。
* PG_isolated
为了防止几个CPU同时进行隔离,VM在lock_page()下将隔离的页面标记为PG_isolated。
因此,如果一个CPU遇到PG_isolated非LRU可移动页面,它可以跳过它。驱动程序不需要
操作这个标志,因为VM会自动设置/清除它。请记住,如果驱动程序看到PG_isolated页,
这意味着该页已经被VM隔离,所以它不应该碰page.lru字段。PG_isolated标志与
PG_reclaim标志是同义的,所以驱动程序不应该为自己的目的使用PG_isolated。
监测迁移
========
以下事件(计数器)可用于监控页面迁移。
1. PGMIGRATE_SUCCESS: 正常的页面迁移成功。每个计数器意味着一个页面被迁移了。如果该
页是一个非THP和非hugetlb页,那么这个计数器会增加1。如果该页面是一个THP或hugetlb
页面,那么这个计数器会随着THP或hugetlb子页面的数量而增加。例如,迁移一个有4KB大小
的基础页(子页)的2MB THP,将导致这个计数器增加512。
2. PGMIGRATE_FAIL: 正常的页面迁移失败。与上面PGMIGRATE_SUCCESS的计数规则相同:如
果是THP或hugetlb,这个计数将被子页的数量增加。
3. THP_MIGRATION_SUCCESS: 一个THP被迁移而没有被分割。
4. THP_MIGRATION_FAIL: 一个THP不能被迁移,也不能被分割。
5. THP_MIGRATION_SPLIT: 一个THP被迁移了,但不是这样的:首先,这个THP必须被分割。
在拆分之后,对它的子页面进行了迁移重试。
THP_MIGRATION_* 事件也会更新相应的PGMIGRATE_SUCCESS或PGMIGRATE_FAIL事件。
例如,一个THP迁移失败将导致THP_MIGRATION_FAIL和PGMIGRATE_FAIL增加。
Christoph Lameter,2006年5月8日。
Minchan Kim,2016年3月28日。
@@ -0,0 +1,216 @@
:Original: Documentation/mm/page_owner.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
================================
page owner: 跟踪谁分配的每个页面
================================
概述
====
page owner是用来追踪谁分配的每一个页面。它可以用来调试内存泄漏或找到内存占用者。
当分配发生时,有关分配的信息,如调用堆栈和页面的顺序被存储到每个页面的特定存储中。
当我们需要了解所有页面的状态时,我们可以获得并分析这些信息。
尽管我们已经有了追踪页面分配/释放的tracepoint,但用它来分析谁分配的每个页面是
相当复杂的。我们需要扩大跟踪缓冲区,以防止在用户空间程序启动前出现重叠。而且,启
动的程序会不断地将跟踪缓冲区转出,供以后分析,这将会改变系统的行为,会产生更多的
可能性,而不是仅仅保留在内存中,所以不利于调试。
页面所有者也可以用于各种目的。例如,可以通过每个页面的gfp标志信息获得精确的碎片
统计。如果启用了page owner,它就已经实现并激活了。我们非常欢迎其他用途。
它也可以用来显示所有的栈以及它们当前分配的基础页面数,这让我们能够快速了解内存的
使用情况,而无需浏览所有页面并匹配分配和释放操作。
page owner在默认情况下是禁用的。所以,如果你想使用它,你需要在你的启动cmdline
中加入"page_owner=on"。如果内核是用page owner构建的,并且由于没有启用启动
选项而在运行时禁用page owner,那么运行时的开销是很小的。如果在运行时禁用,它不
需要内存来存储所有者信息,所以没有运行时内存开销。而且,页面所有者在页面分配器的
热路径中只插入了两个不可能的分支,如果不启用,那么分配就会像没有页面所有者的内核
一样进行。这两个不可能的分支应该不会影响到分配的性能,特别是在静态键跳转标签修补
功能可用的情况下。以下是由于这个功能而导致的内核代码大小的变化。
尽管启用page owner会使内核的大小增加几千字节,但这些代码大部分都在页面分配器和
热路径之外。构建带有page owner的内核,并在需要时打开它,将是调试内核内存问题的
最佳选择。
有一个问题是由实现细节引起的。页所有者将信息存储到struct page扩展的内存中。这
个内存的初始化时间比稀疏内存系统中的页面分配器启动的时间要晚一些,所以,在初始化
之前,许多页面可以被分配,但它们没有所有者信息。为了解决这个问题,这些早期分配的
页面在初始化阶段被调查并标记为分配。虽然这并不意味着它们有正确的所有者信息,但至
少,我们可以更准确地判断该页是否被分配。在2GB内存的x86-64虚拟机上,有13343
个早期分配的页面被捕捉和标记,尽管它们大部分是由结构页扩展功能分配的。总之,在这
之后,没有任何页面处于未追踪状态。
使用方法
========
1) 构建用户空间的帮助::
cd tools/mm
make page_owner_sort
2) 启用page owner: 添加 "page_owner=on" 到 boot cmdline.
3) 做你想调试的工作。
4) 分析来自页面所有者的信息::
cat /sys/kernel/debug/page_owner_stacks/show_stacks > stacks.txt
cat stacks.txt
post_alloc_hook+0x177/0x1a0
get_page_from_freelist+0xd01/0xd80
__alloc_pages+0x39e/0x7e0
allocate_slab+0xbc/0x3f0
___slab_alloc+0x528/0x8a0
kmem_cache_alloc+0x224/0x3b0
sk_prot_alloc+0x58/0x1a0
sk_alloc+0x32/0x4f0
inet_create+0x427/0xb50
__sock_create+0x2e4/0x650
inet_ctl_sock_create+0x30/0x180
igmp_net_init+0xc1/0x130
ops_init+0x167/0x410
setup_net+0x304/0xa60
copy_net_ns+0x29b/0x4a0
create_new_namespaces+0x4a1/0x820
nr_base_pages: 16
...
...
echo 7000 > /sys/kernel/debug/page_owner_stacks/count_threshold
cat /sys/kernel/debug/page_owner_stacks/show_stacks> stacks_7000.txt
cat stacks_7000.txt
post_alloc_hook+0x177/0x1a0
get_page_from_freelist+0xd01/0xd80
__alloc_pages+0x39e/0x7e0
alloc_pages_mpol+0x22e/0x490
folio_alloc+0xd5/0x110
filemap_alloc_folio+0x78/0x230
page_cache_ra_order+0x287/0x6f0
filemap_get_pages+0x517/0x1160
filemap_read+0x304/0x9f0
xfs_file_buffered_read+0xe6/0x1d0 [xfs]
xfs_file_read_iter+0x1f0/0x380 [xfs]
__kernel_read+0x3b9/0x730
kernel_read_file+0x309/0x4d0
__do_sys_finit_module+0x381/0x730
do_syscall_64+0x8d/0x150
entry_SYSCALL_64_after_hwframe+0x62/0x6a
nr_base_pages: 20824
...
cat /sys/kernel/debug/page_owner > page_owner_full.txt
./page_owner_sort page_owner_full.txt sorted_page_owner.txt
``page_owner_full.txt`` 的一般输出情况如下::
Page allocated via order XXX, ...
PFN XXX ...
// 栈详情
Page allocated via order XXX, ...
PFN XXX ...
// 栈详情
默认情况下,它将以一个给定的pfn开始,做完整的pfn转储,且page_owner支持fseek。
FILE *fp = fopen("/sys/kernel/debug/page_owner", "r");
fseek(fp, pfn_start, SEEK_SET);
``page_owner_sort`` 工具忽略了 ``PFN`` 行,将剩余的行放在buf中,使用regexp提
取页序值,计算buf的次数和页数,最后根据参数进行排序。
在 ``sorted_page_owner.txt`` 中可以看到关于谁分配了每个页面的结果。一般输出::
XXX times, XXX pages:
Page allocated via order XXX, ...
// Detailed stack
默认情况下, ``page_owner_sort`` 是根据buf的时间来排序的。如果你想
按buf的页数排序,请使用-m参数。详细的参数是:
基本函数::
排序:
-a 按内存分配时间排序
-m 按总内存排序
-p 按pid排序。
-P 按tgid排序。
-n 按任务命令名称排序。
-r 按内存释放时间排序。
-s 按堆栈跟踪排序。
-t 按时间排序(默认)。
--sort <order> 指定排序顺序。排序的语法是[+|-]key[,[+|-]key[,...]]。从
**标准格式指定器**那一节选择一个键。"+"是可选的,因为默认的方向是数字或
词法的增加。允许混合使用缩写和完整格式的键。
例子:
./page_owner_sort <input> <output> --sort=n,+pid,-tgid
./page_owner_sort <input> <output> --sort=at
其它函数::
剔除:
--cull <rules>
指定剔除规则。剔除的语法是key[,key[,...]]。从**标准格式指定器**
部分选择一个多字母键。
<rules>是一个以逗号分隔的列表形式的单一参数,它提供了一种指定单个剔除规则的
方法。 识别的关键字在下面的**标准格式指定器**部分有描述。<规则>可以通过键的
序列k1,k2,...来指定,在下面的标准排序键部分有描述。允许混合使用简写和完整形
式的键。
Examples:
./page_owner_sort <input> <output> --cull=stacktrace
./page_owner_sort <input> <output> --cull=st,pid,name
./page_owner_sort <input> <output> --cull=n,f
过滤:
-f 过滤掉内存已被释放的块的信息。
选择:
--pid <pidlist> 按pid选择。这将选择进程ID号出现在<pidlist>中的块。
--tgid <tgidlist> 按tgid选择。这将选择其线程组ID号出现在<tgidlist>
中的块。
--name <cmdlist> 按任务命令名称选择。这将选择其任务命令名称出现在
<cmdlist>中的区块。
<pidlist>, <tgidlist>, <cmdlist>是以逗号分隔的列表形式的单个参数,
它提供了一种指定单个选择规则的方法。
例子:
./page_owner_sort <input> <output> --pid=1
./page_owner_sort <input> <output> --tgid=1,2,3
./page_owner_sort <input> <output> --name name1,name2
标准格式指定器
==============
::
--sort的选项:
短键 长键 描述
p pid 进程ID
tg tgid 线程组ID
n name 任务命令名称
st stacktrace 页面分配的堆栈跟踪
T txt 块的全文
ft free_ts 页面释放时的时间戳
at alloc_ts 页面被分配时的时间戳
ator allocator 页面的内存分配器
--curl的选项:
短键 长键 描述
p pid 进程ID
tg tgid 线程组ID
n name 任务命令名称
f free 该页是否已经释放
st stacktrace 页面分配的堆栈跟踪
ator allocator 页面的内存分配器
@@ -0,0 +1,69 @@
.. SPDX-License-Identifier: GPL-2.0
:Original: Documentation/mm/page_table_check.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
页表检查
========
概述
====
页表检查允许通过确保防止某些类型的内存损坏来强化内核。
当新的页面可以从用户空间访问时,页表检查通过将它们的页表项(PTEs PMD等)添加到页表中来执行额外
的验证。
在检测到损坏的情况下,内核会被崩溃。页表检查有一个小的性能和内存开销。因此,它在默认情况下是禁用
的,但是在额外的加固超过性能成本的系统上,可以选择启用。另外,由于页表检查是同步的,它可以帮助调
试双映射内存损坏问题,在错误的映射发生时崩溃内核,而不是在内存损坏错误发生后内核崩溃。
双重映射检测逻辑
================
+-------------------+-------------------+-------------------+------------------+
| Current Mapping | New mapping | Permissions | Rule |
+===================+===================+===================+==================+
| Anonymous | Anonymous | Read | Allow |
+-------------------+-------------------+-------------------+------------------+
| Anonymous | Anonymous | Read / Write | Prohibit |
+-------------------+-------------------+-------------------+------------------+
| Anonymous | Named | Any | Prohibit |
+-------------------+-------------------+-------------------+------------------+
| Named | Anonymous | Any | Prohibit |
+-------------------+-------------------+-------------------+------------------+
| Named | Named | Any | Allow |
+-------------------+-------------------+-------------------+------------------+
启用页表检查
============
用以下方法构建内核:
- PAGE_TABLE_CHECK=y
注意,它只能在ARCH_SUPPORTS_PAGE_TABLE_CHECK可用的平台上启用。
- 使用 "page_table_check=on" 内核参数启动。
可以选择用PAGE_TABLE_CHECK_ENFORCED来构建内核,以便在没有额外的内核参数的情况下获得页表
支持。
实现注意事项
============
我们特意决定不使用 VMA 信息,以避免依赖于 MM 状态(除了有限的 “struct page” 信息)。页表检查
独立于 Linux-MM 状态机,它验证用户可访问的页面不会被错误地共享。
PAGE_TABLE_CHECK 依赖于 EXCLUSIVE_SYSTEM_RAM。原因在于,若没有 EXCLUSIVE_SYSTEM_RAM,
用户被允许通过 /dev/mem 将任意物理内存区域映射到用户空间。同时,页面可能在映射到用户空间期间
改变自己的属性(例如,从匿名页面变为命名页面),导致页表检查检测到“损坏”。
即使有 EXCLUSIVE_SYSTEM_RAM,I/O 页面可能仍然被允许通过 /dev/mem 映射。然而,这些页面始终
被视为命名页面,所以它们不会破坏页表检查中使用的逻辑。
@@ -0,0 +1,221 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/page_tables.rst
:翻译:
张鹏宇 Pengyu Zhang <zpenya1314@gmail.com>
:校译:
====
页表
====
分页虚拟内存是随虚拟内存的概念一起于 1962 年在 Ferranti Atlas 计算机上被提出的,
这是第一台有分页虚拟内存的计算机。随着时间推移,这个特性被迁移到更新的计算机上,
并且成为所有类 Unix 系统实际的特性。在 1985 年,这个特性被包含在了英特尔 80386
中,也就是运行 Linux 1.0 的CPU。
页表将 CPU 看到的虚拟地址映射到外部内存总线上看到的物理地址。
Linux 将页表定义为一个分级结构,目前有五级。对于支持的每种架构,其代码会根据硬件
限制对这个层级结构进行映射。
虚拟地址对应的物理地址通常由底层物理页帧引用。 **页帧号(page frame number,pfn)**
是页的物理地址(在外部内存总线看到的地址)除以 `PAGE_SIZE` 得到的值。
物理内存地址 0 对应 *pfn 0*,而最大的 pfn 对应处理器外部地址总线所能寻址物理地址
的最后一页。
在页粒度为 4KB 且地址范围为32位的情况下,pfn 0 对应地址0x00000000,pfn 1 对应
地址0x00001000,pfn 2 对应地址 0x00002000,以此类推,直到 pfn 0xfffff 对应
0xfffff000。如果页粒度为 16KB,则 pfn 分别对应地址 0x00004000、0x00008000
... 0xffffc000,pfn 的范围从 0 到 0x3ffff。
如你所见,对于 4KB 页面粒度,页基址使用地址的 12-31 位,这就是为什么在这种情况下
`PAGE_SHIFT` 被定义为 12,并且 `PAGE_SIZE` 通常由页偏移定义,为 `(1 << PAGE_SHIFT)`。
随着内存容量的增加,久而久之层级结构逐渐加深。Linux 最初使用 4KB 页面和一个名为
`swapper_pg_dir` 的页表,该页表拥有 1024 个表项(entries),覆盖 4MB 的内存,
事实上Torvald 的第一台计算机正好就有 4MB 物理内存。表项在这张表中被称为 *PTE*:s
- 页表项(page table entries)。
软件页表层级结构反映了页表硬件已经变得分层化的事实,而这种分层化的目的是为了节省
页表内存并加快地址映射速度。
当然,人们可以想象一张拥有大量表项的单一线性的页表将整个内存分为一个个页。而且,
这样的页表会非常稀疏,因为虚拟内存中大部分位置通常是未使用的。通过页表分层,虚拟
内存中的大量空洞不会浪费宝贵的页表内存,因为只需要在上层页表中将大块的区域标记为
未映射即可。
另外,在现代处理器中,上层页表项可以直接指向一个物理地址范围,这使得单个上层
页表项可以连续映射几兆字节甚至几千兆字节的内存范围,从而快捷地实现虚拟地址到
物理地址的映射:当你找到一个像这样的大型映射范围时,无需在层级结构中进一步遍历。
页表的层级结构目前发展为如下所示::
+-----+
| PGD |
+-----+
|
| +-----+
+-->| P4D |
+-----+
|
| +-----+
+-->| PUD |
+-----+
|
| +-----+
+-->| PMD |
+-----+
|
| +-----+
+-->| PTE |
+-----+
不同页表层级的符号含义从最底层开始如下:
- **pte**, `pte_t`, `pteval_t` = **页表项** - 前面提到过。*pte* 是一个由
`PTRS_PER_PTE` 个 `pteval_t` 类型元素组成的数组,每个元素将一个虚拟内存页
映射到一个物理内存页。体系结构定义了 `pteval_t` 的大小和内容。
一个典型的例子是 `pteval_t` 是一个 32 或者 64 位的值,其中高位是 **pfn**,
而低位则一些特定体系架构相关的位,如内存保护。
这个 **表项(entry)** 有点令人困惑,因为在 Linux 1.0 中它确实指的是单层顶级
页表中的单个页表项,但在首次引入二级页表时,它被重新定义为映射元素的数组。
因此,*pte* 现在指的是最底层的页 *表*,而不是一个页表 *项*。
- **pmd**, `pmd_t`, `pmdval_t` = **页中间目录(Page Middle Directory)**,
位于 *pte* 之上的层级结构,包含 `PTRS_PER_PMD` 个指向 *pte* 的引用。
- **pud**, `pud_t`, `pudval_t` = **页上级目录(Page Upper Directory)**
是在其他层级之后引入的,用于处理四级页表。它可能未被使用,或者像我们稍后
讨论的那样被“折叠”。
- **p4d**, `p4d_t`, `p4dval_t` = **页四级目录(Page Level 4 Directory)**
是在 *pud* 之后用于处理五级页表引入的。至此,显然需要用数字来替代 *pgd*、
*pmd*、*pud* 等目录层级的名称,不能再继续使用临时的命名方式。这个目录层级
只在实际拥有五级页表的系统上使用,否则它会被折叠。
- **pgd**, `pgd_t`, `pgdval_t` = **页全局目录(Page Global Directory)** -
Linux 内核用于处理内核内存的 *PGD* 主页表仍然位于 `swapper_pg_dir`。
但系统中的每个用户空间进程也有自己的内存上下文,因此也有自己的 *pgd*,
它位于 `struct mm_struct` 中,而 `struct mm_struct` 又在每个 `struct task_struct`
中有引用。所以,任务(进程)存在一个形式为 `struct mm_struct` 的内存上下文,
而这个结构体中有一个指向指向相应的页全局目录 `struct pgt_t *pgd` 指针。
重申一下:页表层级结构中的每一层都是一个 *指针数组*,所以 *pgd* 包含 `PTRS_PER_PGD`
个指向下一层的指针,*p4d* 包含 `PTRS_PER_P4D` 个指向 *pud* 项的指针,依此类推。
每一层的指针数量由体系结构定义。::
PMD
--> +-----+ PTE
| ptr |-------> +-----+
| ptr |- | ptr |-------> PAGE
| ptr | \ | ptr |
| ptr | \ ...
| ... | \
| ptr | \ PTE
+-----+ +----> +-----+
| ptr |-------> PAGE
| ptr |
...
页表折叠
========
如果架构不使用所有的页表层级,那么这些层级可以被 *折叠*,也就是说被跳过。在
访问下一层时,所有在页表上执行的操作都会在编译时增强,以跳过这一层。
与架构无关的页表处理代码(例如虚拟内存管理器)需要编写得能够遍历当前的所有五个
层级。对于特定架构的代码,也应优先采用这种风格,以便对未来的变化具有更好的适应性。
MMU,TLB 和缺页异常
===================
`内存管理单元(MMU)` 是处理虚拟地址到物理地址转换的硬件组件。它可能会使用相对较小
的硬件缓存,如 `转换后备缓冲区(TLB)` 和 `页遍历缓存`,以加快这些地址翻译过程。
当 CPU 访存时,它会向 MMU 提供一个虚拟地址。MMU 会首先检查 TLB 或者页遍历缓存
(在支持的架构上)是否存在对应的转换结果。如果没有,MMU 会通过遍历来确定物理地址
并且建立映射。
当页面被写入时,该页的脏位会被设置(即打开)。每个内存页面都有相关的权限位和脏位。
后者表明这个页自从被加载到内存以来是否被修改。
如果没有任何阻碍,物理内存到头来可以被任意访问并且对物理帧进行请求的操作。
MMU 无法找到某些转换有多种原因。有可能是 CPU 试图去访问当前进程没有权限访问的
内存,或者因为访问的数据还不在物理内存中。
当这些情况发生时,MMU 会触发缺页异常,这是一种异常类型,用于通知 CPU 暂停当前
执行并运行一个特殊的函数去处理这些异常。
缺页异常有一些常见且预期的原因。这些因素是由称为“懒加载”和“写时复制”的进程管理
优化技术来触发的。缺页异常也可能发生在当页帧被交换到持久存储(交换分区或者文件)
并从其物理地址移出时。
这些技术提高了内存效率,减少了延迟,并且最小化了空间占用。本文档不会深入讨论
“懒加载”和“写时复制”的细节,因为这些的主题属于进程地址管理范畴,超出了本文范围。
交换技术和前面提到的其他技术不同,因为它是在压力过大下情况下减少内存消耗的一种
迫不得已的手段,因此是不受欢迎的。
交换不适用于由内核逻辑地址映射的内存。这些地址是内核虚拟地址空间的子集,直接映射
一段连续的物理内存。对于提供的任意逻辑地址,它的物理地址可以通过对偏移量进行简单
的算数运算来确定。对逻辑地址的访问很快,因为这避免了复杂的页表查找,但代价是这些
内存不能被驱逐或置换。
如果内核无法为必须存在于物理帧中的数据腾出空间,那么它会调用内存不足(out-of-memory,
OOM)杀手,通过杀掉低优先级的进程来腾出空间,直到内存压力下降到安全阈值之下。
另外,代码漏洞或指示 CPU 访问的精心制作的恶意地址也可能导致缺页异常。一个进程的
线程可以利用指令来访问不属于其地址空间的(非共享)内存,或者试图执行写入只读位置
的指令。
如果上述情况发生在用户态,内核会向当前线程发送 `段错误` (SIGSEGV)信号。该信号
通常导致线程及其所属的进程终止。
本文将简化并概述 Linux 内核如何处理这些缺页中断、创建表和表项、检查内存是否存在,
以及当内存不存在时,如何请求从持久存储或其他设备加载数据,并更新 MMU 及其缓存。
最初的步骤依赖于架构。大多是架构跳转到 `do_page_fault()`,而 x86 中断处理程序是由
`DEFINE_IDTENTRY_RAW_ERRORCODE()` 宏定义的,该宏调用 `handle_page_fault()`。
无论调用路径如何,所有架构最终都会调用 `handle_mm_fault()`,该函数通常会调用
`__handle_mm_fault()` 来执行实际分配页表的任务。
如果不幸无法调用 `__handle_mm_fault()` 则意味着虚拟地址指向了无权访问的物理
内存区域(至少对于当前上下文如此)。这种情况会导致内核向该进程发送上述的 SIGSEGV
信号,并引发前面提到的后果。
这些用于查找偏移量的函数名称通常以 `*_offset()` 结尾,其中“\*”可以是 pgd,p4d,
pud,pmd 或者 pte;而分配相应层级页表的函数名称是 `*_alloc`,它们按照上述命名
约定以对应页表层级的类型命名。
页表遍历可能在中间或者上层结束(PMD,PUD)。
Linux 支持比通常 4KB 更大的页面(即所谓的 `巨页`)。当使用这种较大的页面时,没有
必要使用更低层的页表项(PTE)。巨页通常包含 2MB 到 1GB 的大块连续物理区域,分别由
PMD 和 PUD 页表项映射。
巨页带来许多好处,如减少 TLB 压力,减少页表开销,提高内存分配效率,以及改善
特定工作负载的性能。然而,这些好处也伴随着权衡,如内存浪费和分配难度增加。
在遍历和分配的最后,如果没有返回错误,`__handle_mm_fault()` 最终调用 `handle_pte_fault()`
通过 `do_fault()` 执行 `do_read_fault()`、 `do_cow_fault()` 和 `do_shared_fault()`。
“read”,“cow”和“shared”分别暗示了它处理错误的类型和原因。
实际的工作流程实现是非常复杂的。其设计允许 Linux 根据每种架构的特定特性处理缺页
异常,同时仍然共享一个通用的整体结构。
为了总结 Linux 如何处理缺页中断的概述,需要补充的是,缺页异常处理程序可以通过
`pagefault_disable()` 和 `pagefault_enable()` 分别禁用和启用。
许多代码路径使用了这两个函数,因为它们需要禁止陷入缺页异常处理程序,主要是为了
防止死锁。
@@ -0,0 +1,356 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/physical_memory.rst
:翻译:
王亚鑫 Yaxin Wang <wang.yaxin@zte.com.cn>
========
物理内存
========
Linux可用于多种架构,因此需要一个与架构无关的抽象来表示物理内存。本章描述
了管理运行系统中物理内存的结构。
第一个与内存管理相关的主要概念是 `非一致性内存访问(NUMA)
<https://en.wikipedia.org/wiki/Non-uniform_memory_access>`
在多核和多插槽机器中,内存可能被组织成不同的存储区,这些存储区根据与处理器
的距离“不同”而有不同的访问开销。例如,可能为每个CPU分配内存存储区,或者为
外围设备在附近分配一个非常适合DMA的内存存储区。
每个存储区被称为一个节点,节点在Linux中表示为 ``struct pglist_data``,
即使是在UMA架构中也是这样表示。该结构总是通过 ``pg_data_t`` 来引用。特
定节点的 ``pg_data_t`` 结构体可以通过NODE_DATA(nid)引用,其中nid被称
为该节点的ID。
对于非一致性内存访问(NUMA)架构,节点数据结构在引导时由特定于架构的代码早
期分配。通常,这些结构在其所在的内存区上本地分配。对于一致性内存访问(UMA)
架构,只使用一个静态的 ``pg_data_t`` 结构体,称为 ``contig_page_data``。
节点将会在 :ref:`节点 <nodes>` 章节中进一步讨论。
整个物理内存被划分为一个或多个被称为区域的块,这些区域表示内存的范围。这
些范围通常由访问内存的架构限制来决定。在节点内,与特定区域对应的内存范围
由 ``struct zone`` 结构体描述,该结构被定义为 ``zone_t``,每种区域都
属于以下描述类型的一种。
* ``ZONE_DMA`` 和 ``ZONE_DMA32`` 在历史上代表适用于DMA的内存,这些
内存由那些不能访问所有可寻址内存的外设访问。多年来,已经有了更好、更稳
固的接口来获取满足特定DMA需求的内存(这些接口由
Documentation/core-api/dma-api.rst 文档描述),但是 ``ZONE_DMA``
和 ``ZONE_DMA32`` 仍然表示访问受限的内存范围。
取决于架构的不同,这两种区域可以在构建时通过关闭 ``CONFIG_ZONE_DMA`` 和
``CONFIG_ZONE_DMA32`` 配置选项来禁用。一些64位的平台可能需要这两种区域,
因为他们支持具有不同DMA寻址限制的外设。
* ``ZONE_NORMAL`` 是普通内存的区域,这种内存可以被内核随时访问。如果DMA
设备支持将数据传输到所有可寻址的内存区域,那么可在该区域的页面上执行DMA
操作。``ZONE_NORMAL`` 总是开启的。
* ``ZONE_HIGHMEM`` 是指那些没有在内核页表中永久映射的物理内存部分。该区
域的内存只能通过临时映射被内核访问。该区域只在某些32位架构上可用,并且是
通过 ``CONFIG_HIGHMEM`` 选项开启。
* ``ZONE_MOVABLE`` 是指可访问的普通内存区域,就像 ``ZONE_NORMAL``
一样。不同之处在于 ``ZONE_MOVABLE`` 中的大多数页面内容是可移动的。
这意味着这些页面的虚拟地址不会改变,但它们的内容可能会在不同的物理页面
之间移动。通常,在内存热插拔期间填充 ``ZONE_MOVABLE``,在启动时也可
以使用 ``kernelcore``、``movablecore`` 和 ``movable_node``
这些内核命令行参数来填充。更多详细信息,请参阅内核文档
Documentation/mm/page_migration.rst 和
Documentation/admin-guide/mm/memory-hotplug.rst。
* ``ZONE_DEVICE`` 表示位于持久性内存(PMEM)和图形处理单元(GPU)
等设备上的内存。它与RAM区域类型有不同的特性,并且它的存在是为了提供
:ref:`struct page<Pages>` 结构和内存映射服务,以便设备驱动程序能
识别物理地址范围。``ZONE_DEVICE`` 通过 ``CONFIG_ZONE_DEVICE``
选项开启。
需要注意的是,许多内核操作只能使用 ``ZONE_NORMAL`` 来执行,因此它是
性能最关键区域。区域在 :ref:`区域 <zones>` 章节中有更详细的讨论。
节点和区域范围之间的关系由固件报告的物理内存映射决定,另外也由内存寻址
的架构约束以及内核命令行中的某些参数决定。
例如,在具有2GB RAM的x86统一内存架构(UMA)机器上运行32位内核时,整
个内存将位于节点0,并且将有三个区域: ``ZONE_DMA``、 ``ZONE_NORMAL``
和 ``ZONE_HIGHMEM``::
0 2G
+-------------------------------------------------------------+
| node 0 |
+-------------------------------------------------------------+
0 16M 896M 2G
+----------+-----------------------+--------------------------+
| ZONE_DMA | ZONE_NORMAL | ZONE_HIGHMEM |
+----------+-----------------------+--------------------------+
在内核构建时关闭 ``ZONE_DMA`` 开启 ``ZONE_DMA32``,并且具有16GB
RAM平均分配在两个节点上的arm64机器上,使用 ``movablecore=80%`` 参数
启动时,``ZONE_DMA32``、``ZONE_NORMAL`` 和 ``ZONE_MOVABLE``
位于节点0,而 ``ZONE_NORMAL`` 和 ``ZONE_MOVABLE`` 位于节点1::
1G 9G 17G
+--------------------------------+ +--------------------------+
| node 0 | | node 1 |
+--------------------------------+ +--------------------------+
1G 4G 4200M 9G 9320M 17G
+---------+----------+-----------+ +------------+-------------+
| DMA32 | NORMAL | MOVABLE | | NORMAL | MOVABLE |
+---------+----------+-----------+ +------------+-------------+
内存存储区可能位于交错的节点。在下面的例子中,一台x86机器有16GB的RAM分
布在4个内存存储区上,偶数编号的内存存储区属于节点0,奇数编号的内存条属于
节点1::
0 4G 8G 12G 16G
+-------------+ +-------------+ +-------------+ +-------------+
| node 0 | | node 1 | | node 0 | | node 1 |
+-------------+ +-------------+ +-------------+ +-------------+
0 16M 4G
+-----+-------+ +-------------+ +-------------+ +-------------+
| DMA | DMA32 | | NORMAL | | NORMAL | | NORMAL |
+-----+-------+ +-------------+ +-------------+ +-------------+
在这种情况下,节点0将覆盖从0到12GB的内存范围,而节点1将覆盖从4GB到16GB
的内存范围。
.. _nodes_zh_CN:
节点
====
正如我们所提到的,内存中的每个节点由 ``pg_data_t`` 描述,通过
``struct pglist_data`` 结构体的类型定义。在分配页面时,默认情况下,Linux
使用节点本地分配策略,从离当前运行CPU的最近节点分配内存。由于进程倾向于在同
一个CPU上运行,很可能会使用当前节点的内存。分配策略可以由用户控制,如内核文
档 Documentation/admin-guide/mm/numa_memory_policy.rst 中所述。
大多数NUMA(非统一内存访问)架构维护了一个指向节点结构的指针数组。这些实际
的结构在启动过程中的早期被分配,这时特定于架构的代码解析了固件报告的物理内
存映射。节点初始化的大部分工作是在由free_area_init()实现的启动过程之后
完成,该函数在后面的小节 :ref:`初始化 <initialization>` 中有详细描述。
除了节点结构,内核还维护了一个名为 ``node_states`` 的 ``nodemask_t``
位掩码数组。这个数组中的每个位掩码代表一组特定属性的节点,这些属性由
``enum node_states`` 定义,定义如下:
``N_POSSIBLE``
节点可能在某个时刻上线。
``N_ONLINE``
节点已经上线。
``N_NORMAL_MEMORY``
节点拥有普通内存。
``N_HIGH_MEMORY``
节点拥有普通或高端内存。当关闭 ``CONFIG_HIGHMEM`` 配置时,
也可以称为 ``N_NORMAL_MEMORY``。
``N_MEMORY``
节点拥有(普通、高端、可移动)内存。
``N_CPU``
节点拥有一个或多个CPU。
对于具有上述属性的每个节点,``node_states[<property>]``
掩码中对应于节点ID的位会被置位。
例如,对于具有常规内存和CPU的节点2,第二个bit将被设置::
node_states[N_POSSIBLE]
node_states[N_ONLINE]
node_states[N_NORMAL_MEMORY]
node_states[N_HIGH_MEMORY]
node_states[N_MEMORY]
node_states[N_CPU]
有关使用节点掩码(nodemasks)可能进行的各种操作,请参考
``include/linux/nodemask.h``。
除此之外,节点掩码(nodemasks)提供用于遍历节点的宏,即
``for_each_node()`` 和 ``for_each_online_node()``。
例如,要为每个在线节点调用函数 foo(),可以这样操作::
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid);
foo(pgdat);
}
节点数据结构
------------
节点结构 ``struct pglist_data`` 在 ``include/linux/mmzone.h``
中声明。这里我们将简要描述这个结构体的字段:
通用字段
~~~~~~~~
``node_zones``
表示该节点的区域列表。并非所有区域都可能被填充,但这是
完整的列表。它被该节点的node_zonelists以及其它节点的
node_zonelists引用。
``node_zonelists``
表示所有节点中所有区域的列表。此列表定义了分配内存时首选的区域
顺序。``node_zonelists`` 在核心内存管理结构初始化期间,
由 ``mm/page_alloc.c`` 中的 ``build_zonelists()``
函数设置。
``nr_zones``
表示此节点中已填充区域的数量。
``node_mem_map``
对于使用FLATMEM内存模型的UMA系统,0号节点的 ``node_mem_map``
表示每个物理帧的struct pages数组。
``node_page_ext``
对于使用FLATMEM内存模型的UMA系统,0号节点的 ``node_page_ext``
是struct pages的扩展数组。只有在构建时开启了 ``CONFIG_PAGE_EXTENSION``
选项的内核中才可用。
``node_start_pfn``
表示此节点中起始页面帧的页面帧号。
``node_present_pages``
表示此节点中存在的物理页面的总数。
``node_spanned_pages``
表示包括空洞在内的物理页面范围的总大小。
``node_size_lock``
一个保护定义节点范围字段的锁。仅在开启了 ``CONFIG_MEMORY_HOTPLUG`` 或
``CONFIG_DEFERRED_STRUCT_PAGE_INIT`` 配置选项中的某一个时才定义。提
供了 ``pgdat_resize_lock()`` 和 ``pgdat_resize_unlock()`` 用来操作
``node_size_lock``,而无需检查 ``CONFIG_MEMORY_HOTPLUG`` 或
``CONFIG_DEFERRED_STRUCT_PAGE_INIT`` 选项。
``node_id``
节点的节点ID(NID),从0开始。
``totalreserve_pages``
这是每个节点保留的页面,这些页面不可用于用户空间分配。
``first_deferred_pfn``
如果大型机器上的内存初始化被推迟,那么第一个PFN(页帧号)是需要初始化的。
在开启了 ``CONFIG_DEFERRED_STRUCT_PAGE_INIT`` 选项时定义。
``deferred_split_queue``
每个节点的大页队列,这些大页的拆分被推迟了。仅在开启了 ``CONFIG_TRANSPARENT_HUGEPAGE``
配置选项时定义。
``__lruvec``
每个节点的lruvec持有LRU(最近最少使用)列表和相关参数。仅在禁用了内存
控制组(cgroups)时使用。它不应该直接访问,而应该使用 ``mem_cgroup_lruvec()``
来查找lruvecs。
回收控制
~~~~~~~~
另见内核文档 Documentation/mm/page_reclaim.rst 文件。
``kswapd``
每个节点的kswapd内核线程实例。
``kswapd_wait``, ``pfmemalloc_wait``, ``reclaim_wait``
同步内存回收任务的工作队列。
``nr_writeback_throttled``
等待写回脏页时,被限制的任务数量。
``kswapd_order``
控制kswapd尝试回收的order。
``kswapd_highest_zoneidx``
kswapd线程可以回收的最高区域索引。
``kswapd_failures``
kswapd无法回收任何页面的运行次数。
``min_unmapped_pages``
无法回收的未映射文件支持的最小页面数量。由 ``vm.min_unmapped_ratio``
系统控制台(sysctl)参数决定。在开启 ``CONFIG_NUMA`` 配置时定义。
``min_slab_pages``
无法回收的SLAB页面的最少数量。由 ``vm.min_slab_ratio`` 系统控制台
(sysctl)参数决定。在开启 ``CONFIG_NUMA`` 时定义。
``flags``
控制回收行为的标志位。
内存压缩控制
~~~~~~~~~~~~
``kcompactd_max_order``
kcompactd应尝试实现的页面order。
``kcompactd_highest_zoneidx``
kcompactd可以压缩的最高区域索引。
``kcompactd_wait``
同步内存压缩任务的工作队列。
``kcompactd``
每个节点的kcompactd内核线程实例。
``proactive_compact_trigger``
决定是否使用主动压缩。由 ``vm.compaction_proactiveness`` 系统控
制台(sysctl)参数控制。
统计信息
~~~~~~~~
``per_cpu_nodestats``
表示节点的Per-CPU虚拟内存统计信息。
``vm_stat``
表示节点的虚拟内存统计数据。
.. _zones_zh_CN:
区域
====
.. admonition:: Stub
本节内容不完整。请列出并描述相应的字段。
.. _pages_zh_CN:
页
====
.. admonition:: Stub
本节内容不完整。请列出并描述相应的字段。
.. _folios_zh_CN:
页码
====
.. admonition:: Stub
本节内容不完整。请列出并描述相应的字段。
.. _initialization_zh_CN:
初始化
======
.. admonition:: Stub
本节内容不完整。请列出并描述相应的字段。
@@ -0,0 +1,32 @@
:Original: Documentation/mm/remap_file_pages.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
==============================
remap_file_pages()系统调用
==============================
remap_file_pages()系统调用被用来创建一个非线性映射,也就是说,在这个映射中,
文件的页面被无序映射到内存中。使用remap_file_pages()比重复调用mmap(2)的好
处是,前者不需要内核创建额外的VMA(虚拟内存区)数据结构。
支持非线性映射需要在内核虚拟内存子系统中编写大量的non-trivial的代码,包括热
路径。另外,为了使非线性映射工作,内核需要一种方法来区分正常的页表项和带有文件
偏移的项(pte_file)。内核为达到这个目的在PTE中保留了标志。PTE标志是稀缺资
源,特别是在某些CPU架构上。如果能腾出这个标志用于其他用途就更好了。
幸运的是,在生活中并没有很多remap_file_pages()的用户。只知道有一个企业的RDBMS
实现在32位系统上使用这个系统调用来映射比32位虚拟地址空间线性尺寸更大的文件。
由于64位系统的广泛使用,这种使用情况已经不重要了。
syscall被废弃了,现在用一个模拟来代替它。仿真会创建新的VMA,而不是非线性映射。
对于remap_file_pages()的少数用户来说,它的工作速度会变慢,但ABI被保留了。
仿真的一个副作用(除了性能之外)是,由于额外的VMA,用户可以更容易达到
vm.max_map_count的限制。关于限制的更多细节,请参见DEFAULT_MAX_MAP_COUNT
的注释。
@@ -0,0 +1,96 @@
:Original: Documentation/mm/split_page_table_lock.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
=================================
分页表锁(split page table lock)
=================================
最初,mm->page_table_lock spinlock保护了mm_struct的所有页表。但是这种方
法导致了多线程应用程序的缺页异常可扩展性差,因为对锁的争夺很激烈。为了提高可扩
展性,我们引入了分页表锁。
有了分页表锁,我们就有了单独的每张表锁来顺序化对表的访问。目前,我们对PTE和
PMD表使用分页锁。对高层表的访问由mm->page_table_lock保护。
有一些辅助工具来锁定/解锁一个表和其他访问器函数:
- pte_offset_map_lock()
映射pte并获取PTE表锁,返回所取锁的指针;
- pte_unmap_unlock()
解锁和解映射PTE表;
- pte_alloc_map_lock()
如果需要的话,分配PTE表并获取锁,如果分配失败,返回已获取的锁的指针
或NULL;
- pte_lockptr()
返回指向PTE表锁的指针;
- pmd_lock()
取得PMD表锁,返回所取锁的指针。
- pmd_lockptr()
返回指向PMD表锁的指针;
如果CONFIG_SPLIT_PTLOCK_CPUS(通常为4)小于或等于NR_CPUS,则在编译
时启用PTE表的分页表锁。如果分页锁被禁用,所有的表都由mm->page_table_lock
来保护。
如果PMD表启用了分页锁,并且架构支持它,那么PMD表的分页锁就会被启用(见
下文)。
Hugetlb 和分页表锁
==================
Hugetlb可以支持多种页面大小。我们只对PMD级别使用分页锁,但不对PUD使用。
Hugetlb特定的辅助函数:
- huge_pte_lock()
对PMD_SIZE页面采取pmd分割锁,否则mm->page_table_lock;
- huge_pte_lockptr()
返回指向表锁的指针。
架构对分页表锁的支持
====================
没有必要特别启用PTE分页表锁:所有需要的东西都由pagetable_pte_ctor()
和pagetable_pte_dtor()完成,它们必须在PTE表分配/释放时被调用。
确保架构不使用slab分配器来分配页表:slab使用page->slab_cache来分配其页
面。这个区域与page->ptl共享存储。
PMD分页锁只有在你有两个以上的页表级别时才有意义。
启用PMD分页锁需要在PMD表分配时调用pagetable_pmd_ctor(),在释放时调
用pagetable_pmd_dtor()。
分配通常发生在pmd_alloc_one()中,释放发生在pmd_free()和pmd_free_tlb()
中,但要确保覆盖所有的PMD表分配/释放路径:即X86_PAE在pgd_alloc()中预先
分配一些PMD。
一切就绪后,你可以设置CONFIG_ARCH_ENABLE_SPLIT_PMD_PTLOCK。
注意:pagetable_pte_ctor()和pagetable_pmd_ctor()可能失败--必
须正确处理。
page->ptl
=========
page->ptl用于访问分割页表锁,其中'page'是包含该表的页面struct page。它
与page->private(以及union中的其他几个字段)共享存储。
为了避免增加struct page的大小并获得最佳性能,我们使用了一个技巧:
- 如果spinlock_t适合于long,我们使用page->ptr作为spinlock,这样我们
就可以避免间接访问并节省一个缓存行。
- 如果spinlock_t的大小大于long的大小,我们使用page->ptl作为spinlock_t
的指针并动态分配它。这允许在启用DEBUG_SPINLOCK或DEBUG_LOCK_ALLOC的
情况下使用分页锁,但由于间接访问而多花了一个缓存行。
PTE表的spinlock_t分配在pagetable_pte_ctor()中,PMD表的spinlock_t
分配在pagetable_pmd_ctor()中。
请不要直接访问page->ptl - -使用适当的辅助函数。
@@ -0,0 +1,133 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/mm/vmalloced-kernel-stacks.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
====================
支持虚拟映射的内核栈
====================
:作者: Shuah Khan <skhan@linuxfoundation.org>
.. contents:: :local:
概览
----
这是介绍 `虚拟映射内核栈功能 <https://lwn.net/Articles/694348/>` 的代码
和原始补丁系列的信息汇总。
简介
----
内核堆栈溢出通常难以调试,并使内核容易被(恶意)利用。问题可能在稍后的时间出现,使其难以
隔离和究其根本原因。
带有保护页的虚拟映射内核堆栈如果溢出,会被立即捕获,而不会放任其导致难以诊断的损
坏。
HAVE_ARCH_VMAP_STACK和VMAP_STACK配置选项能够支持带有保护页的虚拟映射堆栈。
当堆栈溢出时,这个特性会引发可靠的异常。溢出后堆栈跟踪的可用性以及对溢出本身的
响应取决于架构。
.. note::
截至本文撰写时, arm64, powerpc, riscv, s390, um, 和 x86 支持VMAP_STACK。
HAVE_ARCH_VMAP_STACK
--------------------
能够支持虚拟映射内核栈的架构应该启用这个bool配置选项。要求是:
- vmalloc空间必须大到足以容纳许多内核堆栈。这可能排除了许多32位架构。
- vmalloc空间的堆栈需要可靠地工作。例如,如果vmap页表是按需创建的,当堆栈指向
具有未填充页表的虚拟地址时,这种机制需要工作,或者架构代码(switch_to()和
switch_mm(),很可能)需要确保堆栈的页表项在可能未填充的堆栈上运行之前已经填
充。
- 如果堆栈溢出到一个保护页,就应该发生一些合理的事情。“合理”的定义是灵活的,但
在没有记录任何东西的情况下立即重启是不友好的。
VMAP_STACK
----------
VMAP_STACK bool配置选项在启用时分配虚拟映射的任务栈。这个选项依赖于
HAVE_ARCH_VMAP_STACK。
- 如果你想使用带有保护页的虚拟映射的内核堆栈,请启用该选项。这将导致内核栈溢出
被立即捕获,而不是难以诊断的损坏。
.. note::
使用KASAN的这个功能需要架构支持用真实的影子内存来支持虚拟映射,并且
必须启用KASAN_VMALLOC。
.. note::
启用VMAP_STACK时,无法在堆栈分配的数据上运行DMA。
内核配置选项和依赖性不断变化。请参考最新的代码库:
`Kconfig <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/Kconfig>`
分配方法
--------
当一个新的内核线程被创建时,线程堆栈是由页级分配器分配的虚拟连续的内存页组成。这
些页面被映射到有PAGE_KERNEL保护的连续的内核虚拟空间。
alloc_thread_stack_node()调用__vmalloc_node_range()来分配带有PAGE_KERNEL
保护的栈。
- 分配的堆栈被缓存起来,以后会被新的线程重用,所以在分配/释放堆栈给任务时,要手动
进行memcg核算。因此,__vmalloc_node_range被调用时没有__GFP_ACCOUNT。
- vm_struct被缓存起来,以便能够找到在中断上下文中启动的空闲线程。 free_thread_stack()
可以在中断上下文中调用。
- 在arm64上,所有VMAP的堆栈都需要有相同的对齐方式,以确保VMAP的堆栈溢出检测正常
工作。架构特定的vmap堆栈分配器照顾到了这个细节。
- 这并不涉及中断堆栈--参考原始补丁
线程栈分配是由clone()、fork()、vfork()、kernel_thread()通过kernel_clone()
启动的。留点提示在这,以便搜索代码库,了解线程栈何时以及如何分配。
大量的代码是在:
`kernel/fork.c <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/fork.c>`.
task_struct中的stack_vm_area指针可以跟踪虚拟分配的堆栈,一个非空的stack_vm_area
指针可以表明虚拟映射的内核堆栈已经启用。
::
struct vm_struct *stack_vm_area;
堆栈溢出处理
------------
前守护页和后守护页有助于检测堆栈溢出。当堆栈溢出到守护页时,处理程序必须小心不要再
次溢出堆栈。当处理程序被调用时,很可能只留下很少的堆栈空间。
在x86上,这是通过处理表明内核堆栈溢出的双异常堆栈的缺页异常来实现的。
用守护页测试VMAP分配
--------------------
我们如何确保VMAP_STACK在分配时确实有前守护页和后守护页的保护?下面的 lkdtm 测试
可以帮助检测任何回归。
::
void lkdtm_STACK_GUARD_PAGE_LEADING()
void lkdtm_STACK_GUARD_PAGE_TRAILING()
结论
----
- vmalloced堆栈的percpu缓存似乎比高阶堆栈分配要快一些,至少在缓存命中时是这样。
- THREAD_INFO_IN_TASK完全摆脱了arch-specific thread_info,并简单地将
thread_info(仅包含标志)和'int cpu'嵌入task_struct中。
- 一旦任务死亡,线程栈就可以被释放(无需等待RCU),然后,如果使用vmapped栈,就
可以将整个栈缓存起来,以便在同一cpu上重复使用。
@@ -0,0 +1,78 @@
:Original: Documentation/mm/zsmalloc.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
zsmalloc
========
这个分配器是为与zram一起使用而设计的。因此,该分配器应该在低内存条件下工作良好。特别是,
它从未尝试过higher order页面的分配,这在内存压力下很可能会失败。另一方面,如果我们只
是使用单(0-order)页,它将遭受非常高的碎片化 - 任何大小为PAGE_SIZE/2或更大的对象将
占据整个页面。这是其前身(xvmalloc)的主要问题之一。
为了克服这些问题,zsmalloc分配了一堆0-order页面,并使用各种"struct page"字段将它
们链接起来。这些链接的页面作为一个单一的higher order页面,即一个对象可以跨越0-order
页面的边界。代码将这些链接的页面作为一个实体,称为zspage。
为了简单起见,zsmalloc只能分配大小不超过PAGE_SIZE的对象,因为这满足了所有当前用户的
要求(在最坏的情况下,页面是不可压缩的,因此以"原样"即未压缩的形式存储)。对于大于这
个大小的分配请求,会返回失败(见zs_malloc)。
此外,zs_malloc()并不返回一个可重复引用的指针。相反,它返回一个不透明的句柄(无符号
长),它编码了被分配对象的实际位置。这种间接性的原因是zsmalloc并不保持zspages的永久
映射,因为这在32位系统上会导致问题,因为内核空间映射的VA区域非常小。因此,在使用分配
的内存之前,对象必须使用zs_map_object()进行映射以获得一个可用的指针,随后使用
zs_unmap_object()解除映射。
stat
====
通过CONFIG_ZSMALLOC_STAT,我们可以通过 ``/sys/kernel/debug/zsmalloc/<user name>``
看到zsmalloc内部信息。下面是一个统计输出的例子。::
# cat /sys/kernel/debug/zsmalloc/zram0/classes
class size almost_full almost_empty obj_allocated obj_used pages_used pages_per_zspage
...
...
9 176 0 1 186 129 8 4
10 192 1 0 2880 2872 135 3
11 208 0 1 819 795 42 2
12 224 0 1 219 159 12 4
...
...
class
索引
size
zspage存储对象大小
almost_empty
ZS_ALMOST_EMPTY zspage的数量(见下文)。
almost_full
ZS_ALMOST_FULL zspage的数量(见下图)
obj_allocated
已分配对象的数量
obj_used
分配给用户的对象的数量
pages_used
为该类分配的页数
pages_per_zspage
组成一个zspage的0-order页面的数量
当n <= N / f时,我们将一个zspage分配给ZS_ALMOST_EMPTYfullness组,其中
* n = 已分配对象的数量
* N = zspage可以存储的对象总数
* f = fullness_threshold_frac(即,目前是4个)
同样地,我们将zspage分配给:
* ZS_ALMOST_FULL when n > N / f
* ZS_EMPTY when n == 0
* ZS_FULL when n == N