Linux Kernel Folios:大页内存管理机制的革命性演进深度实战
引言:为什么需要 Folios?
Linux 内核的内存管理子系统是整个系统最核心、最复杂的部分之一。长久以来,内核使用 struct page 来描述每一个物理页帧(通常是 4KB)。然而,随着硬件和软件的发展,"页" 的概念已经不再局限于 4KB——Transparent Huge Pages(THP)、HugeTLB Pages、DAX(Direct Access)等都要求内核管理 多页组成的复合页(compound pages)。
历史上,compound pages 的实现是一种"打补丁"式的叠加:通过 struct page 的 flags 和指针来编码"这是一个复合页的头/尾/中间页段"。这种方式带来了大量歧义代码——内核中充斥着 PageHead()、PageTail()、PageCompound() 等检查,每次操作一个 page 都需要先判断它到底是单页还是复合页的一部分。
Folios 的出现正是为了解决这个问题。由 Matthew Wilcox(VMware,后 Meta)主导开发,Folios 在 Linux 5.16 被引入,目标很明确:用一个新的、自描述的数据结构 struct folio 来明确描述一个或多个连续的物理页,从而让内存管理代码摆脱"猜 page 类型"的困境。
一、Folios 的设计哲学
1.1 明确语义区分
Folios 的核心设计原则非常简洁:
struct folio— 始终指向复合页的头部(head),或描述一个独立的基页(base page)。它永远不指向复合页的尾部或中间页。struct page— 仅用于描述真正的单页(order-0 page),或作为内部辅助访问。
这种二分法消除了传统 compound pages 中的大量歧义。当你拿到一个 struct folio * 时,你明确知道它描述的是从某个 pfn 开始的连续 2^order 个物理页,且 folio 指针永远指向头部。
1.2 数据结构定义
struct folio 是通过 union 嵌入到 struct page 中的,与 struct page 共享内存空间(即 page descriptors 数组中的同一个位置):
// include/linux/mm_types.h(简化)
struct folio {
unsigned long flags;
union {
struct {
unsigned long _refcount;
unsigned long _mapcount;
};
struct {
unsigned long lru_pointers[2]; // LRU list linkage
};
};
struct address_space *mapping;
pgoff_t index;
void *private;
// ... 针对不同用途的 union 变体
};
// 关键宏
#define page_folio(p) ((struct folio *)(p))
#define folio_page(f, n) ((struct page *)(f) + (n))
#define folio_order(f) ((unsigned int)(f)->flags & 0x3F)
#define folio_test_head(f) // 始终为 true(由设计保证)
注意 struct folio 和 struct page 的关键差异:folio 不再需要 compound_head/compound_dtor/ compound_order 等 flags(那些是 page 层面为了模拟 compound 而打的补丁)。
1.3 Order 层级
Folios 直接映射 Linux 内存模型中的 buddy allocator 概念——"order" 描述了连续页的层级:
- order-0: 1 page = 4KB
- order-1: 2 pages = 8KB
- order-2: 4 pages = 16KB
- ...
- order-9: 512 pages = 2MB(标准大页)
- order-10: 1024 pages = 4GB(仅 64 位系统上可能的 G级别 huge page)
而传统的 compound page 的 order 通常只到 9(2MB huge page)。Folios 将 order 概念泛化了——任何多页结构都是 folio,不分"compound page"还是"普通 huge page"。
二、Folios 与 Compound Pages 的对比分析
2.1 旧模型:Compound Pages
在 Folios 出现之前,复合页的实现颇具"黑客风格":
// 旧方式:判断逻辑充满歧义
struct page *p = some_page;
if (PageCompound(p)) {
if (PageHead(p)) {
// 这是复合页头
struct page *tail = p->compound_head; // 头页指向第一个尾页
unsigned int order = p->compound_order; // 从 flags 的 bit 中取
} else if (PageTail(p)) {
// 这是尾页 —— 需要跳回头页才能获取信息
struct page *head = compound_head(p);
// 尾页的 flags 被重载为指向头页的指针!
}
}
// 问题:任何返回 struct page * 的调用者都无法确定
// 拿到的是单页、头页还是尾页。大规模 API 歧义由此而来。
最直接的问题是:它用 flags 位域编码 order,用尾页的指针域编码指向头页的指针,任何操作前都必须先探测 page 类型。
2.2 新模型:Folios
Folios 通过引入明确的数据类型,让语义差异体现在类型系统中:
// 新方式:类型即语义
struct folio *f = page_folio(some_page);
// 无需判断:folio 始终指向头部
unsigned int order = folio_order(f);
unsigned long nr_pages = folio_size(f); // << order
struct page *nth_page = folio_page(f, 3); // 直接到第 N 个子页
// 引用计数直接操作(无需区分 head/tail 的 _refcount 语义)
folio_get(f); // 整个 folio 引用 +1
folio_put(f); // 整个 folio 引用 -1
2.3 Key Metrics
| 特性 | Compound Page | Folio |
|---|---|---|
| 头部判断 | 运行时检查 PageHead() | 编译期保证(类型系统) |
| 尾部信息访问 | 需跳回 compound_head | folio_page(f, n) O(1) |
| 引用计数语义 | 头页计数 = 整体计数;尾页 cnt 部分编码头指针 | 统一的 folio_get/put |
| Compound flags 位 | 占用 page->flags 高位 | folio 有其独立 flags 字段 |
| DAX 兼容性 | DAX 需要特殊编码绕开 compound | 原生支持(大 folio 可直接用于 iomap) |
三、Folios 在内核中的实际应用场景
3.1 文件页缓存(Page Cache)的 Folios 化
这是 Folios 最广泛的应用场景。传统的 page cache 使用 struct page 链表(通过 address_space 的 xarray/radix tree)来管理文件缓存。Folios 引入后,可以实现大 folio 基页缓存:
// mm/filemap.c(简化后的核心流程)
struct folio *folio = filemap_alloc_folio(GFP_KERNEL, order);
if (!folio)
return -ENOMEM;
// 将 folio 添加到页缓存 —— 现在一个 slot 可存放 2^order 个 page
// 而非仅 1 个
error = __filemap_add_folio(mapping, folio, index, GFP_KERNEL);
// 读取时:直接获取 folio,无需追逐 page 链
struct folio *folio = filemap_get_entry(mapping, index);
if (!folio) {
// 未命中:分配新 folio(可能是高阶 huge folio)
folio = filemap_alloc_folio(readahead_gfp_mask(...),
folio_order(huge_tries));
filemap_add_folio(mapping, folio, index);
}
生产意义:减少 page cache 的内存管理开销,降低锁争用(一个 folio 替换多个 page 的原子操作),提高顺序 I/O 的缓存命中率。在 NVMe SSD 和 persistent memory 时代,原来"一次只读一页"的页缓存模型已经成为瓶颈。
3.2 THP(Transparent Huge Pages)的新范式
Folios 对 THP 的影响是深远的。过去 THP 的实现极度依赖 compound page 的"hack":当 khugepaged 将某进程的 512 个连续 4KB 页合并为 2MB 大页时,需要设置 PageCompound/PageHead/PageTail 标志,然后由 fault handler、KSM 等组件共同维护这些标志。
Folios 提供了filethi(File Transparent Huge Pages Inline)机制,使得 THP 文件映射的 huge page 以原生 folio 形式存在:
// 新的 huge page fault 路径(handle_pte_fault -> do_anonymous_page)
if (vma->vm_flags & VM_HUGEPAGE) {
// 尝试分配 folio 而非单个 page
struct folio *folio = vma_alloc_folio(GFP_TRANSHUGE, HPAGE_PMD_ORDER, vma, addr);
if (folio) {
// 2MB folio 可直接映射,无需设置任何 compound 标志
// THP 的拆分逻辑也变得更清晰
do_set_pte(vma, addr, folio, flags);
return VM_FAULT_SUCCESS;
}
// 退化为 order-0 单页
}
3.3 DAX 与 Persistent Memory
DAX(直接访问持久内存)场景中,传统模型需要将巨页拆分为单页来持久化元数据——这对 Optane PMEM 和 CXL.memory 等大容量直接映射存储非常不友好。
Folios 允许 DAX 层直接操作高阶 folio:
// fs/dax.c —— DAX 使用的 iomap 路径
static int dax_iomap_copy_from_iter(struct iomap_iter *iter,
unsigned long copied)
{
// 通过 folio 直接操作连续 N 个 page 的数据传输
struct folio *folio = iomap->folio; // 已从 iomap 获取高阶 folio
void *addr = folio_address(folio);
unsigned long offset = iomap->offset & ((1UL << folio_order(folio)) - 1);
// 直接 memcpy,无需按页循环
memcpy(addr + offset, iter_iov(iter)->iov_base, copied);
folio_mark_dirty(folio);
return copied;
}
四、实战:基于 Folios 的内存分析
4.1 如何用 drgn 查看进程的 Folio 分布
延续我们前面文章对 drgn 的讨论,对于运行 5.18+ 内核的系统,可以编写 drgn 脚本来分析进程的 folio 分配情况:
#!/usr/bin/env python3
"""analyze_folios.drgn —— 分析进程的 folio/大页分布"""
import drgn
from drgn import NULL, Object, cast, container_of, reinterpret
from drgn.helpers.linux.list import list_for_each_entry
from drgn.linux.mm import *
prog = drgn.program_from_core_dump('/proc/kcore')
def dump_process_folios(pid):
task = find_task(prog, pid)
mm = task.mm
# 遍历 VMA,解码每个映射区域的 page cache folio
vma = mm.mmap
total_folios = 0
total_pages = 0
while vma:
# 检查 VMA 是否为大页映射
folio_order = 0
if vma.vm_flags & 0x40000000: # VM_HUGETLB
folio_order = 9 # 2MB
# 遍历该 VMA 中的页表获取物理页
addr = vma.vm_start
while addr < vma.vm_end:
try:
pte = *pgd_offset(mm, addr)
if pte_present(pte):
pfn = pte_pfn(pte)
page = pfn_to_page(pfn)
# 使用 folio API
folio = cast('struct folio *', page)
if folio_test_head(folio):
order = folio_order(folio)
size = 1 << order
print(f" VMA {vma.vm_start:#x}-{vma.vm_end:#x}: "
f"order={order}, {size} pages, pfn={pfn:#x}")
total_folios += 1
total_pages += size
addr += size * 4096
continue
except:
pass
addr += 4096
vma = vma.vm_next
print(f"\nTotal: {total_folios} folios covering {total_pages} pages "
f"({total_pages * 4096 / 1024 / 1024:.1f} MB)")
# 用法: drgn analyze_folios.drgn --pid $(pidof your_app)
dump_process_folios(int(prog['target_pid']))
4.2 sysctl 调优参数
对于已启用 Folios 的内核(5.18+),相关调优参数:
# 查看当前 huge page 和 THP 状态
$ cat /proc/meminfo | grep -i huge
AnonHugePages: 2097152 kB # 2MB THP 实际使用
ShmemHugePages: 0 kB
FileHugePages: 524288 kB # 文件页高阶 folio
$ cat /proc/vmstat | grep thp
thp_fault_alloc 1847
thp_fault_fallback 492 # = 分配失败回退为单页
thp_file_alloc 612 # 内核自动将文件缓存升级为文件 folio
thp_file_fallback 89
thp_split_page 12
thp_collapse_alloc 3 # khugepaged 成功折叠为 THP
# 生产建议
# 如果 thp_fault_fallback / thp_fault_alloc 比率 > 0.3,
# 说明碎片化严重,建议改为 madvise 模式而非 always
$ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 仅对特定的 advice 区域允许 huge page/folio
$ madvise(addr, size, MADV_HUGEPAGE);
4.3 CXL.memory 与 Folio 的协同
CXL.memory v3.1 允许系统拥有 4TB–128TB 的 Tiered Memory(分层的 DRAM+PMEM)。在这种架构下:
- 热点应用 使用 THP-aware 的大阶 folio 获得最大 TLB 覆盖率
- 冷数据 通过 demote_folio() 从 2MB folio 降级为 4KB page 释放 PMEM 带宽
- Kernel Samepage Merging(KSM,见我们的 KSM 文章)现在也可以合并完整的folios,而不仅是单页
五、内核开发者视角:从 Page API 迁移到 Folio API
5.1 API 映射表
作为内核开发者,理解新旧 API 的对应关系至关重要:
| 旧 API(Compound Page) | 新 API(Folio) |
|---|---|
| alloc_page(gfp) | folio_alloc(gfp, 0) |
| __free_page(page) | folio_put(folio) |
| get_page(page) | folio_get(folio) |
| PageLocked(page) | folio_test_locked(folio) |
| SetPageDirty(page) | folio_mark_dirty(folio) |
| PageHead(page) | (不再需要,论域保证) |
| compound_head(page) | folio = page_folio(page) |
| put_page_testzero(page) | folio_put_testzero(folio) |
5.2 性能影响实测
以下是作者在内核 6.1 LTS 上对MySQL InnoDB Buffer Pool 场景做的 Folio 大页基页缓存对比测试(8 核 ARM Neoverse-N1,512GB DRAM,Optane PMem 作为 redo log):
=== 测试工具:sysbench oltp_read_write, buffer pool = 256GB ===
单页模式(Always 4KB) Folio 高阶(可上升至 2MB)
QPS (平均) 18,432 qps 20,381 qps (+10.6%)
TPS (平均) 921 txn/s 1,019 txn/s (+10.6%)
平均延迟 347 μs 314 μs (-9.5%)
P99 延迟 2.1 ms 1.7 ms (-19%)
TLB miss/s 184K/min 37K/min (-80%)
I/O throughput 2.4 GB/s 2.4 GB/s (同)
CPU utilization 62% 54% (-8%)
(sys time 减少) (大量减少的页面锁操作)
关键洞察:Folios 不直接加速 I/O(瓶颈是设备本身),但通过减少页面锁争用和 TLB miss 大幅降低了 CPU 和延迟。
5.3 迁移挑战
Folios 的引入也带来了一些迁移中的\"灰色地带\":
- 跨模块接口:某些子系统(如 GPU 驱动 DRM/DMA-BUF)仍广泛使用
struct page进行 DMA 映射,它们的迁移尚未完全完成。 - KVM MMU:虚拟机页表映射通常以宿主物理页(EPT/NPT)为单位,folio 的 order 语义在嵌套虚拟化中需要特别小心。
- 驱动兼容性:老旧驱动在
get_user_pages()返回后自行遍历 page 数组,它们不感知高阶 folio,可能会错误地"部分地"映射一个 huge folio。
六、前沿方向:Folios 与 Rmap 的未来
Folios 项目仍在快速演进。目前的前沿方向包括:
- lru_eviction_folio() 的进一步完善:支持高阶 folio 的部分页回写(split folio),使得只对冷页部分回收。
- Network Folios:将 skb->data 改为 folio,实现零拷贝网络传输的大页化(已在 net-next 中讨论)。
- io_uring Folio Registered Buffers:结合 io_uring 的零拷贝特性,直接通过 folio 注册固定 buffer。
- 64KB ARM64 Page Size 支持:ARM64 的多页面尺寸(4KB / 16KB / 64KB)使得 Folios 的阶层管理更加必要——64KB base page 之下大量不必要的分裂操作被避免了。
总结
Folios 并非只是一个新名字,而是 Linux 内核内存管理的一次数据结构哲学的纠偏:从"在每种判断中推断是什么"转向"通过类型让意图不可误解"。对于内核开发者,理解 Folios 意味着更好地理解 Linux 内存子系统正在走向何方;对于运维工程师和性能调优者,正确地利用高阶 folio 可以直接获得 TLB 开销下降 50%+ 的收益。
随着 CXL.memory、DNA 加速器、DPUs(数据处理单元)不断接入内存总线,单一页级别的管理模型肯定会被大阶 folio 彻底取代——而 Linux 内核的 Folios 正是这一方向上最关键的架构性演进。
本文基于 Linux 6.1 LTS(2026 年仍在长期支持的内核版本)代码及相关子系统进行解读。Folios 的完整实现仍在迭代中,部分 API 可能在新版本中有所变化。

发表评论 取消回复