Skip to content

第七十二章:大页

元婴后期

涉及内核源码:

林小源站在内景中,面前是一面巨大的墙。

不是普通的墙——这面墙由无数个细小的方格组成,每个方格只有指甲盖大小,密密麻麻地排列着,一直延伸到看不见的高处。每个方格里都刻着一串数字——地址、权限、物理页帧号。

"页表。"他认出来了。

"准确说,是 TLB。" mm_struct 的声音从背后传来,"你看到的每个方格,都是 TLB 的一个条目。翻译过的地址缓存在这里——下次访问同一个页面时,直接从这里取结果。"

林小源伸手数了数。一排有三十二个方格,一共有……三十二排。

"一千零二十四个。" mm_struct 说,"TLB 的条目数量就这么多。"

"才这么点?"

"就这么多。" mm_struct 的声音里没有波动,"如果你用 4KB 的页面,一千零二十四个条目只能覆盖 4MB 内存。你的堆有 128MB——光是翻译地址,TLB 就要不停地换入换出。"

林小源看着那面墙。他注意到有些方格在不停地闪烁——旧的被擦掉,新的被写入。频率快得让人眼花缭乱。

"这就是 TLB 压力。" mm_struct 说,"每次 TLB 缺失,硬件就要去查三级页表——三次内存访问。你的程序跑得越快,TLB 缺失越频繁,性能就越差。"

林小源皱眉。"有没有办法……让每个条目覆盖更大的范围?"

mm_struct 没有回答。但林小源感觉到脚下的地面在震动——不是地震,而是某种更大的东西正在靠近。

他转过头,看到远处的地平线上,有什么东西在升起。

那是一块巨大的、平整的石板。不是他熟悉的 4KB 小方格——这块石板有整座城市那么大,表面光滑如镜,散发着沉稳的金色光芒。

"大页。" mm_struct 终于开口了,声音里带着一丝……林小源不确定那是什么,也许是敬意。"2MB 的大页。一个条目覆盖 2MB——比 4KB 大五百一十二倍。"

林小源走到那块石板跟前。石板的表面刻着一行字:0x00000000 - 0x00200000,权限 ,一个巨大的 PPN。

"用 2MB 大页," mm_struct 说,"一千零二十四个 TLB 条目可以覆盖 2GB 内存。比 4KB 页面提高了五百一十二倍。"

林小源踩上石板。脚下的感觉完全不同——不是踩在一块块拼接的小砖上,而是踩在一整块坚实的岩石上。没有缝隙,没有接缝,稳如磐石。

"他琢磨了一下——大页是 TLB 友好的。"

但石板并不是完美的。

林小源沿着石板走了几步,发现它的边缘有些粗糙。仔细一看——石板的末端有一大片区域是暗灰色的,没有任何数据。

"那片灰色的是什么?"

"浪费。" mm_struct 的声音冷了下来,"你的进程只需要 1MB 内存,但大页的最小单位是 2MB。剩下的 1MB 就这样空着——不能给别的进程用,也不能回收。"

林小源蹲下来,敲了敲那片灰色区域。空洞的回响,像敲在空箱子上。

"内部碎片。" mm_struct 说,"大页的代价。页面越大,浪费的可能性就越高。如果你分配了一百个 2MB 大页,每个只用了 1MB——你浪费了 100MB。"

林小源站起来,望向远处。在大页石板的旁边,他看到了另一些更巨大的东西——像一座座山峰,每座山峰都有 1GB 那么大。

"那些也是大页?"

"1GB 大页。" mm_struct 说,"只用一级页表,TLB 条目只需要一个就能覆盖 1GB。但代价更大——需要连续的 1GB 物理内存,而且一旦分配,这 1GB 就被锁死了。"

林小源看着那些山峰。它们宏伟、壮观,但山脚下堆满了碎石——那是被挤压出去的小页面,因为连续的物理内存被大页占用了。

"分配大页容易吗?"他问。

"不容易。" mm_struct 说,"大页需要连续的物理内存。系统运行久了,内存碎片化严重,想找一块连续的 2MB 或 1GB 区域——就像在挤满人的广场上找一块连续的空地。"

"所以要在系统启动时预留?"

"对。 是专门的文件系统,启动时预留大页池。" mm_struct 的声音变得严肃,"还有一种方式——THP,透明大页。内核自动在运行时尝试使用大页,不需要应用程序修改。但 THP 有自己的问题——延迟抖动、碎片化、有时候反而会降低性能。"

林小源沉默了。他看着远处那些宏伟的山峰,又看看脚下那些细小的方格。大小之间,取舍之间,没有完美的答案。

"大也有大的代价——他喃喃道,觉得自己踩到了一个很实际的问题。"

"不。" mm_struct 纠正他,"大有大的优势,小有小的灵活。关键是知道什么时候用大页,什么时候用 4KB 页面。盲目追求大页——和盲目追求小页面一样愚蠢。"

林小源坐在一块大页石板的边缘,双脚悬空。

他闭上眼睛,试着感受 TLB 的运转。在意识深处,他"看到"了——每一次内存访问,都有一个微小的光点从虚拟地址飞向 TLB 墙壁。如果命中,光点直接穿墙而过,快如闪电。如果未命中,光点撞上墙壁弹回来,然后一级一级地查页表——L2、L1、L0——每次查找都是一次漫长的等待。

"感觉到了?" mm_struct 问。

"感觉到了。"林小源睁开眼,"TLB 命中和 TLB 缺失的差距——太大了。命中是一瞬间,缺失是三次内存访问。"

"对于大内存应用——数据库、虚拟机、JVM——TLB 缺失是主要的性能瓶颈。" mm_struct 说,"大页不是万能药,但它是最直接的优化手段。"

林小源站起来,看着脚下那块 2MB 的石板。石板的表面温暖而坚实,不像 4KB 小方格那样有缝隙和接缝。

"我该怎么做选择?"他问。

"看你需要什么。" mm_struct 说,"如果内存访问模式是顺序的、大块的——用大页。如果内存访问是零散的、小块的——用 4KB 页面。如果你不确定——让 THP 帮你决定,但要做好它选错的准备。"

林小源点了点头。他从石板上跳下来,脚踩在 4KB 的小方格上。方格在他的重量下微微下沉,像踩在沙滩上。

他回头看了一眼那块金色的大页石板。石板在阳光下闪耀着光芒,稳如磐石。

页面大小是性能和灵活性的天平——没有银弹,只有取舍。


c
/*
 * 大页的类型:
 *   2MB 大页 — 二级页表(跳过 L0)
 *   1GB 大页 — 一级页表(跳过 L0 和 L1)
 *
 * 大页的好处:
 *   1. 减少页表项数量
 *   2. 减少 TLB 缺失
 *   3. 减少页 fault 次数
 *   4. 提高内存访问性能
 *
 * 大页的坏处:
 *   1. 内存碎片 — 需要连续的物理内存
 *   2. 内部碎片 — 进程可能不需要整个大页
 *   3. 灵活性降低 — 不能按 4KB 粒度回收
 *
 * 大页的使用方式:
 *   hugetlbfs — 专门的文件系统
 *   MAP_HUGETABLE — mmap 标志
 *   THP — 透明大页(自动)
 */

printf("=== 大页 — 减少 TLB 压力 ===\n\n");

/* 比较不同页面大小 */
printf("页面大小对比:\n");
printf("%-10s %-12s %-15s %-15s\n",
       "页面大小", "页表级数", "覆盖 1GB 需要", "TLB 条目");
printf("%-10s %-12s %-15s %-15s\n",
       "---", "---", "---", "---");
printf("%-10s %-12s %-15s %-15s\n",
       "4KB", "3 级", "262144 个 PTE", "约 1024 个");
printf("%-10s %-12s %-15s %-15s\n",
       "2MB", "2 级", "512 个 PTE", "约 32 个");
printf("%-10s %-12s %-15s %-15s\n",
       "1GB", "1 级", "1 个 PTE", "约 1 个");

printf("\n--- 大页的优势 ---\n");
printf("1. 减少页表项数量\n");
printf("   4KB 页: 1GB 内存需要 262144 个页表项\n");
printf("   2MB 大页: 1GB 内存只需要 512 个页表项\n");
printf("   1GB 大页: 1GB 内存只需要 1 个页表项\n\n");

printf("2. 减少 TLB 缺失\n");
printf("   TLB 条目有限(通常 1024 个)\n");
printf("   4KB 页: TLB 覆盖 4MB\n");
printf("   2MB 大页: TLB 覆盖 2GB\n");
printf("   1GB 大页: TLB 覆盖 1GB\n\n");

printf("3. 减少页 fault 次数\n");
printf("   大页只需要一次页 fault\n");
printf("   4KB 页需要多次页 fault\n\n");

printf("--- 大页的使用场景 ---\n");
printf("数据库: 大量内存访问\n");
printf("虚拟机: 大块内存分配\n");
printf("科学计算: 大数组\n");
printf("JVM: 大堆内存\n");

printf("\n--- 大页的分配方式 ---\n");
printf("1. hugetlbfs: 专门的文件系统\n");
printf("   mount -t hugetlbfs none /dev/hugepages\n");
printf("   mmap(NULL, size, PROT_READ | PROT_WRITE,\n");
printf("        MAP_SHARED | MAP_HUGETABLE, fd, 0)\n\n");
printf("2. THP (透明大页): 自动\n");
printf("   内核自动使用大页\n");
printf("   不需要应用程序修改\n");

道藏笔记

内核启示

大页(Huge Pages)减少 TLB 压力,提高内存访问性能。

大页的类型:

  • 2MB 大页 — 二级页表
  • 1GB 大页 — 一级页表

大页的优势:

  1. 减少页表项数量
  2. 减少 TLB 缺失
  3. 减少页 fault 次数

大页的劣势:

  1. 内存碎片 — 需要连续物理内存
  2. 内部碎片 — 可能浪费内存
  3. 灵活性降低

大页的使用方式:

  • — 专门的文件系统
  • MAP_HUGETABLE — mmap 标志
  • THP — 透明大页(自动)

大页是"TLB 友好"的优化——用更大的页面减少 TLB 缺失。


破关试炼

大页之试

大页减少页表层级和缓存项压力,正文特别指出它能减轻哪种地址翻译缓存的负担?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道