内存管理-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)    收藏  举报

导航