处理器页表项(Page-Table Entry, PTE)的尺寸大小,直接决定了CPU到底能够寻址并管理多大规模的物理内存。回想 32 位计算时代,物理内存的最大寻址上限被死死限制在 4GB——这在当年看似是一片近乎无限的内存海,但在当下,可能连跑一个最基础的 AI 驱动版“Hello World”应用都捉襟见肘。后来主流架构普遍升级到了 64 位,这道物理屏障似乎被彻底打破了;以部分 Arm 系统为例,它们通过使用 56 位物理地址空间,便能一举挂载并访问高达 72PB 的庞大内存。
因此,当得知 Arm 架构正在演进并开始支持尺寸更大的页表项(PTE)时,不少人可能会感到十分意外。由内核开发者 Anshuman Khandual 提交的这套新补丁集,正式在 Linux 内核中引入了对 128 位 PTE 的支持。不过,究竟哪些具体的技术场景或工作负载能从这项新技术中切实获益,目前尚未有明确的答案。
一、128 位页表:改变了什么?付出了什么?
在页表项(PTE)中,最关键的核心数据莫过于物理地址——对于最末级的页表项,它记录的是实际物理内存页的地址;而对于更高级别的页表项,它记录的则是下一级页表的基地址。当然,PTE 内部并非所有比特位都用来存储地址;至少在虚拟地址中对应页内偏移量的低位部分,可以挪作他用。
以配置了 56 位地址空间和 4KB 页面的现代 64 位 Arm 系统为例,其 64 位 PTE 中最多可以使用 44 位来表示物理页帧号,余下的 12 位则被留作内核的管理用途。
将 PTE 的尺寸一举翻倍扩展至 128 位,最直观的好处自然是能在单个表项中塞入更多的信息。但天下没有免费的午餐,这种扩展伴随着极其明显的代价:PTE 尺寸翻倍,意味着整个页表所占用的内存空间也会直接翻倍。
对于某些内存敏感型的工作负载来说,页表本身占用的内存本就居高不下;而页表多吃掉的每一页内存,都意味着系统少了一页可供实际业务使用的物理内存。更严重的影响体现在巨页(Huge Pages)上:由于单个内存页现在只能装下过去一半数量的 PTE,巨页的覆盖范围发生了大幅收缩:
-
在 64 位 PTE 下,PMD 级别的巨页大小为 2MB,升级到 128 位 PTE 后直接缩水至 1MB。
-
PUD 级别的巨页则从 1GB 锐减到了 256MB,整整缩小了 4 倍(因为在此过程中跳过了两级页表)。
-
同样,为了提升 TLB 命中率而凝聚的多尺寸透明巨页(mTHP),其能够合并的尺寸也面临着类似的同比缩减。
对于高度依赖巨页来获取极致性能的应用场景而言,这种覆盖能力的下降可能会带来不可忽视的性能痛点。
二、新格式里的“玄机”:SKL 字段与软件扩展位
既然引入更大的 PTE 会付出巨大的内存开销代价,那么它必然能在其他维度带来弥补性的收益。要理解这些收益,我们需要深入解析一下全新的 128 位 Arm PTE 结构。
在底层的 128 位 PTE 格式中,最令人吃惊的一点是:从第 12 位延伸至第 55 位的地址字段,居然依然只有 44 位长! 加上 12 位的页内偏移,其支持的最大物理地址依然是 56 位——与现有的 64 位 PTE 寻址上限完全一致。
这表明:扩大物理内存的直接寻址极限,并不是本次引入 128 位页表的直接首要目标。
不过,新格式中留下了大量未标记的保留位(即未被使用的空白位)。在最高地址位之上,整整预留了 35 个保留位,这为未来进一步拓展物理地址空间留出了极为充裕的想象空间。
除此之外,新格式还带来了其他关键变化:
-
软件保留位翻倍:硬件完全不解析、专门留给操作系统软件使用的比特位从过去的 5 位增加到了 10 位。这额外的 5 个位虽然看似不起眼,但未来 Linux 内核的高级内存管理机制很可能会利用它们大做文章。
-
引入跳层(Skip Level, SKL)字段:位于第 109 和 110 位的 SKL 字段存在于页表的所有层级中,用于灵活控制页表的遍历过程。以往的页表层级结构十分僵化,通常固定在 3 到 5 层;只有在末端遇到巨页时才会提前终止查找。而全新的 SKL 字段将这种机制通用化了——它可以在页表查找的中间层级就直接指定下一步为巨页。这一特性极有可能为“将巨页直接用于页表本身”打开大门,从而显著加快地址转译速度并降低 TLB 缓存开销。
三、内核适配与发行版难题
为了在 Linux 内核中支持 128 位 PTE,改动代码总量其实并不算大,大部分改动集中在描述字段与位位置的宏定义上。
真正的技术挑战在于页表原子性操作。在遍历或修改页表各级表项时,内核必须保证读取到的数据是完全一致且原子的。以往内核使用 READ_ONCE() 来完成这一操作,但 READ_ONCE() 依赖底层的原子指令,而 Arm CPU 并不具备通用的 128 位单指令原子操作。
为此,补丁集引入了一对新的封装函数 ptval_get() 与 ptval_set()。它们默认回退到标准的 READ_ONCE() 和 WRITE_ONCE(),但在 128 位 Arm 架构下,则会被重写为利用 ldp(Load Pair)和 stp(Store Pair)指令来实现 128 位数据的原子读写。
不过,目前的内核实现要求必须在编译期明确指定是否启用 128 位 PTE 支持;一旦编译启用,该内核镜像将无法在不支持 128 位页表的旧 CPU 上启动。这给各大 Linux 发行版厂商(如 Red Hat、Ubuntu 等)带来了巨大困扰,因为他们总是希望用尽可能少的内核镜像去兼容更多的硬件。未来要解决这一痛点,恐怕需要在系统启动时引入大量的动态代码打补丁(Boot-time Code Patching)技术。
虽然这套补丁集已经历了多轮 RFC 讨论并趋于稳定,但鉴于目前手头拥有支持 128 位页表实际硬件的人少之又少,真正的实测反馈依然非常有限。或许等到此类硬件大规模普及之时,Linux 内核早已做好了充分的准备;但就短期而言,究竟有多少生产系统能从这项新特性中获益,依然是一个未知数。
joib 调侃道:“就在维护者们终于准备把 HIGHMEM(高端内存)支持彻底删掉(因为需要更多内存的东西都迁移到 64 位平台了)的时候,HIGHMEM 居然又要回归了!这次是为了处理 64 位指针和 128 位页表!真是讽刺……”
很多对内核历史不太了解的技术人员可能会产生疑问:当年 32 位系统的 HIGHMEM(高端内存)是因为“指针太短、虚拟地址空间装不下物理内存,导致内存无法直接访问”才引入的。难道到了 64 位指针和 128 位页表的时代,物理内存又会面临“无法直接访问”的困境吗?
答案显然是:从硬件寻址能力上看,绝对不会!
64 位指针拥有高达 $2^{64}$(16 EB)的虚拟寻址空间,而 128 位页表支持的物理地址也高达 56 位(72 PB)甚至更高。当前的物理内存容量距离填满 64 位虚拟地址空间还有着天文数字般的距离,不存在“空间不够用而无法直接映射”的问题。
开发人员之所以这样调侃,是一种对 Linux 内核代码演进的历史幽默(Tech Humor)与讽刺:
-
历史的轮回(复杂的间接抽象概念重现):
-
32 位时代:因为 32 位指针太短(内核只有 1GB 虚拟地址空间),装不下几 GB 的物理内存,内核不得不写了一套极其复杂的动态映射抽象层——HIGHMEM(通过
kmap()/kunmap()动态借用地址来间接访问内存)。 -
64 位时代:虚拟地址空间无限大,内核终于可以把所有物理内存都做直接映射(Direct Map),不再需要间接映射了。因此,维护者们近年来一直在全力清理和删除内核里这套折腾了二十年的 HIGHMEM 历史遗留代码。
-
128 位页表时代:虽然地址空间够用,但由于 128 位的数据太大,普通的单条 CPU 指令/通用的
READ_ONCE()API 无法再做到直接、简单的原子 Dereference(解引用)。内核不得不再次引入专门的封装函数(如补丁中的ptval_get()/ptval_set())和特殊的硬件机制来处理这些表项。
-
-
硬件架构的复杂化(如 CHERI 架构): 评论中其他开发者(如 ejr)也补充道,在类似 CHERI 这种使用 128 位“带能力指针(Capabilities Pointers)”的硬件架构中,指针里不仅装了地址,还夹带了权限和界限信息。在软件访问这些超宽指针或跨节点内存时,依然无法像过去那样简单地“直接访问”,可能又需要借助于软件句柄、转换层或特权映射。
总结来说: 评论者的调侃并不是说内存真的“打不开了”,而是感慨“软件工程里没有新鲜事”——内核维护者们花了十几年苦心孤诣想把 HIGHMEM 这套“为了特殊/间接访问而生的复杂抽象”彻底赶出内核源码树,结果随着 128 位页表和超宽指针的到来,“无法用简单通用 API 直接读取,必须通过特殊抽象层才能访问” 的复杂性,又以全新的姿态卷土重来了。


3889

被折叠的 条评论
为什么被折叠?



