处理器页表项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 架构下则会被重写为利用ldpLoad Pair和stpStore 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 直接读取必须通过特殊抽象层才能访问” 的复杂性又以全新的姿态卷土重来了。
阅读完成 · 觉得有帮助?