内存管理-5-物理内存数据结构-6-struct folio

基于 Linux 6.1.114

一、folio 简介

struct folio - 页面描述符(Linux内核页面管理的抽象单位)。folio 是 "front object of LRU and IO" 的缩写,代表一个或多个物理连续的页面。相比传统的 struct page,folio 为复合页(compound pages)提供了更统一的抽象。folio 可以是:

- 单页(0阶): 大小为 PAGE_SIZE(通常 4KB)
- 多页(高阶): 大小为 2^order * PAGE_SIZE(如 THP 2MB)
- HugeTLB 页:由多个连续页组成

内存布局:folio 中的所有字段都与 struct page 的对应字段位置对齐####,通过 FOLIO_MATCH(都是 offsetof 比较) 宏在编译时进行验证,确保二者可以安全互转。//TODO:细看每个成员的对比


二、struct folio

1. 定义

struct folio { //include/linux/mm_types.h
    union {
        struct {
            unsigned long flags;
            union {
                struct list_head lru;
                struct {
                    void *__filler;
                    unsigned int mlock_count;
                };
            };
            struct address_space *mapping;
            pgoff_t index;
            void *private;
            atomic_t _mapcount;
            atomic_t _refcount;
#ifdef CONFIG_MEMCG //1
            unsigned long memcg_data;
#endif
        };
        struct page page;
    };
    unsigned long _flags_1;
    unsigned long __head;
    unsigned char _folio_dtor;
    unsigned char _folio_order;
    atomic_t _total_mapcount;
    atomic_t _pincount;
#ifdef CONFIG_64BIT //1
    unsigned int _folio_nr_pages;
#endif
};

struct folio 表示一组连续的字节。一个 folio 是一组物理、虚拟和逻辑上连续的字节####。它的大小是 2 的幂次方,并且按 2 的幂次方对齐。它的大小至少为 %PAGE_SIZE。如果它位于页缓存中,则其文件偏移量是该 2 的幂次方的倍数。它可以映射到用户空间中任意页偏移量的地址,但其内核虚拟地址按其大小对齐。


2. 成员介绍

flags:

页面标志位,用于标记页面的状态和属性,常见标志 PG_locked/PG_dirty 等。


lru:

LRU 链表节点,用于链接 folio 到内存管理系统的 LRU 链表。包含多个链表节点:
- active_anon: 活跃匿名页链表(用户分配的堆/栈)
- inactive_anon: 不活跃匿名页链表
- active_file: 活跃文件页链表(最近访问的文件缓存)
- inactive_file: 不活跃文件页链表(长期未访问的缓存)

LRU 工作流程:新页 --> 加入 inactive --> 被引用 -->晋升 active --> 不再引用 --> 降级 inactive --> 内存压力 --> 从 LRU 移除并回收。当 folio 不在 LRU 上时,此字段可作他用(通过 __filler)。


__filler:

填充字段(LRU 节点的一部分)。与 lru 共享内存空间,当 lru 不使用时,这个字段用于数据对齐。本字段通常不直接使用。


mlock_count:

mlocked 引用计数。用于记录有多少个进程对此 folio 执行了 mlock.

计数语义:
当计数 > 0 时,folio 被固定在内存中。多个进程 mlock 同一页时,计数相应增加。计数仅在所有 mlock 都释放后才降为 0。

mlock 的作用:
- 将虚拟地址空间映射的页面锁定在物理内存;
- 防止 folio 被交换到 swap 或被回收;
- 提高实时应用的可预测性;

常见来源:
- mmap(addr, len, prot, flags | MAP_LOCKED)
- mlock(addr, len)、mlockall()
- 内核自身对关键页面的锁定.


mapping:

指向地址空间(address_space)的指针。用于连接 folio 与其所属的文件/缓冲区,确定页面的语义。

含义(根据 folio 的类型):
- 文件页:指向对应文件的 inode->i_mapping
- shmem/tmpfs 页:指向 shmem inode->i_mapping
- 匿名页(非 shmem):特殊值或 NULL
- swap 缓存页:有特殊编码(最低位置位)
- 其他页:NULL 或特殊值

使用场景:
- page cache 查询:根据 mapping 和 index 快速定位页
- fsync/sync:遍历 address_space 的脏页进行回写
- writepages:调用文件系统的回写方法
- 页面释放:根据 mapping 调用相应的 writepage
-
-配合 index 字段,可唯一确定 folio 的逻辑位置。


index:

在 address_space 中的索引/偏移。

含义(根据 folio 类型):
- 文件页:folio 在文件中对应的页号。例如 index=2 表示文件的第 2 页(不是字节),物理位置 = index * PAGE_SIZE
- shmem/tmpfs:虚拟地址空间中的页偏移
- 匿名页:虚拟地址空间的某个页号
- swap 页:swap 设备上的页号

多页 folio 的 index:对于 THP(order=9,512 页),index 指向第一页,后续 510 页隐式地对应 index+1...index+511,但内核实际上通过 folio_page(folio, nr) 访问子页。

计算相关:字节偏移 = index * PAGE_SIZE; 对于多页 folio,尾页 index = 头页 index + order;


private:

私有数据指针。由各个文件系统或 I/O 子系统自定义使用。内核对 private 的解释完全取决于 folio 的类型和 mapping。

常见用途:
- ext4/ext3:指向 buffer_head 链表(每个 folio 可包含多个块);
- btrfs:存储块 I/O 私有数据、错误标记等;
- shmem:存储 xattr 数据、私有链表等;
- swap:存储额外的 swap 相关信息;
- NFS/网络 FS:协议特定的元数据;
- fuse:用户空间文件系统的上下文;

生命周期:页面分配时初始化为 NULL(通常),特定子系统在使用前初始化 private,页面回收前必须清理 private(否则导致内存泄漏)。

注意事项:若 private 非 NULL,回收时需特殊处理。某些操作(如 truncate)可能清除 private

 

_mapcount:

用户页表映射计数(第一页),原子变量类型。

含义(值的解释):
-1: 页未被映射到任何用户页表中,或为 PageReserved;
-2: 对应特定的 compound 页标记;
0: 页处于某些中间状态或已被标记为不可用;
>0: 页被映射到 N 个不同的用户页表中。例如 _mapcount=3 表示被 3 个进程的页表引用;

操作:page_add_anon_rmap():增加计数(在页面映射时)。page_remove_rmap():减少计数(在取消映射时)。page_mapcount():读取计数。

用途:
- 决定页面是否可被回收(>0 的页面一般不回收)
- 支持 page migration 时需要找到所有映射
- 统计 Copy-on-Write (CoW) 候选页
- 决定是否执行懒复制 vs 预先复制

对于复合页(order > 0):_mapcount 记录的是第一个子页(head page)的映射计数,复合页的尾页(tail pages)的 _mapcount 通常不可用,复合页的总映射计数由 _total_mapcount 记录。

与 _refcount 的区别:
- _mapcount:统计页表项数(用户映射)
- _refcount:统计所有引用(包括缓存、I/O 等)
- 一个页表项对应一个 _refcount 引用


_refcount:

引用计数。用于记录对此 folio 的引用总数, 初值为 1(页创建时),get_page() 或 page_ref_add() 时 +1,put_page() 或 page_ref_dec() 时 -1, 计数降至 0 时,页面会被释放或回收。

引用来源:
- 页表项(每个用户映射一个),例如多个进程 fork() 共享的页,映射 +N。
- 页 cache 自身的引用,页 cache 对页面的引用时 +1.
- GUP (Get User Pages) 固定,pin_user_pages() 操作时 +1,长期固定(RDMA、GPU 等)
- I/O 操作,正在进行磁盘 I/O 时临时 +1,读/写操作完成后 -1。
- 内核临时使用,页表遍历、页面检索等,临时增加后立即释放。
- swap 缓存,页面在 swap 缓存中时的引用。

关键操作:
- page_ref_count(page):读取引用计数;
- page_ref_add(page, n):增加 N 个引用;
- page_ref_sub(page, n):减少 N 个引用;
- get_page(page):+1 引用;
- put_page(page):-1 引用,可能触发回收;
- page_ref_freeze(page, count):特殊用途,冻结页面;

特殊值:REFCOUNT_SATURATED (INT_MAX),计数溢出保护,防止恶意代码通过循环增加引用计数的攻击,达到此值后,put_page() 不再减少。

页面回收的关键:shrink_page_list() 中检查 _refcount,若 _refcount > 1,页面被锁定,无法回收,只有 _refcount == 1(或 0)的页面才能释放。


memcg_data:

内存控制组关联数据,用于将 folio 与内存 cgroup(memcg)关联,用于内存使用统计和限制。

memcg 的目的:限制每个 cgroup 能使用的内存总量。按类型统计文件缓存、slab、anon、swap 等。OOM 时选择合适的 victim 进程进行杀死。提供 /proc/[pid]/cgroup 等接口供用户查询。

编码方式:低位指向 memcg_data 结构体(指针压缩); 保留位用于 memcg 内部标志; MEMCG_DATA_LIVE:memcg 在线,页面应被计费; MEMCG_DATA_OBJCGS:页面关联了 slab objcgs;

统计类别(LRU_GEN 模式):
- NR_FILE_PAGES:文件缓存总数
- NR_FILE_DIRTY:脏文件页
- NR_FILE_WRITEBACK:正在写回的文件页
- NR_ANON_MAPPED:已映射的匿名页
- NR_SLAB_*:slab 分配器的各类页面
- NR_KERNEL_*:内核各组件的页面
- NR_SWAP:swap 缓存页

用途举例:
- 分配时检查:page_alloc() 检查 memcg 是否超限,决定是否分配;
- 回收优先级:OOM killer 优先回收超限 memcg 的页面;
- 统计接口: memory.stat、memory.usage_in_bytes 等;
- 缓存压力:某个 memcg 接近限制时,优先回收其页面

当 CONFIG_MEMCG 未定义时:此字段不存在,系统不进行内存 cgroup 统计,所有内存按全局统计(如容器场景无法隔离)


page:

与 struct page 的并集,用于在向后兼容层中,folio 可被当作 struct page 使用。

设计理由:Linux 内核正在从 struct page 逐步迁移到 struct folio,过渡阶段需要支持两种 API 并存,通过并集实现二者无成本互转。

内存对齐验证:page 的所有字段都位于 folio 前面的字段中(联合体),通过 FOLIO_MATCH 宏在编译时逐一验证,例如:FOLIO_MATCH(flags, flags);

使用建议:新代码应直接使用 folio API(如 folio_get(), folio_put())。旧驱动/文件系统可继续使用 page API(自动转换)。混用时通过 folio_page() 和 page_folio() 转换。

成本:零开销,编译时完全展开,无运行时操作。


_flags_1:

扩展标志位(第二个页框的标志)。位于 struct folio 的末尾(紧跟在 page 之后),对应 struct page[1]->flags(第二页的标志位),物理地址 = folio 物理地址 + PAGE_SIZE

对于复合页(order >= 1),用于存储适用于整个 folio 的标志,这些标志与单页 folio 的 flags 不同,有特殊含义。

常见标志(部分):FOLIO_ORDER_0 相关标记,其他 compound page 特定标志。

特点与限制:对于单页 folio(order=0),_flags_1 位于 folio 之外的未使用内存。对于多页 folio,_flags_1 位于第二页的起始处。因此不同 order 的 folio 在使用 _flags_1 时有不同的含义。


__head:

复合页头部指针。

内存位置:位于 struct folio[1].lru(第二页的 lru 字段),对于单页 folio,此位置未使用。

用途:对于复合页的尾页(tail pages),此字段指向头页的 folio,允许从任意子页快速定位回头页,特别重要的是在中断等不能睡眠的上下文中。

编码方式:存储的是 folio 指针,加上特殊标记位。标记位 1: 表示这是一个 compound page,标记位 0: 用于表示页面的某些状态。

提取方法:compound_head() 宏安全提取 __head 指向的头页,处理标记位的清除和验证,对于非复合页,可能会返回页面本身。

性能优化:避免遍历所有尾页来查找头页,O(1) 时间复杂度获取头页,支持 tail page 的快速识别。


_folio_dtor:

复合页析构函数类型标识,指定复合页释放时应调用的析构函数。

内存位置:位于 struct page[1].compound_dtor(第二页的特定字段), 对应位置与 struct page 的 compound_dtor 对齐。

可能的析构函数类型:
- COMPOUND_PAGE_DTOR (0):标准复合页析构,调用 free_compound_page(),递减所有尾页的引用计数,最终释放所有物理页到伙伴系统。
- HUGETLB_PAGE_DTOR (1):HugeTLB 页析构,调用 free_huge_page(),页面返回给 HugeTLB 内存池(不是伙伴系统),涉及复杂的 cgroup 等计费逻辑。
- TRANS_HUGE_PAGE_DTOR (2):透明巨页(THP)析构,调用 free_transhuge_page(),THP 特定的清理逻辑,处理 THP 分裂时的引用计数。

用途:__free_pages_ok() 释放 folio 时查阅此值; 根据类型调用不同的析构函数; 确保不同类型的复合页被正确清理,例如 HugeTLB 页如果用标准析构,会导致内存泄漏;

设置时机:复合页创建时设置(set_compound_page_dtor()),通常由创建者负责设置正确的值,错误设置可能导致内存泄漏或页面错误释放。

典型流程:prep_compound_page(page, order) 创建复合页,set_compound_page_dtor(page, dtor) 设置析构函数,folio 被使用,put_page(page) 触发释放,根据 _folio_dtor 调用对应析构函数。


_folio_order:

复合页的阶数(order)

内存位置:位于 struct page[1].compound_order(第二页),对应位置与 struct page 的 compound_order 对齐。

含义:folio 包含的物理连续页数 = 2^_folio_order,常见值及对应大小:
- order=0: 1 页 = 1 × 4KB = 4KB(单页,不是复合页)
- order=1: 2 页 = 2 × 4KB = 8KB
- order=2: 4 页 = 4 × 4KB = 16KB
- order=3: 8 页 = 8 × 4KB = 32KB
- order=9: 512 页 = 512 × 4KB = 2MB(THP 透明巨页)
- order=10: 1024 页 = 1024 × 4KB = 4MB
- order=20: 1MB 页(某些架构的 HugeTLB)
- order=30: 1GB 页(某些架构的 HugeTLB,如 ARM64)

计算总大小:folio_size = PAGE_SIZE << _folio_order。

获取方法:folio_order(folio) 宏,page_order(page) 宏(针对 struct page)。

用途:伙伴系统分配/释放时查询阶数; 判断是否为复合页(order >= 1); 计算 folio 的虚拟地址范围; 迭代 folio 中的所有子页。

特性:对于单页 folio(order=0),通常不作为复合页处理; 对于多页 folio(order >= 1),所有子页共享同一 order; 页面迁移时不会改变 order(migrated 到其他内存仍保持相同大小).

页面中的位置:只有复合页的"头页"有意义的 _folio_order, 尾页的 compound_order 通常不使用。


_total_mapcount:

复合页的总映射计数。

内存位置:位于 struct page[1].compound_mapcount(第二页),对应位置与 struct page 的 compound_mapcount 对齐。

作用:记录整个 folio(所有子页)被映射到用户页表的总次数。

含义(值的解释):-1: folio 未被映射到任何用户页表,0: folio 可能处于某些中间状态,>0: folio 被映射到用户页表中。对于 THP,_total_mapcount = (映射次数 × 512)####

用途:判断复合页是否可以合并或分割。决定 THP 分裂的时机(分裂后性能可能更好)。支持 THP swapout 的批量操作。优化 page migration 时的映射更新。

操作:compound_mapcount(page) 读取值。page_add/remove_rmap() 操作时同时更新 _mapcount 和 _total_mapcount.

特性(对于 THP):
- 当 THP 被单个进程映射时,_total_mapcount = 512(THP 的页数)
- 当 THP 被分裂后,分裂出的单页各自有独立的 _mapcount。
- swapout 时的优化,如果 _total_mapcount 很高,可能不值得交换

与 _mapcount 的区别:

┌──────────────────┬──────────────────────┬────────────────────────────┐
│ 字段             │ _mapcount            │ _total_mapcount            │
├──────────────────┼──────────────────────┼────────────────────────────┤
│ 位置             │ 第一页               │ 第二页                     │
│ 适用范围         │ 单页/多页            │ 仅多页                     │
│ 含义             │ 第一页映射数         │ 所有子页总映射数           │
│ 使用场景         │ 低级操作             │ 复合页策略                 │
├──────────────────┼──────────────────────┼────────────────────────────┤
│ 示例(THP,512页) │                      │                            │
│ 被进程A映射      │ _mapcount += 1       │ _total_mapcount += 512     │
│ 被进程B映射      │ _mapcount += 0*      │ _total_mapcount += 512     │
│ (*因为A已映射)   │                      │                            │
└──────────────────┴──────────────────────┴────────────────────────────┘


_pincount:

Pin 计数(FOLL_PIN 引用)。用于记录通过 pin_user_pages()(GUP 快速路径)操作对 folio 的固定次数。

内存位置:位于 struct page[1].compound_pincount(第二页),对应位置与 struct page 的 compound_pincount 对齐。

Pin 与引用计数的根本区别:

┌───────────────────┬─────────────────────┬────────────────────────────┐
│ 特性              │ 引用计数 (_ref)     │ Pin 计数 (_pin)            │
├───────────────────┼─────────────────────┼────────────────────────────┤
│ 生命周期          │ 短期                │ 长期(可能数秒)             │
│ 何时释放          │ 立即(I/O 后)        │ 受应用控制                 │
│ 能否迁移          │ 是                  │ 否(物理地址固定)           │
│ 能否回收          │ 是                  │ 否                         │
│ 典型来源          │ 页表、缓存          │ RDMA、GPU DirectIO         │
│ 风险              │ 页被回收            │ 内存泄漏                   │
└───────────────────┴─────────────────────┴────────────────────────────┘

何时增加(pin 操作):
(1) pin_user_pages(addr, nr_pages, flags, pages), FOLL_PIN 标志的 GUP 操作, 长期固定页面在内存中.
(2) RDMA 缓冲区注册, InfiniBand 等 RDMA 硬件需要物理地址保证不变, NVMe-oF、iSER 等协议.
(3) GPU DirectIO,NVIDIA/AMD/Intel GPU 直接访问系统内存,需要 DMA 访问的 GPU 缓冲区。

何时释放(unpin 操作):
(1) unpin_user_page(page), 释放单个 pin.
(2) unpin_user_pages(pages, nr), 批量释放.
(3) unpin_user_pages_dirty_lock(pages, nr, make_dirty) 标记脏页的批量释放.

内存热拔插的含义:系统尝试热拔插内存时,先检查是否有 pinned 页, 若存在 _pincount > 0 的页面,热拔插无法进行, 必须应用主动 unpin 才能允许热拔插, 这是一种防护机制,避免热拔插导致 GPU/RDMA 的物理地址失效。

安全性考虑:Pinned 页面必须由应用正确管理,否则:页面永远不能被回收(内存泄漏)、系统缺页异常可能导致卡死、内存碎片化严重,因此内核不允许大量长期 pin(有限制)。

操作原子性:使用原子操作保证 pin/unpin 的并发安全,多个线程的 pin 操作可并发进行。


_folio_nr_pages:

含义是该 folio 包含的物理页总数,值等于 1 << _folio_order,单位:页(每页 PAGE_SIZE 字节,通常 4KB)

内存位置:位于 struct page[1] 的末尾偏移处, 对应 struct page 的 compound_nr 字段, 仅当 CONFIG_64BIT 定义时存在。

常见值:1(单页):order=0; 2、4、8(小阶):order=1,2,3; 512(THP):order=9; 262144(1GB HugeTLB):order=18。

计算关系:_folio_nr_pages = 1 << _folio_order, 例如 order=9 --> _folio_nr_pages=512(2^9)

用途:快速获取 folio 的总页数; 避免每次都计算 (1 << _folio_order),提高性能; 遍历 folio 中的所有页面时用于循环边界; 统计内存使用量时直接相乘:bytes = _folio_nr_pages * PAGE_SIZE.

与 _folio_order 的区别:_folio_order 是指数形式(需要左移计算),存储空间小。_folio_nr_pages 是直接的页数,查询更快,但占用更多空间。

宏接口:
folio_nr_pages(folio):跨平台获取页数,自动处理 CONFIG_64BIT 的差异。
- folio_size(folio):计算总字节数,返回值 = folio_nr_pages * PAGE_SIZE。

初始化时机:prep_compound_page() 中设置,永远不会动态改变(页面大小在分配后固定)。

 

posted on 2026-07-21 20:56  Hello-World3  阅读(2)  评论(0)    收藏  举报

导航