内存管理-77-kernel-internals系列-Memory专题-Advanced-6-Folio Abstraction
一、The Folio Abstraction 小节翻译
注: 翻译自 https://kernel-internals.org/mm/folio/
Folio 抽象
struct folio 用一等内核对象取代 compound-page 补丁,让多页内存单元安全、显式、高效
1. 关键源文件
------------------------------------------------------------------------------------------------------------------------------------- 文件 用途 ------------------------------------------------------------------------------------------------------------------------------------- include/linux/mm_types.h struct folio 定义与布局断言 include/linux/page-flags.h page_folio()、folio_page()、flag 访问函数 include/linux/mm.h folio_order()、folio_nr_pages()、folio_size()、folio_pfn()、folio_address()、folio_get()、folio_put() include/linux/page_ref.h folio_ref_count()、folio_try_get() include/linux/pagemap.h filemap_alloc_folio()、filemap_grab_folio()、readahead_folio()、filemap_add_folio() include/linux/swap.h folio_add_lru() mm/folio-compat.c 委托给 folio 函数的 Page-API 垫片 -------------------------------------------------------------------------------------------------------------------------------------
2. 为什么需要 folio
2.1 compound page 问题
struct page 最初为 order-0(单个 4KB)页设计。大页是后来用 compound page 机制硬接上去的:一组连续页一起分配,第一页是 head,之后每页都是 tail。Tail 在 page->compound_head 里编码回指 head 的指针,并以 LSB 置位做标记。
于是收到任意 struct page * 的代码必须不停地问:这是 head 还是 tail?如果是 tail,先解回指再干活。搞错就静默 corrupt 掉 head 的引用计数、标志或映射指针。
结果就是到处都是这个模式:
/* 老风格:每个调用者都要跳这个舞 */ page = compound_head(page);
这容易错、类型系统看不见、散落在几千个调用点。
2.2 folio 是什么
struct folio 是一等内核对象,代表一组物理、虚拟、逻辑都连续、2 的幂对齐的字节。它至少 PAGE_SIZE 大小。关键保证是:指向 struct folio 的指针永远指向 head。没有 tail folio。
order-0 folio 就是单个 4KB 页。
order-N folio 是 2^N 个物理连续页(2^N × PAGE_SIZE 字节)。
大小由对象自身经 folio_order() 编码,不靠上下文推断。
struct folio 由 Matthew Wilcox 自 Linux 5.16(2022)起引入,之后每个版本都在持续转换中。
3. struct folio
定义在 include/linux/mm_types.h:
struct folio { /* private:不展开 anon union 细节 */ 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; /* ... CONFIG_MEMCG、WANT_PAGE_VIRTUAL 等 ... */ }; struct page page; /* 在偏移 0 处的 union */ }; /* 第二、三页槽:大 folio 元数据 */ union { struct { ...; }; struct page __page_1; }; union { struct { ...; }; struct page __page_2; }; union { struct { ...; }; struct page __page_3; }; };
关键设计点:struct page page 是匿名 union 的第一个成员,在偏移 0。struct folio * 强转成 struct page * 永远是 head 页。内核在结构体定义后紧跟编译期 static_assert 强制保证:
/* 断言在 page 结构中的成员 pg 是否和在 folio 中的 fl 成员的偏移一样 */ #define FOLIO_MATCH(pg, fl) static_assert(offsetof(struct page, pg) == offsetof(struct folio, fl)) FOLIO_MATCH(flags, flags); FOLIO_MATCH(mapping, mapping); FOLIO_MATCH(_refcount, _refcount); /* ... */
布局一旦漂移,构建就挂。
3.1 大小与 order 访问函数
都在 include/linux/mm.h:
/* 分配 order:单页为 0,2^N 页为 N */ static inline unsigned int folio_order(const struct folio *folio); /* 该 folio 跨了几个 struct page:1 << folio_order() */ static inline unsigned long folio_nr_pages(const struct folio *folio); /* 总字节数:PAGE_SIZE << folio_order() */ static inline size_t folio_size(const struct folio *folio); /* 字节大小的以 2 为底对数:PAGE_SHIFT + folio_order() */ static inline unsigned int folio_shift(const struct folio *folio);
order-0(常见情况)folio_order() 直接返回 0,不解引用额外字段 —— folio_test_large() 检查立即短路。
4. page ↔ folio 转换
4.1 page_folio() —— 页到所属 folio
定义在 include/linux/page-flags.h:
#define page_folio(p) (_Generic((p), \ const struct page *: (const struct folio *)_compound_head(p), \ struct page *: (struct folio *)_compound_head(p)))
这是 _Generic 宏,保持 const 正确性。_compound_head() 在 LSB 置位(tail 页)时跟随 compound_head 指针,已是 head 则直接返回。结果恒为强转成 struct folio * 的 head 页。
无引用的竞态:
内核文档提醒:调用者若事先没持有引用,page_folio() 可能与 folio 切分竞态。拿到引用后务必再确认 folio 仍包含该页。
4.2 folio_page() —— folio 内第 n 页
#define folio_page(folio, n) (&(folio)->page + (n))
简单指针算术。folio_page(folio, 0) 是 head 页;folio_page(folio, folio_nr_pages(folio) - 1) 是最后一页。不做越界检查 —— 假定调用者持有引用。
4.3 folio_file_page() —— 文件偏移对应的页
当大 folio 跨多个 page-cache 索引,folio_file_page() 把文件偏移映射到正确的子页:
/* include/linux/pagemap.h */ static inline struct page *folio_file_page(struct folio *folio, pgoff_t index);
4.4 folio_pfn() 与 folio_address()
/* folio 第一页的页帧号 */ static inline unsigned long folio_pfn(const struct folio *folio); /* folio 的内核虚地址(直接映射) */ static inline void *folio_address(const struct folio *folio); //对称的 pfn_folio(pfn) 把 PFN 转回 folio: static inline struct folio *pfn_folio(unsigned long pfn) { return page_folio(pfn_to_page(pfn)); }
5. 引用计数
5.1 模型
引用计数存在 folio(head 页)的 _refcount 里。Tail 页没有独立引用计数。所有引用操作都走 folio。
5.2 folio_get() 与 folio_put()
/* include/linux/mm.h */ /* 引用计数加一 —— 要求已持有引用 */ static inline void folio_get(struct folio *folio); /* 引用计数减一 —— 到零释放 folio */ static inline void folio_put(struct folio *folio);
get_page() 和 put_page() 还在,但只是包装:
static inline void get_page(struct page *page)
{
struct folio *folio = page_folio(page);
/* ... */
folio_get(folio);
}
static inline void put_page(struct page *page)
{
struct folio *folio = page_folio(page);
folio_put(folio);
}
5.3 folio_ref_count()
/* include/linux/page_ref.h */ static inline int folio_ref_count(const struct folio *folio);
返回原始引用计数。内核文档警告不要直接碰 _refcount;永远用该访问函数。
5.4 folio_try_get()
手里没引用、想投机拿一个时用:
/* include/linux/page_ref.h */ static inline bool folio_try_get(struct folio *folio);
拿成功返回 true,folio 已到零(已释放或为切分/迁移冻结)返回 false。相当于老 get_page_unless_zero() 的 folio 版。
folio_get() vs folio_try_get():
已持有引用(例如 page cache 里带锁拿到的 folio)才用 folio_get()。经数据结构找到 folio 指针、加锁前要先 pin 住,用 folio_try_get()。
6. 今天 folio 用在哪里
6.1 Page cache
struct address_space 经 XArray(i_pages)存缓存页。XArray 条目是 struct folio * 指针。所有现代 page-cache 查找路径都返回 folio:
/* 查找 —— 返回上锁 folio,不存在则建 */ static inline struct folio *filemap_grab_folio(struct address_space *mapping, pgoff_t index); /* 带标志的底层查找/创建 */ struct folio *__filemap_get_folio(struct address_space *mapping, pgoff_t index, fgf_t fgf_flags, gfp_t gfp); /* 把 folio 插进 page cache 和 LRU */ int filemap_add_folio(struct address_space *mapping, struct folio *folio, pgoff_t index, gfp_t gfp);
6.2 Readahead
->readahead 地址空间操作收到 struct readahead_control *。已转换的驱动和文件系统循环调 readahead_folio():
/* include/linux/pagemap.h */ static inline struct folio *readahead_folio(struct readahead_control *ractl);
每次调用返回下一个待填充 folio,并把控制结构推进 folio_nr_pages() 页。
6.3 Writeback
回写路径用 folio 原生操作。mm/folio-compat.c 放着老 page API 到 folio 等价物的垫片:
/* 这些 page-API 函数是兼容垫片(folio-compat.c) */ void end_page_writeback(struct page *page) /* → folio_end_writeback() */ bool set_page_dirty(struct page *page) /* → folio_mark_dirty() */ void mark_page_accessed(struct page *page) /* → folio_mark_accessed() */
6.4 LRU 链表
/* include/linux/swap.h */ void folio_add_lru(struct folio *folio);
LRU 按 folio 记账。大 folio 在 LRU 上按 folio_nr_pages() 页计数,但是单个驱逐单位 —— 内核不能部分驱逐一个 folio。
6.5 文件系统采纳
树内多数文件系统已转换 ->readahead、->read_folio、->writepages 方法。地址空间操作表(struct address_space_operations)现在是 folio 原生槽:dirty_folio、migrate_folio、error_remove_folio。没转的走兼容包装;转换仍在进行。
7. Large folios
N ≥ 1 的 order-N folio 是大 folio。folio_test_large() 对它们返回 true:
/* include/linux/page-flags.h */ static inline bool folio_test_large(const struct folio *folio);
7.1 Allocation
page-cache 用经 filemap_alloc_folio() 分配:
/* include/linux/pagemap.h */
#define filemap_alloc_folio(gfp, order) \
alloc_hooks(filemap_alloc_folio_noprof(gfp, order))
内核通用分配用 folio_alloc()(在 include/linux/gfp.h),包装伙伴系统:
#define folio_alloc(gfp, order) \
alloc_hooks(folio_alloc_noprof(gfp, order))
7.2 文件 backed 大 folio 与 TLB
page-cache folio 横跨多个 PMD 大小对齐页时,映射它需要的页表项更少。顺序读或 mmap 大文件的大 folio 负载可减少:
(1) 缺页次数:一次缺页装整个 folio。
(2) TLB 压力:对齐允许时连续物理页可由单个 PMD 条目覆盖。
(3) 回写开销:一次 writepages 可把整个 folio 当单次 I/O 提交。
page-cache folio 最大 order 由 MAX_PAGECACHE_ORDER 管,即 min(MAX_XAS_ORDER, PREFERRED_MAX_PAGECACHE_ORDER)。多数 64 位配置 PREFERRED_MAX_PAGECACHE_ORDER 是 HPAGE_PMD_ORDER(order 9,2MB),与 PMD 大小对齐。文件系统经下式声明支持的 folio order 范围:
/* include/linux/pagemap.h */ static inline void mapping_set_folio_order_range(struct address_space *mapping, unsigned int min, unsigned int max);
7.3 与 mTHP 的关系
page cache 大 folio 是匿名内存多尺寸 THP(mTHP)的文件 backed 对应物。匿名侧见 mTHP。关键区别:文件 backed 大 folio 由 page cache 和 struct address_space 管理;匿名大 folio 由 THP 机制和 struct vm_area_struct 管理。
8. 进行中的转换
8.1 模式:folio-compat.c
mm/folio-compat.c 是转换如何推进的范本。文件里每个函数结构都一样:
/* 老 page API —— 为未转换调用者保留 */ void unlock_page(struct page *page) { return folio_unlock(page_folio(page)); } EXPORT_SYMBOL(unlock_page);
page 函数转成 folio 再委托。调用者都改成直接调 folio_unlock() 后,page 垫片没有调用者就可以删了。
8.2 命名约定
内核已采用一致命名:
------------------------------------------------------------------------------------------------------------------------------------- API 风格 命名模式 例子 ------------------------------------------------------------------------------------------------------------------------------------- Folio 原生(新) folio_* folio_lock()、folio_mark_dirty() Page(遗留) 原名 lock_page()、set_page_dirty() 转换 helper page_folio() page_folio(page) -------------------------------------------------------------------------------------------------------------------------------------
函数名以 folio_ 开头即收 struct folio *、操作整个 folio。若收 struct page *,去 mm/folio-compat.c 查 —— 很可能就是垫片。
8.3 如何判断函数转了没
(1) 看参数类型:struct folio * 即已转。
(2) 去 mm/folio-compat.c 找:老名字在里面即实现已搬到 folio_* 对应物。
(3) 搜地址空间操作表(include/linux/fs.h 的 struct address_space_operations):叫 *_folio 的槽已转;还叫 *_page 的没转。
9. 写 folio 感知代码
9.1 决策规则
------------------------------------------------------------------------------------------------------------------------------------- 你在弄… 用… ------------------------------------------------------------------------------------------------------------------------------------- Page cache(文件数据) 全程 Folio API LRU 跟踪 folio_add_lru() 回写状态 folio_mark_dirty()、folio_start_writeback()、folio_end_writeback() 裸页分配(alloc_pages) 返回 struct page *;立即 page_folio() 传 struct page * 的老代码 在边界转换;碰标志前先持有 folio 引用 -------------------------------------------------------------------------------------------------------------------------------------
9.2 典型 folio 感知读路径(示意)
struct folio *folio; /* 1. 在 page cache 查或建 folio */ folio = filemap_grab_folio(mapping, index); if (IS_ERR(folio)) return PTR_ERR(folio); /* folio 已上锁并持有引用 */ /* 2. 看是否已 uptodate */ if (folio_test_uptodate(folio)) { folio_unlock(folio); return 0; } /* 3. 给整个 folio 提交 I/O */ /* ... 文件系统相关读 ... */ /* 4. 标 uptodate 并放掉 */ folio_mark_uptodate(folio); folio_unlock(folio); folio_put(folio);
9.3 直接分配 folio
page cache 之外要 folio(例如私有内核 buffer):
/* 分配单页(order-0)folio */
struct folio *folio = folio_alloc(GFP_KERNEL, 0);
if (!folio)
return -ENOMEM;
/* 用它 ... */
folio_put(folio); /* 丢分配器的引用;到零释放 */
9.4 没有前置引用别调 folio_get()
/* 错:没拿锁从数据结构拿到 folio 指针 */
folio_get(folio); /* 可能与释放竞态 */
/* 对:投机拿引用 */
if (!folio_try_get(folio))
return -ENOENT; /* 到手前 folio 已释放 */
10. Reference summary
------------------------------------------------------------------------------------------------------------------------------------- 函数 头文件 说明 ------------------------------------------------------------------------------------------------------------------------------------- page_folio(page) page-flags.h 任一页转所属 folio folio_page(folio, n) page-flags.h folio 内第 n 页 folio_order(folio) mm.h 分配 order(单页为 0) folio_nr_pages(folio) mm.h 页数:1 << folio_order() folio_size(folio) mm.h 字节数:PAGE_SIZE << folio_order() folio_pfn(folio) mm.h 第一页 PFN folio_address(folio) mm.h 内核虚地址 folio_get(folio) mm.h 引用计数加一(要求已有引用) folio_put(folio) mm.h 引用计数减一;到零释放 folio_try_get(folio) page_ref.h 投机拿引用 folio_ref_count(folio) page_ref.h 原始引用计数 folio_test_large(folio) page-flags.h order ≥ 1 为真 folio_add_lru(folio) swap.h 加进 LRU filemap_grab_folio(mapping, index) pagemap.h 查或建 page-cache folio filemap_alloc_folio(gfp, order) pagemap.h 分配 page-cache folio readahead_folio(ractl) pagemap.h 从预读批次中获取下一个 folio -------------------------------------------------------------------------------------------------------------------------------------
11. 延伸阅读
LWN: Folios — Matthew Wilcox 撰写的 2021 年文章,介绍了 folio 概念并阐述了为何需要替换 `struct page`.
LWN: Large folios for the page cache — 关于针对文件映射内存(file-backed memory)和页缓存(page cache)支持大 folio(large folio)的后续报道.
mm/folio-compat.c — 兼容性适配层;其中的每个函数都是一个 `page` API 调用,负责将任务委托给对应的 `folio_*` 函数,并等待后续转换.
mm/filemap.c — 页缓存核心;`__filemap_get_folio()` 和 `filemap_add_folio()` 是主要的 folio 原生(folio-native)入口点.
include/linux/mm_types.h — `struct folio` 的标准定义,包含 `FOLIO_MATCH` 布局断言.
mthp.md — 多尺寸 THP(multi-size THP):对应于文件映射大 folio 的匿名内存实现;两者共用相同的 order-N folio 表示形式.
二、补充
1. page_folio() 宏详解
//include/linux/page-flags.h #define page_folio(p) (_Generic((p), \ const struct page *: (const struct folio *)_compound_head(p), \ struct page *: (struct folio *)_compound_head(p)))
作用: 将任意 struct page * 转换为它所属的 struct folio *。
1.1 为什么用 _Generic?为了保 const
_Generic 是 C11 的编译期类型分发,类似 switch(类型)。
这里只有两个分支:
(1) 传进来是 const struct page * → 返回 const struct folio *
(2) 传进来是 struct page * → 返回 struct folio *
如果写成普通函数 struct folio *page_folio(struct page *p),传 const struct page * 会丢 const,编译器告警/破坏 const 正确性。用 _Generic,const 进、const 出,非 const 进、非 const 出。
传其他类型(比如 int *)直接编译失败,提前拦错。
1.2 核心:_compound_head(p)
struct page 有两种:
(1) order-0 单页 / 大 folio 的 head 页:自己就是 head
(2) compound / 大 folio 的 tail 页:page->compound_head 存着回指 head 的指针,最低位 LSB=1 做标记
_compound_head() 逻辑就是:
if (PageTail(page)) // LSB 置位
return page->compound_head & ~1UL; // 跟随回指
else
return page; // 已是 head,直接返回
所以无论你拿到的是 folio 里的第 0 页还是第 N 个子页,经过这一步都回到 head。
1.3 为什么强转成 struct folio * 是安全的?
看 struct folio 定义:
struct folio {
union {
struct { flags,mapping,... } ;
struct page page; // 偏移 0
};
...
};
folio 的前半部分和 struct page 布局完全一致,内核还用 FOLIO_MATCH() 做 static_assert 锁死偏移。folio * 转 page * 就是 head 页,反过来 head 页转 folio * 也成立。
而宏的保证是:page_folio() 永远返回 head,转出来的一定是合法 folio。不存在“tail folio”。
1.4 展开例子
struct page *p = folio_page(folio, 3); // 拿到大 folio 第 4 个子页 struct folio *f = page_folio(p); // _compound_head 回到 head,再转 folio // f == 原 folio const struct page *cp = p; const struct folio *cf = page_folio(cp); // 走第一个分支,保持 const
1.5 两个坑
(1) 零开销但无检查:就是一次 LSB 判断 + 强转,不加引用计数、不判空、不检查越界。
(2) 竞态:如果你没持有引用,page_folio() 拿到 head 后,folio 可能并发被 split,head 就变了。内核文档要求:先 folio_try_get() pin 住,再确认该页还在该 folio 内。手里已有引用(比如 filemap_grab_folio() 刚返回的)才能直接用 folio_get()。
1.6 一句话:它解决老代码 compound_head(page) 到处手写、类型系统看不见的问题,用一个带 const 分发的宏强制“进 page、出 head folio”。
posted on 2026-10-05 21:41 Hello-World3 阅读(3) 评论(0) 收藏 举报
浙公网安备 33010602011771号