Linux 内核 folio 机制分析:从 struct page 到统一内存抽象
1. 背景
Linux 内核长期以来使用 struct page 作为物理内存的基本描述单元。每个物理页对应一个 struct page 实例,内核通过它管理页面状态、引用计数、LRU 链表等。
但随着 Transparent Huge Page(THP)、hugetlbfs、compound page 等机制的引入,struct page 的局限性越来越明显:
单一页与多页(compound)的处理逻辑大量重复:同一个函数需要判断 PageHead() / PageTail() 分别处理
head/tail 指针混用容易出错:compound_head() 的频繁调用增加了代码复杂度和运行时开销
flag 位冲突:struct page 的 flags 字段在 head page 和 tail page 中有不同的含义,容易误用
API 不统一:某些函数接受 struct page *,某些需要 struct page *head,调用方容易传错
这些问题在 Matthew Wilcox 的 folio 提案 中被系统性地指出。从 Linux 5.15 开始,内核引入 struct folio 作为新的内存管理抽象层,目标是统一单页和多页的处理路径。
信息来源:wiki/concepts/memory-management.md(内核版本 6.12.17),本文源码分析基于 Linux 7.0。
2. Folio 定义
Folio 不是一个独立的内存分配单元,而是对 struct page 的封装和统一。每个 folio 包含一个或多个连续的物理页。
2.1 源码位置
文件路径: include/linux/mm_types.h行号: 401内核版本: Linux 7.0
2.2 struct folio 定义
/* include/linux/mm_types.h:401 */struct folio { union { struct { memdesc_flags_t flags; union { struct list_head lru; struct { void *__filler; unsigned int mlock_count; }; struct dev_pagemap *pgmap; }; struct address_space *mapping; union { pgoff_t index; unsigned long share; }; union { void *private; swp_entry_t swap; }; atomic_t _mapcount; atomic_t _refcount;#ifdef CONFIG_MEMCG unsigned long memcg_data;#endif#ifdef WANT_PAGE_VIRTUAL void *virtual;#endif#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS int _last_cpupid;#endif }; struct page page; /* 与 struct page 共用同一块内存 */ }; /* 后续还有 _flags_1, _head_1 等扩展字段, 用于大 folio(large folio)的额外信息 */};
关键设计:struct folio 通过 union 包含 struct page page,这意味着 folio 和 page 共享同一块内存地址。一个 folio 可以直接当作 page 使用,这保证了向后兼容性。
2.3 内存布局图
上图展示了:
左侧:mem_map 中连续的 struct page 数组,每个 page 占 64 字节
中间:struct folio 覆盖在 struct page 上,通过 union 共享同一片内存
右侧:转换函数 page_folio() 和 folio_page() 实现零成本转换
底部:folio_alloc 直接返回 struct folio *,无需额外包装
注意图中绿色字段是 folio 与 page 完全共享的部分(相同偏移),橙色部分是 folio 扩展的专用字段(如 _nr_pages)。
3. Folio 与 struct page 的关系
3.1 核心概念
| |
|---|
| |
| |
| nr_pages == 1 |
| nr_pages > 1 的 folio,对应 compound page / THP / hugetlb |
每个 page 都属于某个 folio。这是 folio 机制的核心不变量。即使是传统的单页,也在逻辑上被视为一个只包含 1 页的 folio。
3.2 转换 API
/* include/linux/page-flags.h:300 */#define page_folio(p) (_Generic((p), \ const struct page *: (const struct folio *)_compound_head(p), \ struct page *: (struct folio *)_compound_head(p)))
page_folio() 使用 C11 _Generic 实现类型安全的转换。底层调用 _compound_head() 获取 folio 地址。
/* include/linux/mm.h:240 */staticinlineunsignedlongfolio_page_idx(conststruct folio *folio, const struct page *page){ return page - &folio->page;}
folio_page_idx() 返回某个 page 在 folio 中的索引(从 0 开始)。
4. 关键 API 对照
4.1 分配与释放
| | |
|---|
alloc_pages(gfp, order) | folio_alloc(gfp, order) | |
__free_pages(page, order) | folio_put(folio) | |
/* include/linux/gfp.h:323-350 */struct folio *folio_alloc_noprof(gfp_t gfp, unsigned int order);#define folio_alloc(...) alloc_hooks(folio_alloc_noprof(__VA_ARGS__))
folio_alloc() 分配指定 order 的 folio,底层通过 __folio_alloc_node() 实现。
4.2 引用计数
/* include/linux/mm.h:2087 */staticinlinevoidfolio_put(struct folio *folio){ if (folio_put_testzero(folio)) __folio_put(folio);}
folio_put() 将 folio 的引用计数减一。如果计数归零,调用 __folio_put() 释放内存。这与原来的 put_page() 逻辑一致,但 API 更加统一——不需要区分 head page 和 tail page。
4.3 类型判断
/* include/linux/page-flags.h:862 */staticinlineboolfolio_test_large(conststruct folio *folio){ return folio_test_head(folio);}
folio_test_large() 判断 folio 是否包含多个页面(large folio)。返回 true 意味着这是一个 compound page 或 THP。
4.4 属性查询
/* include/linux/mm.h:1722 */staticinlineunsignedintfolio_order(conststruct folio *folio)/* include/linux/mm.h:2813 */static inline size_t folio_size(conststruct folio *folio)/* include/linux/mm.h:2530 */static inline unsigned long folio_pfn(conststruct folio *folio)
| | |
|---|
folio_order() | | |
folio_size() | | |
folio_pfn() | | |
4.5 Folio 生命周期流程图
上图展示了 folio 的完整生命周期:
分配阶段:folio_alloc() 通过 buddy allocator 分配 2^order 个连续页面,返回 struct folio *
初始化阶段:设置 mapping、index、加锁、标记 accessed
I/O 阶段:writeback 流程中的状态转换(locked → writeback → unlocked)
释放阶段:folio_put() 减少引用计数,归零时通过 __folio_put() 释放
红色节点为热路径(hot path),是内核中最频繁调用的 API。
5. 为什么需要 Folio:问题分析
5.1 老代码的问题
在引入 folio 之前,处理 compound page 的典型代码如下:
/* 旧模式:需要手动处理 head/tail */struct page *head = compound_head(page);if (PageHead(page)) { /* 处理 head page */ atomic_sub(compound_nr(page), &page->_refcount);} else { /* 处理 tail page */ /* 需要确保先获取 head,再操作 */}
这段代码有三个问题:
容易忘记调用 compound_head():如果传入 tail page 但没有先获取 head,操作会出错
head/tail 分支重复:同一个函数需要两套处理逻辑
语义不清晰:compound_head(page) 返回的是什么?为什么有时等于 page?
5.2 Folio 模式
/* 新模式:直接操作 folio */struct folio *folio = page_folio(page);unsigned int nr = folio_nr_pages(folio);folio_ref_sub(folio, nr);
改进:
统一入口:无论单页还是多页,都先获取 folio
消除分支:不需要判断 head/tail,folio API 自动处理
语义清晰:page_folio(page) 明确表示"获取该页面所属的 folio"
5.3 性能影响
folio 机制在以下方面带来性能优化:
减少 compound_head() 调用:原来每次操作 compound page 都需要调用,现在 folio 本身已经是 head
减少 flag 位冲突:folio 的 flags 字段含义统一,不需要为 head/tail 设置不同标志
更好的 cache 局部性:folio 结构体设计减少了不必要的字段访问
5.4 Folio 替换 struct page 的演进路线
上图展示了 folio 从提出到逐步替换 struct page 的完整时间线:
| | |
|---|
| | Matthew Wilcox 提出 folio RFC |
| | struct folio 定义引入,API 骨架,兼容层转换函数 |
| | MM 核心迁移:引用计数、锁操作、flags、LRU |
| | 文件系统迁移:filemap、iomap、ext4/xfs、buffer_head |
| | 驱动与网络:netfs、网络设备、DMA-buf/IOMMU |
| | |
绿色节点表示已完成,橙色表示正在进行中,灰色表示规划中。
6. 实践验证
6.1 验证环境
内核版本:Linux 7.0
源码路径:
include/linux/mm_types.h(struct folio 定义)
include/linux/page-flags.h(page_folio、folio_test_large)
include/linux/mm.h(folio_put、folio_order)
include/linux/gfp.h(folio_alloc)
6.2 源码中 folio 的替换进度
在 Linux 7.0 中:
# include/linux/ 目录下grep -r "folio_" include/linux/ --include="*.h" | wc -l # 1033 处grep -rc "struct page" include/linux/ | awk -F: '{s+=$2} END {print s}' # 994 处
folio API 的数量已经略微超过 struct page 的引用,说明核心路径(page cache、buffer、部分文件系统)已基本完成迁移,但仍有部分代码使用 struct page。
6.3 编写代码时如何选择
| |
|---|
| |
| |
| 使用 page API 即可,通过 page_folio() 转换 |
| 优先使用 folio API(VFS 层已大量使用 folio) |
7. 常见问题
7.1 Folio 是取代 struct page 吗?
不是。folio 是对 struct page 的封装和统一,不是替代。struct folio 通过 union 包含 struct page,两者共享内存地址。内核中仍然保留了大量 struct page 相关的代码,迁移是渐进式的。
7.2 为什么 struct folio 和 struct page 要用 union?
为了零成本兼容:
folio 可以直接当作 page 使用(通过 &folio->page)
不需要修改已有的 struct page * 函数签名
内存地址相同,所有基于 page 的硬件 DMA、驱动接口不受影响
7.3 旧代码还能用吗?
可以。内核保留了完整的 struct page API 向后兼容。新旧代码可以共存,通过 page_folio() 和 folio_page() 互相转换。
7.4 Folio 和 THP 的关系?
THP(Transparent Huge Page)是 folio 的使用场景之一。一个 THP 对应一个 large folio(order >= 9,即 2MB+)。folio 机制使得 THP 的处理路径和普通页面完全统一。
8. 总结
本文解决了什么问题
分析了 Linux 内核 folio 机制的引入动机、设计原理和关键 API,帮助开发者理解从 struct page 到 struct folio 的抽象演进。
核心结论
Folio 是 struct page 的封装而非替代,两者通过 union 共享内存地址
核心价值在于统一单页和多页的处理路径,消除 head/tail 分支
在 Linux 7.0 中,folio API 数量(1033 处)已略微超过 struct page(994 处),核心路径已基本完成迁移
实践中应该注意什么
编写新的 mm 代码时优先使用 folio API
现有 struct page 代码可以保持不动,逐步迁移
通过 page_folio() 和 folio_page() 实现新老 API 的桥接
folio 不是银弹,驱动开发中仍然可以只用 struct page
9. 参考资料
Linux Kernel Documentation: Memory Management
Linux kernel source tree (v7.0): include/linux/mm_types.h, page-flags.h, mm.h, gfp.h
Matthew Wilcox: “Folio: A new memory management abstraction for the Linux kernel” (LPC 2021)
lore.kernel.org/linux-mm folio 补丁系列