🐧 第五章 内存管理
📄 一句话概括

大页不省内存,省 TLB。1 GB 内存用 4 KB 页要 26 万项页表,用 2 MB 页只要 512 项;512 项的 TLB 装小页只盖得住 2 MB,装大页能盖住 1 GB。

📝 本节和前面几节的关系

5.16 靠 TLB 兜住了多级页表的开销,这一节反过来:如果 TLB 装不下,就【把页做大】。

一、页数差多少

1 GB 内存:4 KB 页要 262144 页,2 MB 页只要 512 页。 页表项数也就跟着差 512 倍。 页表小了,建表快、换出快、缺页次数也少——但这不是主要收益。 主要收益在 TLB 上。

二、TLB 只有几百项,装不下小页

一个典型 TLB 有 512 项。 用 4 KB 页:全装满了也只能盖住 512 × 4 KB = 2 MB。访问 1 GB 的随机位置,TLB 几乎次次不命中。 用 2 MB 页:512 项能盖住 1 GB——整个 1 GB 一次装下。 实测:随机访问 1 GB,小页命中率 0.3%,大页 97.4%。 这个差距比【页表小了 512 倍】重要得多——因为 TLB 不命中意味着要查四级页表,那是四次额外访存。

三、代价

一、内部碎片:一页 2 MB,你只用 100 KB 就浪费 1.9 MB。 二、要有连续的物理内存:2 MB 连续在跑久了之后不容易凑出来——这正是 5.3 讲的外部碎片。 三、申请会失败:内存碎片多的时候,大页申请直接返回失败,所以程序得准备降级方案。 所以大页不是默认可用的:要么显式申请(mmap 加标志、或 hugetlbfs),要么靠【透明大页】自动合并。

四、透明大页与显式大页

透明大页(THP):内核自己判断,觉得合适就悄悄把 4 KB 页合并成 2 MB 页。不用改程序,默认通常开着。 显式大页(hugetlbfs):程序自己申请,预先分配好,一定用得上。数据库、虚拟化常用。 THP 的争议:它合并的时候要花时间,且【可能合并不该合并的】,所以有些服务会显式关掉它。 一句话:大页适合【大块、顺序、长期驻留】的内存,不适合【小而零碎】的分配。

五、动手 Demo:标准大页——把 4KB 的页换成 2MB 的页

这一节算三笔账:页数、TLB 覆盖范围,再用一个模拟 TLB 真跑随机访问,看命中率差多少。

total, small, huge = 1024 ** 3, 4096, 2 * 1024 ** 2
ns, nh = total // small, total // huge
print("4KB 页", ns, "个")
print("2MB 页", nh, "个")

tlb = 512
print("TLB 装 4KB 页能盖", tlb * small // 1024 // 1024, "MB")
print("TLB 装 2MB 页能盖", tlb * huge // 1024 ** 3, "GB")

实跑输出:

--------------------------------------------------------------------
标准大页——把 4KB 的页换成 2MB 的页
--------------------------------------------------------------------
① 1 GB 内存,页数差多少

  页大小     要多少页      页表项
  -----------------------------------
  4 KB       262144 页     262144 项
  2 MB       512 页        512 项

  **少 512 倍。**

② TLB 只有 512 项,能覆盖多少内存

  页大小     TLB 全命中时覆盖     1 GB 要缺多少次 TLB
  ----------------------------------------------------
  4 KB       2 MB                 512 次
  2 MB       1.00 GB              0 次

  4 KB 页时,**TLB 全部装满也只盖得住 2 MB**,连 1 GB 的零头都不够。
  2 MB 页时,**512 项就能盖住 1.00 GB**——整个 1 GB 一次装下。

③ 真跑一次:随机访问 1 GB,看 TLB 命中率

  页大小      随机访问次数     TLB 命中     命中率
  -------------------------------------------------
  4 KB 页     20000            51           0.3%
  2 MB 页     20000            19488        97.4%

  **同样是随机访问,小页几乎次次不命中,大页次次命中。**
  **这就是大页的全部理由:不是省内存,是省 TLB。**

④ 代价

  好处                     代价
  --------------------------------------------------
  TLB 覆盖大,命中率高     一页 2 MB,用不完就浪费
  页表项少,建表快         要有连续 2 MB 的物理内存
  缺页次数少               内存碎片多时申请会失败

  **所以大页不是默认可用的**:要显式申请(`mmap` 加标志、或 hugetlbfs),
  **系统只把【透明大页】这种自动版默认打开**,专门给大块顺序访问的程序用。

页数差 512 倍,TLB 覆盖率也差 512 倍。 0.3% 对 97.4%——这就是为什么大页在某些负载下能带来百分之几十的性能提升。 注意这个模拟只算了 TLB 容量,没算查表开销;真实系统里 TLB 不命中要查四级页表,代价更大。 所以【大页的收益主要落在【随机访问大块内存】的场景】——虚拟机内存、数据库缓冲池、大数组计算。 对【小而零碎的分配】用大页,只会浪费内存。

📋 Linux 上看大页配了多少:grep -i huge /proc/meminfo——HugePages_Total 与 Hugepagesize 两行就够了。

30 秒实验

  1. grep -i huge /proc/meminfo,看透明大页与显式大页各配了多少。
  2. cat /sys/kernel/mm/transparent_hugepage/enabled,看 THP 是 always、madvise 还是 never。
  3. grep -o 'pdpe1gb' /proc/cpuinfo | head -1,看 CPU 支不支持 1 GB 大页。

常见误区

  1. 【大页是为了省内存】。它反而浪费内存(内部碎片);它省的是 TLB 项。
  2. 【页越大越好】。大页要连续的物理内存,碎的时候申请不到;小分配用大页纯属浪费。
  3. 【透明大页应该永远开着】。它合并要花时间,有些延迟敏感的服务会显式关掉。
自测三问(点开对答案) 1. 大页最主要的好处是什么? —— **省 TLB 项**:同样的 TLB 容量能覆盖大得多的内存范围,**命中率从个位数升到九十几。** 2. 大页的三个代价是什么? —— **内部碎片、要连续物理内存、碎片多时申请失败**。 3. 透明大页和显式大页差在哪? —— **THP 是内核自动合并,不用改程序**;**显式大页要程序自己申请、预先分配,确定用得上。**

相关笔记