透明大页支持

目标

处理大型内存工作集的性能关键型计算应用程序已经在 libhugetlbfs 和 hugetlbfs 之上运行。透明大页支持 (THP) 是一种使用大页来支持虚拟内存的替代方法,它支持页面大小的自动提升和降级,并且没有 hugetlbfs 的缺点。

目前,THP 仅适用于匿名内存映射和 tmpfs/shmem。但将来它可以扩展到其他文件系统。

注意

在下面的示例中,我们假设基本页面大小为 4K,大页大小为 2M,尽管实际数字可能因 CPU 架构而异。

应用程序运行速度更快的原因有两个因素。第一个因素几乎完全无关紧要,而且没有引起人们的重大兴趣,因为它也会带来在页面错误中需要更大的清除页面复制页面的缺点,这可能是一种负面影响。第一个因素包括为用户空间触摸的每个 2M 虚拟区域获取单个页面错误(从而将进入/退出内核的频率降低 512 倍)。这仅在内存映射生命周期内首次访问内存时才重要。第二个持久且更重要的因素将影响应用程序整个运行时对内存的所有后续访问。第二个因素包括两个组成部分

  1. TLB 未命中将运行得更快(尤其是在使用嵌套页表的虚拟化中,但几乎总是在没有虚拟化的裸机上也是如此)

  2. 单个 TLB 条目将映射更大的虚拟内存量,从而减少 TLB 未命中的次数。使用虚拟化和嵌套页表,只有当 KVM 和 Linux 客户机都使用大页时,TLB 才能映射更大的大小,但如果仅其中一个使用大页,则会发生显着的加速,这仅仅是因为 TLB 未命中将运行得更快。

现代内核支持“多大小 THP”(mTHP),它引入了以大于基本页面但小于传统 PMD 大小(如上所述)的块(以 2 的幂数个页面增量)分配内存的能力。mTHP 可以支持匿名内存(例如 16K、32K、64K 等)。这些 THP 继续以 PTE 映射,但在许多情况下仍然可以提供与上面概述的类似的好处:页面错误显着减少(例如,减少 4、8、16 等倍),但延迟峰值不那么明显,因为每个页面大小没有 PMD 大小的变体那么大,并且每个页面错误中需要清除的内存更少。一些架构还采用 TLB 压缩机制,以便在 PTE 集在虚拟和物理上连续并且适当对齐时压缩更多条目。在这种情况下,TLB 未命中将较少发生。

THP 可以在系统范围内启用,也可以限制为某些任务,甚至限制在任务地址空间内的内存范围。除非 THP 完全禁用,否则会有一个 khugepaged 守护程序扫描内存并将基本页面序列折叠为 PMD 大小的大页。

THP 的行为通过 sysfs 接口以及使用 madvise(2) 和 prctl(2) 系统调用进行控制。

与 hugetlbfs 的保留方法相比,透明大页支持通过允许所有未使用的内存用作缓存或其他可移动(甚至不可移动的实体)来最大限度地提高可用内存的利用率。它不需要预留来防止用户空间注意到大页分配失败。它允许在 hugepages 上使用分页和所有其他高级 VM 功能。它不需要修改应用程序即可利用它。

然而,可以进一步优化应用程序以利用此功能,例如,它们之前已经过优化,以避免为每个 malloc(4k) 产生大量的 mmap 系统调用。优化用户空间绝不是强制性的,即使对于处理大量内存的大页未知应用程序,khugepaged 也已经可以处理长期页面分配。

在某些情况下,当系统范围内启用大页时,应用程序可能会最终分配更多内存资源。应用程序可能会 mmap 一个很大的区域,但只接触其中 1 个字节,在这种情况下,可能会分配一个 2M 的页面而不是一个 4k 的页面,而没有任何好处。这就是为什么可以禁用系统范围的大页,并且只在 MADV_HUGEPAGE madvise 区域内使用它们的原因。

嵌入式系统应仅在 madvise 区域内启用大页,以消除浪费任何宝贵字节内存的风险,并且仅以更快的速度运行。

从大页中获得很多好处并且不会因使用大页而冒丢失内存的风险的应用程序,应在其关键 mmap 区域上使用 madvise(MADV_HUGEPAGE)。

sysfs

全局 THP 控制

匿名内存的透明大页支持可以完全禁用(主要用于调试目的),也可以仅在 MADV_HUGEPAGE 区域内启用(以避免消耗更多内存资源的风险),或者在系统范围内启用。可以使用以下方法之一为每个支持的 THP 大小实现此目的

echo always >/sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/enabled
echo madvise >/sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/enabled
echo never >/sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/enabled

其中 <size> 是正在寻址的大页大小,其可用大小因系统而异。

例如

echo always >/sys/kernel/mm/transparent_hugepage/hugepages-2048kB/enabled

或者,可以指定给定的大页大小将继承顶层“enabled”值

echo inherit >/sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/enabled

例如

echo inherit >/sys/kernel/mm/transparent_hugepage/hugepages-2048kB/enabled

顶层设置(与“inherit”一起使用)可以通过发出以下命令之一进行设置

echo always >/sys/kernel/mm/transparent_hugepage/enabled
echo madvise >/sys/kernel/mm/transparent_hugepage/enabled
echo never >/sys/kernel/mm/transparent_hugepage/enabled

默认情况下,PMD 大小的大页的 enabled=”inherit”,所有其他大页大小的 enabled=”never”。如果启用多个大页大小,内核将为给定的分配选择最合适的启用大小。

还可以限制 VM 中生成匿名大页的碎片整理工作,以防它们无法立即释放到 madvise 区域,或者永远不要尝试整理内存,除非立即有大页可用,否则只需回退到常规页面即可。显然,如果我们花费 CPU 时间来整理内存,我们希望通过稍后使用大页而不是常规页来获得更多收益。这不能始终保证,但如果分配是针对 MADV_HUGEPAGE 区域,则可能更有可能。

echo always >/sys/kernel/mm/transparent_hugepage/defrag
echo defer >/sys/kernel/mm/transparent_hugepage/defrag
echo defer+madvise >/sys/kernel/mm/transparent_hugepage/defrag
echo madvise >/sys/kernel/mm/transparent_hugepage/defrag
echo never >/sys/kernel/mm/transparent_hugepage/defrag
always

表示请求 THP 的应用程序将在分配失败时停止,并直接回收页面并压缩内存,以便立即分配 THP。这对于从 THP 使用中受益匪浅并愿意延迟 VM 启动以利用它们的虚拟机可能很理想。

defer

表示应用程序将在后台唤醒 kswapd 以回收页面,并唤醒 kcompactd 以压缩内存,以便在不久的将来可以使用 THP。然后,khugepaged 有责任稍后安装 THP 页面。

defer+madvise

将像 always 一样进入直接回收和压缩,但仅针对已使用 madvise(MADV_HUGEPAGE) 的区域;所有其他区域将在后台唤醒 kswapd 以回收页面,并唤醒 kcompactd 以压缩内存,以便在不久的将来可以使用 THP。

madvise

将像 always 一样进入直接回收,但仅针对那些已使用 madvise(MADV_HUGEPAGE) 的区域。这是默认行为。

从不。

应该是不言自明的。

默认情况下,内核尝试在匿名映射的读取页面错误时使用巨大的、PMD 可映射的零页面。可以通过写入 0 来禁用巨大的零页面,或者通过写入 1 来重新启用它。

echo 0 >/sys/kernel/mm/transparent_hugepage/use_zero_page
echo 1 >/sys/kernel/mm/transparent_hugepage/use_zero_page

某些用户空间(例如测试程序或优化的内存分配库)可能想知道 PMD 可映射透明大页的大小(以字节为单位)。

cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size

所有在页面错误和折叠时期的 THP 都将被添加到 _deferred_list 中,因此如果它们被认为是“未充分利用”的,则会在内存压力下被拆分。如果 THP 中的零填充页面的数量高于 max_ptes_none(见下文),则该 THP 被视为未充分利用。可以通过写入 0 到 shrink_underused 来禁用此行为,并通过写入 1 到 shrink_underused 来启用它。

echo 0 > /sys/kernel/mm/transparent_hugepage/shrink_underused
echo 1 > /sys/kernel/mm/transparent_hugepage/shrink_underused

当启用 PMD 大小的 THP 时(每个大小的匿名控制或顶层控制设置为“always”或“madvise”时),khugepaged 将自动启动,当禁用 PMD 大小的 THP 时(当每个大小的匿名控制和顶层控制都设置为“never”时),它将自动关闭。

Khugepaged 控制

注意

khugepaged 目前仅搜索折叠到 PMD 大小的 THP 的机会,并且没有尝试折叠到其他 THP 大小。

khugepaged 通常以低频率运行,因此虽然人们可能不想在页面错误期间同步调用碎片整理算法,但至少应该在 khugepaged 中调用碎片整理。但是,也可以通过写入 0 来禁用 khugepaged 中的碎片整理,或者通过写入 1 来启用 khugepaged 中的碎片整理。

echo 0 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag
echo 1 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag

您还可以控制 khugepaged 在每次扫描中应扫描的页面数。

/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan

以及 khugepaged 在每次扫描之间等待多少毫秒(您可以将其设置为 0,以使 khugepaged 以一个核心的 100% 利用率运行)。

/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs

以及如果发生大页分配失败,khugepaged 等待多少毫秒以限制下一次分配尝试。

/sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs

khugepaged 的进度可以在折叠的页面数中看到(请注意,此计数器可能不是折叠的页面数的精确计数,因为“折叠”可能意味着多种情况:(1)PTE 映射被 PMD 映射替换,或(2)所有 4K 物理页面被一个 2M 大页替换。每个都可能独立发生,或一起发生,具体取决于内存类型和发生的故障。因此,该值应大致解释为进度的标志,并查阅 /proc/vmstat 中的计数器以进行更准确的核算)。

/sys/kernel/mm/transparent_hugepage/khugepaged/pages_collapsed

对于每次扫描。

/sys/kernel/mm/transparent_hugepage/khugepaged/full_scans

max_ptes_none 指定在将一组小页面折叠成一个大页面时,可以分配多少额外的(尚未映射的)小页面。

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

更高的值会导致程序使用额外的内存。较低的值会导致较少的大页性能提升。max_ptes_none 的值只会浪费极少的 CPU 时间,您可以忽略它。

max_ptes_swap 指定在将一组页面折叠成一个透明大页时,可以从交换空间中调入多少页面。

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_swap

更高的值可能会导致过多的交换 IO 并浪费内存。较低的值可能会阻止 THP 被折叠,导致较少的页面被折叠成 THP,并降低内存访问性能。

max_ptes_shared 指定可以在多个进程之间共享多少页面。如果 THP 的任何页面被共享,khugepaged 可能会将 THP 的页面视为共享的。超过此数量将阻止折叠。

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_shared

更高的值可能会增加某些工作负载的内存占用。

启动参数

您可以通过将参数 transparent_hugepage=alwaystransparent_hugepage=madvisetransparent_hugepage=never 传递给内核命令行,来更改顶层 “enabled” 控制的 sysfs 启动时默认值。

或者,可以通过传递 thp_anon=<size>[KMG],<size>[KMG]:<state>;<size>[KMG]-<size>[KMG]:<state> 来控制每个受支持的匿名 THP 大小,其中 <size> 是 THP 大小(必须是 PAGE_SIZE 的 2 的幂次方,并且是受支持的匿名 THP),而 <state>alwaysmadviseneverinherit 之一。

例如,以下设置会将 16K、32K、64K THP 设置为 always,将 128K、512K 设置为 inherit,将 256K 设置为 madvise,将 1M、2M 设置为 never

thp_anon=16K-64K:always;128K,512K:inherit;256K:madvise;1M-2M:never

可以多次指定 thp_anon= 以根据需要配置所有 THP 大小。如果至少指定一次 thp_anon=,则命令行上未明确配置的任何匿名 THP 大小都将隐式设置为 never

transparent_hugepage 设置仅影响全局切换。如果未指定 thp_anon,则 PMD_ORDER THP 将默认为 inherit。但是,如果用户提供有效的 thp_anon 设置,则将覆盖 PMD_ORDER THP 策略。如果 PMD_ORDER 的策略未在有效的 thp_anon 中定义,则其策略将默认为 never

transparent_hugepage 类似,您可以使用内核参数 transparent_hugepage_shmem=<policy> 来控制内部 shmem 挂载的巨页分配策略,其中 <policy> 是 shmem 的七个有效策略之一(alwayswithin_sizeadviseneverdenyforce)。

thp_anon 控制每个受支持的匿名 THP 大小的方式相同,thp_shmem 控制每个受支持的 shmem THP 大小。thp_shmem 的格式与 thp_anon 相同,但也支持 within_size 策略。

可以多次指定 thp_shmem= 以根据需要配置所有 THP 大小。如果至少指定一次 thp_shmem=,则命令行上未明确配置的任何 shmem THP 大小都将隐式设置为 never

transparent_hugepage_shmem 设置仅影响全局切换。如果未指定 thp_shmem,则 PMD_ORDER 大页将默认为 inherit。但是,如果用户提供了有效的 thp_shmem 设置,则将覆盖 PMD_ORDER 大页策略。如果 PMD_ORDER 的策略未在有效的 thp_shmem 中定义,则其策略将默认为 never

tmpfs/shmem 中的大页

您可以使用挂载选项 huge= 来控制 tmpfs 中的大页分配策略。它可以具有以下值

always

每次需要新页面时尝试分配大页;

从不。

不分配大页;

within_size

仅当它完全在 i_size 内时才分配大页。还要遵循 fadvise()/madvise() 提示;

advise

仅当使用 fadvise()/madvise() 请求时才分配大页;

默认策略为 never

mount -o remount,huge= /mountpoint 在挂载后工作正常:重新挂载 huge=never 将不会尝试分解大页,只会阻止分配更多大页。

还有一个 sysfs 旋钮来控制内部 shmem 挂载的大页分配策略:/sys/kernel/mm/transparent_hugepage/shmem_enabled。该挂载用于 SysV SHM、memfds、共享匿名 mmap(来自 /dev/zero 或 MAP_ANONYMOUS)、GPU 驱动程序的 DRM 对象、Ashmem。

除了上面列出的策略之外,shmem_enabled 还允许另外两个值

deny

用于紧急情况,强制从所有挂载中关闭 huge 选项;

force

强制对所有挂载启用 huge 选项 - 对于测试非常有用;

Shmem 还可以通过添加一个新的 sysfs 旋钮来控制 mTHP 分配来使用“多大小 THP”(mTHP):'/sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/shmem_enabled',并且其对于每个 mTHP 的值与全局设置基本一致。添加了“inherit”选项以确保与这些全局设置的兼容性。相反,删除了“force”和“deny”选项,这些选项是旧时代的测试产物。

always

每次需要新页面时尝试分配 <size> 大页;

inherit

继承顶层 “shmem_enabled” 值。默认情况下,PMD 大小的大页具有 enabled=”inherit”,而所有其他大页大小都具有 enabled=”never”;

从不。

不分配 <size> 大页;

within_size

仅当 <size> 大页完全位于 i_size 内时才分配。同时尊重 fadvise()/madvise() 提示;

advise

仅当通过 fadvise()/madvise() 请求时才分配 <size> 大页;

需要重启应用程序

transparent_hugepage/enabled 和 transparent_hugepage/hugepages-<size>kB/enabled 的值以及 tmpfs 挂载选项仅影响未来的行为。因此,要使它们生效,您需要重启任何可能正在使用大页的应用程序。这也适用于 khugepaged 中注册的区域。

监控使用情况

系统当前使用的 PMD 大小的匿名透明大页的数量可以通过读取 /proc/meminfo 中的 AnonHugePages 字段获得。要确定哪些应用程序正在使用 PMD 大小的匿名透明大页,有必要读取 /proc/PID/smaps 并计算每个映射的 AnonHugePages 字段。(请注意,由于历史原因,AnonHugePages 仅适用于传统的 PMD 大小的 THP,并且应该被称为 AnonHugePmdMapped)。

映射到用户空间的文件透明大页的数量可以通过读取 /proc/meminfo 中的 ShmemPmdMapped 和 ShmemHugePages 字段获得。要确定哪些应用程序正在映射文件透明大页,有必要读取 /proc/PID/smaps 并计算每个映射的 FilePmdMapped 字段。

请注意,读取 smaps 文件开销很大,频繁读取会产生开销。

/proc/vmstat 中有许多计数器可用于监控系统成功提供大页以供使用的程度。

thp_fault_alloc

每次成功分配大页并将其用于处理缺页错误时递增。

thp_collapse_alloc

当 khugepaged 发现一系列页面可以合并为一个大页并成功分配一个新的大页来存储数据时递增。

thp_fault_fallback

如果缺页错误未能分配或使用大页,而是回退到使用小页时递增。

thp_fault_fallback_charge

如果缺页错误未能使用大页,而是回退到使用小页,即使分配成功也递增。

thp_collapse_alloc_failed

如果 khugepaged 发现一系列页面应该合并为一个大页,但分配失败时递增。

thp_file_alloc

每次成功分配 shmem 大页时递增(请注意,尽管以“file”命名,但计数器仅度量 shmem)。

thp_file_fallback

如果尝试分配 shmem 大页但失败,而是回退到使用小页时递增。(请注意,尽管以“file”命名,但计数器仅度量 shmem)。

thp_file_fallback_charge

如果 shmem 大页无法使用,而是回退到使用小页,即使分配成功也递增。(请注意,尽管以“file”命名,但计数器仅度量 shmem)。

thp_file_mapped

每次将文件或 shmem 大页映射到用户地址空间时递增。

thp_split_page

每次将大页拆分为基本页时递增。这可能由于多种原因发生,但常见的原因是大页已过时且正在被回收。此操作意味着拆分该页映射的所有 PMD。

thp_split_page_failed

如果内核无法拆分大页时递增。如果该页被某人固定,则可能发生这种情况。

thp_deferred_split_page

当大页被放入拆分队列时递增。当大页部分取消映射且拆分它会释放一些内存时会发生这种情况。拆分队列中的页面将在内存压力下进行拆分。

thp_underused_split_page

当拆分队列中的大页因使用不足而被拆分时递增。如果 THP 中零页的数量高于某个阈值 (/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none),则 THP 被认为使用不足。

thp_split_pmd

每次 PMD 拆分为 PTE 表时递增。例如,当应用程序对大页的一部分调用 mprotect() 或 munmap() 时,可能会发生这种情况。它不会拆分大页,只会拆分页表项。

thp_zero_page_alloc

每次成功分配用于 thp 的大型零页时递增。请注意,它不计算大型零页的每次映射,仅计算其分配。

thp_zero_page_alloc_failed

如果内核无法分配大型零页并回退到使用小页时递增。

thp_swpout

每次将大页整体换出而不拆分时递增。

thp_swpout_fallback

如果大页必须在换出之前拆分时递增。通常是因为未能为大页分配一些连续的交换空间。

在 /sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/stats 中,还有每个大页大小的单独计数器,可用于监控系统在提供大页以供使用方面的效率。每个计数器都有其对应的文件。

anon_fault_alloc

每次成功分配大页并将其用于处理缺页错误时递增。

anon_fault_fallback

如果缺页错误未能分配或使用大页,而是回退到使用较低阶的大页或小页时递增。

anon_fault_fallback_charge

如果缺页错误未能使用大页,而是回退到使用较低阶的大页或小页,即使分配成功也递增。

zswpout

每次将大页整体换出到 zswap 而不拆分时递增。

swpin

每次将大页从非 zswap 交换设备整体换入时递增。

swpout

每次将大页整体换出到非 zswap 交换设备而不拆分时递增。

swpout_fallback

如果大页必须在换出之前拆分时递增。通常是因为未能为大页分配一些连续的交换空间。

shmem_alloc

每次成功分配 shmem 大页时递增。

shmem_fallback

如果尝试分配 shmem 大页但失败,而是回退到使用小页时递增。

shmem_fallback_charge

如果 shmem 大页无法使用,而是回退到使用小页,即使分配成功也递增。

split

每次成功将大页拆分为较小的阶数时递增。这可能由于多种原因发生,但常见的原因是大页已过时且正在被回收。

split_failed

如果内核无法拆分大页时递增。如果该页被某人固定,则可能发生这种情况。

split_deferred

当大页被放入拆分队列时递增。当大页部分取消映射且拆分它会释放一些内存时会发生这种情况。拆分队列中的页面将在内存压力下进行拆分(如果拆分可能)。

nr_anon

系统中我们拥有的匿名 THP 的数量。这些 THP 可能当前完全映射,或者具有部分未映射/未使用的子页。

nr_anon_partially_mapped

可能部分映射、可能浪费内存并且已排队等待延迟内存回收的匿名 THP 的数量。请注意,在某些极端情况下(例如,迁移失败),我们可能会将匿名 THP 检测为“部分映射”并在此处对其进行计数,即使它实际上不再是部分映射的。

随着系统的老化,分配大页的成本可能很高,因为系统会使用内存压缩在内存中复制数据,以释放一个大页以供使用。/proc/vmstat 中有一些计数器可以帮助监控此开销。

compact_stall

每次进程停止运行内存压缩以使大页可供使用时递增。

compact_success

如果系统压缩内存并释放大页以供使用,则递增。

compact_fail

如果系统尝试压缩内存但失败,则递增。

可以使用函数跟踪器记录在 __alloc_pages() 中花费的时间,并使用 mm_page_alloc 跟踪点来识别哪些分配用于大页,从而确定停顿的时间长度。

优化应用程序

为了保证内核将在任何内存区域立即映射 THP,mmap 区域必须与大页自然对齐。posix_memalign() 可以提供该保证。

Hugetlbfs

您可以在启用了透明大页支持的内核上像往常一样正常使用 hugetlbfs。除了整体碎片更少之外,hugetlbfs 中没有其他区别。hugetlbfs 的所有常用功能都将保留且不受影响。libhugetlbfs 也将像往常一样正常工作。