第七十七章:透明大页
元婴后期涉及内核源码:
一
林小源站在 TLB 墙壁前,看着那些密密麻麻的小格子——512 个 4KB 的 TLB 条目,像一面墙上的砖块。
"每个格子只能映射一个 4KB 的页面。"他自言自语,"512 个格子,总共 2MB。"
"对。"一个声音从头顶传来。
林小源抬头,看到一个年轻人悬浮在半空中。年轻人穿着一身半透明的银色长袍,袍子上印着 512 个小方格,每个方格都微微发光。
"我是 THP,透明大页。"年轻人说,"你知道传统大页需要应用程序显式使用——、——这对应用程序不透明。我解决这个问题。"
"怎么解决?"
年轻人从袍子上撕下一块——那 512 个小方格瞬间合并成一个巨大的金色方块。"我让内核自动把 512 个连续的 4KB 小页合并成一个 2MB 大页。TLB 只需要一个条目就能映射整个大页——从 512 次 TLB 查找变成 1 次。"
林小源感觉到脚下的地面震动了一下。TLB 墙壁上的砖块开始移动,512 个小砖块像拼图一样拼合成一个大砖块。墙壁的面积没变,但覆盖的内存范围大了 512 倍。
"应用程序不需要知道?"
"完全透明。"年轻人说,"应用程序只管分配 4KB 的页面,我在后台自动合并。这就是'透明'的意思。"
"但透明不等于强迫。"年轻人把金色方块放回地面,"THP 的设计原则叫 graceful fallback:如果 2MB 大页因为碎片分配失败,内核应该优雅退回普通 4KB 页,不让用户空间看见失败,也不让路径出现显著延迟。同一个 VMA 里,大页和小页可以混在一起。"
"所以大页是机会,不是契约。"
"对。能用就用,不能用就退。"
透明初试
THP 分配大页失败时应优雅退回普通页,这个设计原则叫什么?
二
林小源注意到,在 TLB 墙壁的远处,有一个影子在不停地移动。它走得很慢,但每走到一处,就停下来检查地面上的小页面,然后把符合条件的合并成大页。
"那是 。"年轻人说,"内核的透明大页守护进程。它在后台扫描内存,找到连续的小页面,自动提升为大页。"
"为什么不立刻合并?"
"条件不够。"年轻人说,"只有当 512 个连续的 4KB 页面都存在、都被访问过、都没有被钉住的时候,才能合并成一个大页。khugepaged 需要检查这些条件。"
林小源看着 khugepaged 的影子。它走得很慢,但从未停止。
"如果大页需要回收怎么办?"
"自动降级。"年轻人说,"当内核需要回收大页中的部分内存时,会自动把大页拆分成小页。拆分之后,TLB 条目也跟着变——一个大条目变成 512 个小条目。"
"拆分也有边界。"年轻人蹲下,指着大页中央一枚被钉住的小符,"如果有人通过 get_user_pages() 或 pin_user_pages() 钉住了 THP,引用会落在 head 或 tail page 上;普通 I/O 只关心物理地址时通常没问题,但若驱动乱翻 tail page 的 struct page 字段,就可能读错语义。更重要的是,split_huge_page() 遇到额外 pin 可能失败。"
"那页表里的 huge PMD 呢?"
"代码走页表时,如果不懂 huge PMD,可以先 split_huge_pmd() 退回 PTE 表。真正 hugepage-aware 的代码,要在 mmap_lock 和页表锁保护下重新检查 pmd_trans_huge(),防止并发拆分或合并把脚下地形换掉。"
拆页之试
THP 被 GUP/pin_user_pages 额外钉住时,可能失败的物理大页拆分函数是什么?
三
林小源坐在一块大页上,思考着。
"听起来很完美。"他说。
年轻人笑了,但笑容里带着一丝苦涩。"没有完美的东西。THP 的问题比你想象的多。"
"什么问题?"
"延迟波动。"年轻人说,"合并一个大页需要复制 512 个小页的内容,拆分也一样。这个过程可能需要几毫秒——对于延迟敏感的应用来说,几毫秒就是灾难。"
他顿了顿,又说:"还有内存浪费。如果你只需要 1 个字节,但分配了一个 2MB 的大页,剩下的 2MB-1 字节都是浪费。这叫内部碎片。"
"所以有些系统禁用了 THP。"
"对。"年轻人说,"数据库、交易系统——那些对延迟零容忍的应用,通常选择禁用 THP。 模式。"
林小源站起来,看着远处 khugepaged 的影子。它仍在不停地扫描、合并、拆分。
"还有部分释放。"年轻人说,"如果 只释放 THP 中的一部分匿名子页,内核通常不会立刻拆掉整块大页;它会把 folio 放进 deferred split 队列,等内存压力来时再通过 shrinker 拆分,释放真正不用的子页。立刻拆分看似干净,却可能在退出路径和锁上下文里制造更大成本。"
"所以透明大页不是一个开关,而是一套和回收、swap、GUP、页表遍历互相妥协的体系。"
他琢磨了一下,忽然明白——"透明"不等于"免费"。
延迟拆分
匿名 THP 部分 unmap 后,内核可能先放入哪个延迟拆分队列,等内存压力再处理?
printf("=== 透明大页 — 自动的大页优化 ===\n\n");
printf("传统大页 vs 透明大页:\n");
printf(" 传统大页:\n");
printf(" 需要应用程序显式使用\n");
printf(" hugetlbfs 或 MAP_HUGETLB\n");
printf(" 预先分配\n\n");
printf(" 透明大页:\n");
printf(" 自动使用,不需要修改\n");
printf(" 内核自动合并/拆分\n");
printf(" 按需分配\n\n");
printf("--- THP 的工作流程 ---\n");
printf("1. 进程分配内存\n");
printf(" 内核分配 4KB 小页\n\n");
printf("2. khugepaged 扫描\n");
printf(" 检测连续的小页\n\n");
printf("3. 自动提升\n");
printf(" 合并 512 个小页 → 1 个 2MB 大页\n\n");
printf("4. 自动降级\n");
printf(" 需要回收时,拆分大页\n\n");
printf("--- THP 模式 ---\n");
printf("always: 总是使用 THP\n");
printf("madvise: 只对 MADV_HUGEPAGE 区域使用\n");
printf("never: 不使用 THP\n\n");
printf("--- THP 的问题 ---\n");
printf("1. 延迟波动: 合并/拆分需要时间\n");
printf("2. 内存浪费: 内部碎片\n");
printf("3. 不适合所有工作负载\n");#include <stdio.h>
int main() {
printf("=== 透明大页 — 自动的大页优化 ===\n\n");
printf("传统大页 vs 透明大页:\n");
printf(" 传统大页:\n");
printf(" 需要应用程序显式使用\n");
printf(" hugetlbfs 或 MAP_HUGETLB\n");
printf(" 预先分配\n\n");
printf(" 透明大页:\n");
printf(" 自动使用,不需要修改\n");
printf(" 内核自动合并/拆分\n");
printf(" 按需分配\n\n");
printf("--- THP 的工作流程 ---\n");
printf("1. 进程分配内存\n");
printf(" 内核分配 4KB 小页\n\n");
printf("2. khugepaged 扫描\n");
printf(" 检测连续的小页\n\n");
printf("3. 自动提升\n");
printf(" 合并 512 个小页 → 1 个 2MB 大页\n\n");
printf("4. 自动降级\n");
printf(" 需要回收时,拆分大页\n\n");
printf("--- THP 模式 ---\n");
printf("always: 总是使用 THP\n");
printf("madvise: 只对 MADV_HUGEPAGE 区域使用\n");
printf("never: 不使用 THP\n\n");
printf("--- THP 的问题 ---\n");
printf("1. 延迟波动: 合并/拆分需要时间\n");
printf("2. 内存浪费: 内部碎片\n");
printf("3. 不适合所有工作负载\n");
return 0;
}道藏笔记
内核启示
透明大页(THP)让内核自动使用大页。
THP 的工作原理:
- 自动提升:合并连续小页为大页
- 自动降级:需要时拆分大页为小页
- 后台扫描和合并
THP 的模式:
- — 总是使用
- — 只对标记区域使用
- — 不使用
THP 的优势:
- 不需要应用程序修改
- 自动优化 TLB 性能
THP 的问题:
- 延迟波动
- 内存浪费
- 不适合所有工作负载
- 大页失败要 graceful fallback 到普通页
- GUP/pinned pages 会影响拆分
- 部分 unmap 可能走 deferred split,等内存压力再释放子页
THP 是"自动优化"——但自动不一定最优。
透明大页之试
透明大页需要后台把普通页合并成大页时,本章提到的守护进程叫什么?