内存管理-72-THP-1-理论

一、transhuge.rst 翻译

注: 翻译自 /kernel-6.1.114/Documentation/admin-guide/mm/transhuge.rst

透明大页(THP)支持

1. 目标

面向大内存工作集的性能敏感型应用,过去通常运行在 libhugetlbfs 之上,进而使用 hugetlbfs。透明大页支持(Transparent HugePage, THP)是另一种为虚拟内存提供大页后端的方式:它支持页大小的自动提升与降级,并且避免了 hugetlbfs 的一些不足。

目前 THP 主要用于匿名内存映射以及 tmpfs/shmem,未来也可能扩展到其他文件系统。

note:: 下面示例默认基础页大小为 4K、大页大小为 2M;实际数值会随 CPU 架构而变化。

应用之所以会更快,主要有两个因素。第一个因素几乎不那么关键,而且可能带来副作用:在缺页时需要更大的清零页/拷贝页开销,这可能是负面影响。该因素是:用户态每触达一个 2M 虚拟区域只需一次缺页处理(内核进出频率理论上降低 512 倍)。它只在某个映射生命周期内“首次访问”时起作用。

第二个因素持续时间更长,也更重要,会影响应用后续整个运行期的访问,包含两部分:

(1) TLB miss 的处理会更快(尤其在虚拟化嵌套页表场景中;在裸机场景通常也会受益).

(2) 单个 TLB 表项可映射更大的虚拟内存,TLB miss 次数因此下降。在虚拟化嵌套页表场景下,只有当 KVM 与 Linux guest 同时使用大页时,TLB 映射范围才会进一步变大;但即便两者仅一方使用大页,通常也能显著加速,因为 TLB miss 本身会更快。

THP 可以全局启用,也可以限制到某些进程,甚至限制到进程地址空间中的特定区间。只要 THP 没被完全关闭,系统就会有 khugepaged 守护线程扫描内存,并把连续的基础页折叠为大页。

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

相较 hugetlbfs 的“预留”模式,透明大页能最大化空闲内存的利用价值:未使用内存可作为页缓存或其他可移动(甚至不可移动)对象使用。它不要求提前预留来避免用户态可感知的大页分配失败;还能在大页上保留分页与其他高级 VM 特性;并且应用无需修改即可受益。

当然,应用仍可进一步优化以放大收益。就像过去避免为每次 malloc(4k) 都触发大量 mmap 系统调用一样,用户态优化并非必须,但依然有价值。即使应用本身不感知大页,khugepaged 也能为长生命周期的大内存分配做折叠处理。

在某些全局开启大页的场景下,应用可能消耗更多内存。例如应用 mmap 了大区间却只触碰其中 1 字节,此时可能分配 2M 页而不是 4K 页,造成浪费。因此内核支持系统范围关闭大页,仅在 MADV_HUGEPAGE 的 madvise 区域启用

嵌入式系统通常建议仅在 madvise 区域启用大页,以降低宝贵内存被浪费的风险,同时获取性能收益。

对于能显著受益且不会因大页导致明显内存浪费的应用,建议在关键 mmap 区域调用 madvise(MADV_HUGEPAGE)。


2. sysfs

2.1 全局 THP 控制

匿名内存的 THP 可以被完全关闭(多用于调试)、仅在 MADV_HUGEPAGE 区域启用(降低内存额外消耗风险),或系统范围启用。可通过如下方式设置::

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

还可以限制 VM 在“立即没有可用匿名大页”时为生成大页而做的内存整理(defrag)强度:可仅对 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、且愿意接受启动时延迟的虚拟机场景可能更合适。

defer: 表示应用仅在后台唤醒 kswapd 回收页面、唤醒 kcompactd 压缩内存,使 THP 在“近期”可用;之后由 khugepaged 再完成 THP 安装。

defer+madvise: 对 madvise(MADV_HUGEPAGE) 区域采用类似 always 的直接回收/压缩;其余区域仅后台唤醒 kswapd/kcompactd,为后续 THP 创造条件。

madvise: 与 always 类似进入直接回收,但仅对 madvise(MADV_HUGEPAGE) 区域生效。这是默认行为。

never: 顾名思义,不做该类积极整理。


默认情况下,匿名映射在读缺页时内核会尝试使用 huge zero page。可写 0 关闭,写 1 恢复::

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

某些用户态(如测试程序或优化过的内存分配库)可能希望知道透明大页大小(字节)::

cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size

当 transparent_hugepage/enabled 设为 "always" 或 "madvise" 时,khugepaged 线程会自动启动;设为 "never" 时会自动停止。


2.2 khugepaged 控制

khugepaged 通常低频运行。也就是说,即便你不希望在缺页路径同步执行 defrag,通常也值得至少在 khugepaged 里执行 defrag。你可以通过写 0/1 关闭或开启它::

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

以及每轮扫描之间休眠多少毫秒(设为 0 可使 khugepaged 近似占满一个核)::

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

以及在一次大页分配失败后,khugepaged 额外休眠多少毫秒来节流下次尝试::

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

khugepaged 进度可通过 pages_collapsed 观察(注意:它不一定是"折叠(collapsed)页数"的精确计数,因为"collapsed"可能指代多种动作,例如:
(1) PTE 映射被 PMD 映射替换;
(2) 全部 4K 物理页被 1 个 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

值更高:程序可能使用更多额外内存。
值更低:THP 性能收益可能变小。
通常它造成的 CPU 浪费很小,可不用过度关注。


max_ptes_swap 指定折叠一组页面为透明大页时,最多允许从 swap 换回多少页::

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

值更高:可能带来较重 swap IO 并增加内存占用。
值更低:可能阻碍 THP 折叠,导致折叠数量减少,内存访问性能下降。


max_ptes_shared 指定在候选区间中最多允许多少页被多进程共享;超过该值将阻止折叠::

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

值更高:在某些负载下可能增加内存占用。


3. 启动参数

你可以通过内核命令行参数改变 THP 的 sysfs 启动默认值:transparent_hugepage=always、transparent_hugepage=madvise 或 transparent_hugepage=never。


4. tmpfs/shmem 中的大页

你可以用 tmpfs 挂载选项 huge= 控制大页分配策略,可选值如下:

always: 每次需要新页时都尝试分配大页;
never: 不分配大页;
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、memfd、共享匿名映射(/dev/zero 或 MAP_ANONYMOUS)、GPU 驱动 DRM 对象、Ashmem。

除上述策略外,shmem_enabled 还支持两个额外值:

deny: 应急开关:强制关闭所有挂载点的大页选项;
force: 强制对所有挂载点开启大页,测试时非常有用;


5. 是否需要重启应用

transparent_hugepage/enabled 与 tmpfs 的挂载选项只影响“未来行为”。因此要让新策略生效,需要重启可能使用大页的应用。这同样适用于已经被 khugepaged 注册处理的区域。


6. 使用情况监控

# cat /proc/2951/smaps | grep Huge
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
AnonHugePages:      2048 kB

# cat /proc/meminfo | grep -E "Huge|Shmem"
Shmem:             13068 kB
AnonHugePages:   1708032 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
FileHugePages:         0 kB

系统当前使用的匿名透明大页数量,可从 /proc/meminfo 的 AnonHugePages 读取。若要定位“哪些应用在使用匿名 THP”,需要读取 /proc/PID/smaps,并汇总每个映射的 AnonHugePages 字段。

映射到用户态的文件型透明大页数量,可从 /proc/meminfo 的 ShmemPmdMapped 与 ShmemHugePages 读取。若要定位“哪些应用在映射文件 THP”,需要读取 /proc/PID/smaps,并汇总每个映射的 FileHugeMapped 字段。

注意:读取 smaps 成本较高,频繁读取会带来额外开销。


/proc/vmstat 中有一组计数器可用于观察系统提供 THP 的成功程度。

# cat /proc/vmstat | grep thp
thp_migration_success 0
thp_migration_fail 0
thp_migration_split 0
thp_fault_alloc 13779
thp_fault_fallback 11331
thp_fault_fallback_charge 0
thp_collapse_alloc 160
thp_collapse_alloc_failed 51
thp_file_alloc 0
thp_file_fallback 0
thp_file_fallback_charge 0
thp_file_mapped 0
thp_split_page 146
thp_split_page_failed 0
thp_deferred_split_page 5667
thp_split_pmd 9051
thp_scan_exceed_none_pte 219
thp_scan_exceed_swap_pte 751
thp_scan_exceed_share_pte 121
thp_zero_page_alloc 1
thp_zero_page_alloc_failed 0
thp_swpout 856
thp_swpout_fallback 0

thp_fault_alloc: 每次为处理缺页而成功分配一个大页时递增。

thp_collapse_alloc: khugepaged 发现可折叠区间并成功分配新大页承载数据时递增。

thp_fault_fallback: 缺页分配大页失败并回退到小页时递增。

thp_fault_fallback_charge: 缺页路径对大页 charge 失败(即使分配本身成功)并回退到小页时递增。

thp_collapse_alloc_failed: khugepaged 找到可折叠区间但分配大页失败时递增。

thp_file_alloc: 每次成功分配文件大页时递增。

thp_file_fallback: 尝试分配文件大页失败并回退到小页时递增。

thp_file_fallback_charge: 文件大页无法 charge(即使分配成功)并回退到小页时递增。

thp_file_mapped: 每次文件大页映射进用户地址空间时递增。

thp_split_page: 每次把大页拆成基础页时递增。原因很多,常见是大页变旧后被回收。该动作意味着该页关联的所有 PMD 映射都会被拆分。

thp_split_page_failed: 内核拆分大页失败时递增。常见原因是该页被 pin 住。

thp_deferred_split_page: 大页被放入延迟拆分队列时递增。通常发生在部分取消映射后,拆分可释放一部分内存;队列中的页会在内存压力下被拆分。

thp_split_pmd: 每次 PMD 被拆成 PTE 表时递增。例如应用在大页的一部分上执行 mprotect() 或
munmap()。这只拆页表项,不一定拆物理大页。

thp_zero_page_alloc: 每次 THP 使用的 huge zero page 被成功分配时递增。注意它统计的是“分配次数”,不是“映射次数”。

thp_zero_page_alloc_failed: 内核分配 huge zero page 失败并回退到小页时递增。

thp_swpout: 每次大页不拆分、整体 swapout 时递增。

thp_swpout_fallback: 大页在 swapout 前不得不先拆分时递增。通常因为无法为该大页分配连续 swap 空间。


随着系统运行时间增长,分配大页可能变贵,因为系统需要通过内存压缩搬迁数据,腾出可用大页。/proc/vmstat 中还有一些计数器可观察这类开销。

# cat /proc/vmstat | grep compact
compact_stall 207
compact_fail 206
compact_success 1

compact_stall: 进程因执行内存压缩而发生阻塞(以便腾出可用大页)时递增。

compact_success: 内存压缩成功并腾出可用大页时递增。

compact_fail: 系统尝试内存压缩但失败时递增。

如果希望评估“阻塞持续了多久”,可用 function tracer 记录 __alloc_pages() 耗时,再结合 mm_page_alloc tracepoint 判断哪些分配请求是大页请求。


7. 应用优化建议

若要保证内核在某个内存区间内“立即映射 2M 页”,该 mmap 区间需要天然按大页对齐。用户空间的 posix_memalign() 可以提供这种保证。


8. Hugetlbfs

在启用透明大页支持的内核上,仍可像以往一样使用 hugetlbfs。hugetlbfs 的行为不会被 THP 改变,除“整体碎片通常更少”外几乎无差异。hugetlbfs 的既有特性都保留且不受影响,libhugetlbfs 也可照常工作。


二、相关文件节点

上面翻译中对部分文件已经有详细介绍了,这里作为补充。

基于 Linux-6.1.114

/sys/kernel/mm/transparent_hugepage # ls -l
-rw-r--r-- 1 root root 4096 2010-01-01 10:18 defrag //[madvise]
-rw-r--r-- 1 root root 4096 2010-01-01 09:37 enabled //[always]
-r--r--r-- 1 root root 4096 2010-01-01 10:18 hpage_pmd_size //2097152
drwxr-xr-x 2 root root    0 2010-01-01 10:18 khugepaged
-rw-r--r-- 1 root root 4096 2010-01-01 10:18 shmem_enabled //[never]
-rw-r--r-- 1 root root 4096 2010-01-01 10:18 use_zero_page //1

/sys/kernel/mm/transparent_hugepage/khugepaged # ls -l
-rw-r--r-- 1 root root 4096 2010-01-01 10:19 alloc_sleep_millisecs //60000
-rw-r--r-- 1 root root 4096 2010-01-01 10:19 defrag //1
-r--r--r-- 1 root root 4096 2010-01-01 10:19 full_scans //1
-rw-r--r-- 1 root root 4096 2010-01-01 10:19 max_ptes_none //511
-rw-r--r-- 1 root root 4096 2010-01-01 10:19 max_ptes_shared //256
-rw-r--r-- 1 root root 4096 2010-01-01 10:19 max_ptes_swap //64
-r--r--r-- 1 root root 4096 2010-01-01 10:19 pages_collapsed //16
-rw-r--r-- 1 root root 4096 2010-01-01 10:19 pages_to_scan //4096
-rw-r--r-- 1 root root 4096 2010-01-01 10:19 scan_sleep_millisecs //10000

用下面命令看各个文件内容: for F in `ls`; do C=`cat $F`; print $F = $C; done


2.1 transparent_hugepage 目录下


1. defrag
/sys/kernel/mm/transparent_hugepage # cat defrag
always defer defer+madvise [madvise] never

作用:控制在 THP 分配时是否允许做更激进的碎片整理(直接回收/压缩等)。取值见cat内容,值越激进,THP 成功率越高,但分配延迟抖动风险越大。


2. enabled
/sys/kernel/mm/transparent_hugepage # cat enabled
[always] madvise never

作用:控制匿名内存是否启用 THP(是否优先用 PMD 级大页,通常是 2MB)。常见取值:
always: 更激进,命中率高,但分配/回收压力可能上升。
madvise: 仅对 madvise(HUGEPAGE) 区域积极启用,较稳妥。
never: 关闭匿名 THP,减少大页相关开销但可能损失 TLB 性能。


3. hpage_pmd_size

只读,显示 PMD 级 THP 的大小,单位字节。典型值为 2097152 (2MB,4K 基页系统常见)。可用于确认你的 THP 大页粒度。


4. shmem_enabled
/sys/kernel/mm/transparent_hugepage # cat shmem_enabled
always within_size advise [never] deny force

控制 tmpfs/shmem(例如共享内存、某些匿名文件页)是否启用 THP。对图形/多媒体/共享内存场景可能影响较大,建议结合业务压测决定。


5. use_zero_page

是否使用 huge zero page(只读全零大页)优化。常见值:0 或 1(默认)。开启可减少小页映射和缺页开销,但在某些负载下也可能有副作用,如额外内存行为复杂度。


6. khugepaged

khugepaged 后台线程相关参数与统计,负责把可折叠的小页集合并成 THP。


2.2 transparent_hugepage/khugepaged

1. alloc_sleep_millisecs

当折叠时分配大页失败后,khugepaged 的休眠间隔。调大,失败后退让更多,减少抖动。调小,更快重试,可能增加系统压力。


2. defrag

khugepaged 线程在折叠过程中是否允许做更激进整理。通常是 0/1 开关(默认是1)。开启有助于折叠成功,但可能增加后台回收/压缩压力。


3. full_scans

只读,khugepaged 完整扫描轮次计数。用于与 pages_collapsed 联合看效率,每轮扫描换来多少折叠收益。


4. max_ptes_none

一个候选 2MB 区间中,允许为 none(未映射)的 PTE 最大数量。允许一定"空洞"仍可折叠,阈值越大越宽松。


5. max_ptes_shared

候选区间允许共享映射 PTE 的最大数量。控制共享页场景下是否继续折叠,避免不合适区域被强行合并。


6. max_ptes_swap

候选区间允许处于 swap 状态的 PTE 最大数量。允许一定换出页参与折叠,过大可能带来更多换入成本。


7. pages_collapsed

只读,累计成功折叠成 THP 的页数/次数统计。可用于观察调参后折叠效果是否提升。


8. pages_to_scan

每轮扫描尝试检查的页数。调大会折叠更积极,但后台 CPU 占用可能上升。调小会更温和,收敛更慢。


9. scan_sleep_millisecs

两轮扫描之间休眠时间。调小,扫描更频繁、CPU 背景开销增大。调大,扫描更稀疏、THP 收敛变慢。


2.3 实战技巧

(1) 追求稳定延迟优先: 通常从 enabled=madvise、较保守 defrag 策略起步。
(2) 追求吞吐/TLB 命中优先: 可尝试更积极 enabled/defrag,并观察卡顿与回收压力。

评估是否"调得值",重点看三类指标:
(1) pages_collapsed 是否明显增长。
(2) 前台延迟是否变差(jank/卡顿)。
(3) 回收和 compaction 压力是否上升(vmstat、trace、内核日志)。

 

posted on 2026-07-17 21:03  Hello-World3  阅读(3)  评论(0)    收藏  举报

导航