一、struct page 结构详解
struct page 是Linux内核中用于描述每个物理页帧的核心数据结构。系统中的每个物理页帧都有一个对应的 struct page。
c
struct page {
unsigned long flags; /* 原子标志位,部分可能异步更新 */
atomic_t _count; /* 使用计数 */
union {
atomic_t _mapcount; /* 映射计数,表示被多少PTE映射 */
struct { /* SLUB使用 */
u16 inuse;
u16 objects;
};
};
union {
struct {
unsigned long private; /* 私有数据 */
struct address_space *mapping; /* 地址空间映射 */
};
#if USE_SPLIT_PTLOCKS
spinlock_t ptl; /* 页表锁 */
#endif
struct kmem_cache *slab; /* SLUB: slab指针 */
struct page *first_page; /* 复合页尾页指向首页 */
};
union {
pgoff_t index; /* 在mapping中的偏移 */
void *freelist; /* SLUB: freelist */
};
struct list_head lru; /* LRU链表,用于页面回收 */
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* 内核虚拟地址 */
#endif
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
unsigned long debug_flags;
#endif
#ifdef CONFIG_KMEMCHECK
void *shadow; /* kmemcheck跟踪 */
#endif
};
bash
struct page 内存布局 (64位系统,约64字节):
+------------------------------------------------------------------+
| offset | field | size | description |
+------------------------------------------------------------------+
| 0 | flags | 8 | 页面状态标志位 |
| 8 | _count | 4 | 引用计数 |
| 12 | _mapcount | 4 | 映射计数 (或SLUB的inuse/objects) |
| 16 | private | 8 | 私有数据 |
| 24 | mapping | 8 | 地址空间指针 |
| 32 | index | 8 | 文件偏移 |
| 40 | lru.next | 8 | LRU链表next |
| 48 | lru.prev | 8 | LRU链表prev |
| 56 | virtual | 8 | 虚拟地址(可选) |
+------------------------------------------------------------------+
注意: struct page设计非常紧凑,使用union来节省空间,
不同使用场景下字段含义不同。
二、flags 标志位详解
2.1 标志位布局
c
page->flags 位布局 (32/64位系统):
+------------------------------------------------------------------+
| [SECTION] [NODE] [ZONE] [....FLAGS....] |
+------------------------------------------------------------------+
具体布局取决于配置:
- 非SPARSEMEM: | NODE | ZONE | ... | FLAGS |
- SPARSEMEM有空间: | SECTION | NODE | ZONE | ... | FLAGS |
- SPARSEMEM无空间: | SECTION | ZONE | ... | FLAGS |
位域计算:
#define SECTIONS_PGOFF ((sizeof(unsigned long)*8) - SECTIONS_WIDTH)
#define NODES_PGOFF (SECTIONS_PGOFF - NODES_WIDTH)
#define ZONES_PGOFF (NODES_PGOFF - ZONES_WIDTH)
c
页面状态标志位 (include/linux/page-flags.h):
+------------------------------------------------------------------+
| 标志位 | 值 | 含义 |
+------------------------------------------------------------------+
| PG_locked | 0 | 页面被锁定,正在使用 |
| PG_error | 1 | 页面I/O发生错误 |
| PG_referenced | 2 | 页面最近被访问过 |
| PG_uptodate | 3 | 页面数据有效(已从磁盘读取) |
| PG_dirty | 4 | 页面内容被修改 |
| PG_lru | 5 | 页面在LRU链表中 |
| PG_active | 6 | 页面在活跃LRU链表中 |
| PG_slab | 7 | 页面属于slab分配器 |
| PG_owner_priv_1 | 8 | 所有者私有标志 |
| PG_arch_1 | 9 | 架构特定标志 |
| PG_reserved | 10 | 保留页面,不可交换 |
| PG_private | 11 | page->private有效 |
| PG_private_2 | 12 | 私有标志2 |
| PG_writeback | 13 | 页面正在回写到磁盘 |
| PG_head | 14 | 复合页的首页 |
| PG_swapcache | 15 | 页面在swap缓存中 |
| PG_mappedtodisk | 16 | 页面已映射到磁盘 |
| PG_reclaim | 17 | 页面将被回收 |
| PG_swapbacked | 18 | 页面有swap后备存储 |
| PG_unevictable | 19 | 页面不可驱逐 |
| PG_mlocked | 20 | 页面被mlock()锁定 |
+------------------------------------------------------------------+
-
I/O 与数据有效性 :
PG_locked(正被 I/O 使用)、PG_uptodate(内容有效)、PG_dirty(被改写待写回)、PG_writeback(正在回写)、PG_error(I/O 出错)。它们共同回答一页内容与磁盘之间是否一致。
-
页缓存/文件 :
PG_private(private里有文件系统数据)、PG_private_2、PG_mappedtodisk(已在磁盘分配了块)。 -
回收/交换 :
PG_lru(在 LRU 链表)、PG_active(活跃一代)、PG_referenced(最近被访问)、
PG_reclaim(待回收)、PG_swapbacked(有 swap 后备)、PG_swapcache(处于交换缓存)、PG_unevictable(不可驱逐)。 -
类型/归属 :
PG_slab(属于 slab)、PG_reserved(保留/不可换)、PG_head/PG_tail(复合页首尾)、PG_mlocked(被 mlock 锁定)。 -
架构/外部私有 :
PG_owner_priv_1、PG_arch_1------内核不规定其含义,留给文件系统或具体架构自由使用。
flags 状态迁移示例(以文件读 I/O 为例)
各标志不是独立翻转,而是按操作阶段 依次迁移。以一次文件读 I/O 到进程写、刷盘为例,
观察 PG_locked / PG_uptodate / PG_dirty / PG_writeback 如何配合:
scss
alloc 后进入 pagecache: SetPageUptodate(page) (数据有效)
发起磁盘读: lock_page(page) 置 PG_locked
读完成: unlock_page(page) 清 PG_locked
进程写后: SetPageDirty(page) 置 PG_dirty
刷盘完成: clear_page_dirty_for_io + ClearPageWriteback
三、引用计数详解
3.1 _count 引用计数
diff
page->_count 引用计数的含义:
+------------------------------------------------------------------+
| _count值 | 状态描述 |
+------------------------------------------------------------------+
| 0 | 页面空闲,可分配 |
| 1 | 页面被分配,有一个使用者 |
| >1 | 页面有多个使用者 |
+------------------------------------------------------------------+
引用计数增加的场景:
1. 页面被分配时 (alloc_pages)
2. 页面被映射到用户空间时 (get_user_pages)
3. 页面被加入页缓存时
4. 页面被锁定时 (lock_page)
引用计数减少的场景:
1. 页面被释放时 (free_pages)
2. 页面映射被解除时
3. 页面从页缓存移除时
4. 页面解锁时
_count 是引用计数 :任何代码想安全地使用一页,必须先调用 get_page() 递增计数,
用完后调用 put_page() 递减。只有当计数归零(page_count()==0)时,内核才认定"没有任何
使用者仍然引用这一页",从而把它释放回 buddy 系统或回收。
上面的增加/减少场景揭示了它与其他子系统的关系:
- 页缓存 :页加入 pagecache 时
get_page,移除时put_page,保证页数据在仍被引用
期间不会被换出。 - 用户页表 :映射建立时递增、解除时递减;因此
page_count与page_mapcount
之差,恰反映"被内核持有但尚未映射到用户空间的引用数"。 - 锁定 :
lock_page/unlock_page也会增减引用,让等待者安全度过 I/O 窗口。
3.2 引用计数操作函数
c
/* 获取页面引用 */
static inline void get_page(struct page *page)
{
page = compound_head(page);
VM_BUG_ON(atomic_read(&page->_count) == 0);
atomic_inc(&page->_count); /* 引用计数+1 */
}
/* 释放页面引用,返回是否为最后一个引用 */
static inline int put_page_testzero(struct page *page)
{
VM_BUG_ON(atomic_read(&page->_count) == 0);
return atomic_dec_and_test(&page->_count); /* 引用计数-1并测试 */
}
/* 获取页面引用计数 */
static inline int page_count(struct page *page)
{
return atomic_read(&compound_head(page)->_count);
}
3.3 _mapcount 映射计数
rust
page->_mapcount 映射计数的含义:
+------------------------------------------------------------------+
| _mapcount值 | 状态描述 |
+------------------------------------------------------------------+
| -1 | 页面未被映射到任何进程的页表 |
| 0 | 页面被映射到一个进程的页表 |
| >0 | 页面被映射到多个进程的页表(共享页) |
+------------------------------------------------------------------+
映射计数用途:
1. 判断页面是否被映射 (page_mapped)
2. 限制反向映射搜索范围
3. 页面回收决策
相关函数:
page_mapped(page) -> (_mapcount >= 0)
page_mapcount(page) -> (_mapcount + 1) /* 返回实际映射数 */
与 _count 的"持有引用"不同,_mapcount 专门统计被多少个页表项 (PTE) 映射进用户
虚拟地址空间 。它从 -1 起算(未映射),每建立一个映射 +1、解除一个 -1,并借助
atomic_inc_and_test / atomic_add_negative(-1) 在 -1↔0 的跳变处捕捉"有无映射"。
这套计数是反向映射 (rmap) 的性能开关:
- 回收 :
try_to_unmap()靠_mapcount知道要反向解除几个 PTE,避免无谓扫描。 - 写时复制 (COW) :
do_wp_page用page_mapcount==1判断是否独占,独占则直接改
保护位复用,否则才复制整页(见 3.5 节)。 - 可移动性 :
_mapcount==0通常意味着该页未映射,更易迁移/换出。
四、页面类型与用途
4.1 页面类型分类
lua
页面类型分类图:
+------------------------------------------------------------------+
| 物理页面分类 |
+------------------------------------------------------------------+
| |
| +------------------+ +------------------+ +----------------+ |
| | 匿名页面 | | 文件页面 | | 特殊页面 | |
| | (Anonymous) | | (File/PageCache)| | (Special) | |
| +------------------+ +------------------+ +----------------+ |
| | - 进程私有数据 | | - 文件内容缓存 | | - 内核使用 | |
| | - 堆/栈分配 | | - 共享库代码 | | - Slab页面 | |
| | - malloc结果 | | - mmap文件 | | - 页表页面 | |
| | | | | | - DMA缓冲 | |
| +------------------+ +------------------+ +----------------+ |
| | | | |
| v v v |
| +------------------+ +------------------+ +----------------+ |
| | Swap后备存储 | | 文件后备存储 | | 无后备存储 | |
| | (swapcache) | | (address_space) | | | |
| +------------------+ +------------------+ +----------------+ |
| |
+------------------------------------------------------------------+
把物理页按"由什么后备存储持久化"来分类,是理解内存语义很有用的一个角度:
- 匿名页 (Anonymous) :与文件无关,来自堆、栈、
malloc(brk / mmap 私有)、fork
后的内存等。它唯一的旁持久化途径是 swap ------换出时把内容写进交换分区,换回时再读
回来,因此挂PG_swapbacked。 - 文件页 (File/PageCache) :
read/mmap文件产生的缓存,内容以文件 为后备存储。
它是可回收的"缓存",回收时往往无需写盘(磁盘本就有副本),只需把改动标记 dirty 待写回。 - 特殊页:slab 页(内核对象)、页表页、DMA 缓冲等,内核直接持有、通常不可换出。
struct page 正是靠 flags(如 PG_swapcache)和 mapping/private 区分这三大类,
进而决定回收时的不同策略
4.2 页面mapping字段含义
c
page->mapping 字段的含义:
+------------------------------------------------------------------+
| mapping值 | 页面类型 |
+------------------------------------------------------------------+
| NULL | 匿名页面(早期) |
| (address_space*) | PAGE_MAPPING_ANON | 匿名页面(指向anon_vma) |
| (address_space*) | 文件页面(指向inode映射) |
| NULL (PageSlab) | Slab页面 |
+------------------------------------------------------------------+
判断页面类型:
static inline int PageAnon(struct page *page)
{
return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;
}
static inline struct address_space *page_mapping(struct page *page)
{
struct address_space *mapping = page->mapping;
if (unlikely(PageSwapCache(page)))
mapping = &swapper_space;
else if (unlikely((unsigned long)mapping & PAGE_MAPPING_ANON))
mapping = NULL;
return mapping;
}
mapping 是对齐的指针 ,其最低位恒为 0;内核
便把最低位复用作为标志------置 1 表示"这是匿名页、指向 anon_vma",置 0 才是真正的
address_space 指针。PageAnon() 与 page_mapping() 正是利用这一点在 O(1) 时间内分
辨页类型,而不必另查字段。page_mapping() 返回的要么是文件页的 address_space,要么
是 swap 缓存(swapper_space),匿名页则返回 NULL------保证上层(writeback、回收、rmap)
拿到的永远是"能用来定位这个页的映射"。
五、复合页 (Compound Page)
5.1 复合页结构
ini
复合页用于表示大于一个页帧的连续物理内存块:
+------------------------------------------------------------------+
| 复合页结构 (Order=2, 4页) |
+------------------------------------------------------------------+
| |
| +------------+------------+------------+------------+ |
| | Head Page | Tail Page | Tail Page | Tail Page | |
| | (首页) | (尾页1) | (尾页2) | (尾页3) | |
| +------------+------------+------------+------------+ |
| | | | | |
| v v v v |
| +------------+------------+------------+------------+ |
| |PG_head=1 |PG_tail=1 |PG_tail=1 |PG_tail=1 | |
| |_count=N |_count=0 |_count=0 |_count=0 | |
| |compound |first_page |first_page |first_page | |
| |order=2 |----->Head |----->Head |----->Head | |
| |dtor | | | | |
| +------------+------------+------------+------------+ |
| |
| 首页存储: |
| - page[1].lru.next = destructor函数指针 |
| - page[1].lru.prev = order值 |
| |
+------------------------------------------------------------------+
复合页 (compound page) 用一整块连续的物理页表示一个"逻辑上更大的对象",典型用途是
大页 (huge page / THP) 和大块 DMA 缓冲。它把第一页设为首页 (head) 、其余设为 尾页 (tail):引用计数与大部分标志只维护在首页上,尾页几乎不参与这些维护。
这里再次体现 union 复用的思想------首页并不额外开字段存"析构函数"和"order",而是复用
第 2 页 (page[1]) 的 lru 两个指针 :lru.next 存析构回调,lru.prev 存 order。
尾页则用 first_page 指回首页,让 compound_head() 能 O(1) 归一到首页。这样整个复合
页不增加任何结构开销 ,只需 PG_head/PG_tail 标志区分角色。正是这种"首尾分工 +
字段复用",使得大页在晦涩的伙伴系统里也能被当成单个可回收实体对待。
5.2 复合页操作函数
c
/* 创建复合页 */
void prep_compound_page(struct page *page, unsigned long order)
{
int i;
int nr_pages = 1 << order;
set_compound_page_dtor(page, free_compound_page);
set_compound_order(page, order);
__SetPageHead(page);
for (i = 1; i < nr_pages; i++) {
struct page *p = page + i;
__SetPageTail(p);
p->first_page = page; /* 尾页指向首页 */
}
}
/* 获取复合页首页 */
static inline struct page *compound_head(struct page *page)
{
if (unlikely(PageTail(page)))
return page->first_page;
return page;
}
/* 获取复合页order */
static inline int compound_order(struct page *page)
{
if (!PageHead(page))
return 0;
return (unsigned long)page[1].lru.prev;
}
六、页面地址转换
6.0 内存模型:struct page 从哪来
struct page 从哪来是理解地址转换的前提:系统中的每个物理页帧都对应一个 struct page,
它们被组织成一个大数组,通常是全局的 mem_map。给定页帧号 pfn,其 struct page 就是
数组的第 pfn 项------pfn_to_page() / page_to_pfn() 正是这套"数组下标 ↔ 指针"的换算
(具体宏见 6.1 节)。由此形成贯穿全文的换算链:
scss
物理地址 物理页帧号 struct page 内核虚拟地址
0x8abc000 <-> 0x8abc <-> mem_map[0x8abc] <-> __va(0x8abc000)
(phys) (pfn) (&page) (virt, 直接映射区)
- 低端内存 :位于内核直接映射区,
__va(page_to_pfn(page) << PAGE_SHIFT)可直接算出
虚拟地址,无需存储额外地址。 - 高端内存 :不在直接映射区,需
kmap()/kmap_atomic()临时映射,映射后的地址才
存入可选的page->virtual字段(见 1.3 节)。
这也是"结构体很小(64 位下约 64 字节)、大量用 union 复用字段"的根本原因:一个
struct page配一个 4KB 物理页,几 GB 内存就需要几百万个 struct page,任何不必要的字段都会成倍放大内存占用。
6.1 页帧号与物理地址转换
ini
页帧号(PFN)与物理地址转换:
+------------------------------------------------------------------+
| |
| 物理地址 = PFN << PAGE_SHIFT |
| PFN = 物理地址 >> PAGE_SHIFT |
| |
| PAGE_SHIFT = 12 (4KB页面) |
| PAGE_SIZE = 1 << PAGE_SHIFT = 4096 |
| |
+------------------------------------------------------------------+
转换函数:
#define page_to_pfn(page) ((page) - mem_map)
#define pfn_to_page(pfn) (mem_map + (pfn))
#define pfn_to_phys(pfn) ((pfn) << PAGE_SHIFT)
#define phys_to_pfn(phys) ((phys) >> PAGE_SHIFT)
struct page 指针、页帧号 (pfn)、物理地址三者本质是在描述同一块物理内存的不同编号
方式 ,因此可以直接算术互换。mem_map 在平坦内存 (FLATMEM) 下是一个连续的
struct page 数组,下标即 pfn------于是 page_to_pfn() 只是"指针相减"、pfn_to_page()
只是"数组加下标",极快。它的意义在于:任何拿到 struct page 的代码都可随时换算到
硬件相关的物理/虚拟地址 ,反之亦然(如从页表 PTE 的 PFN 找回 struct page)。
NUMA / SPARSEMEM 下这条链会被重写成按节点/区段定位,但语义一致
(见 numa/ 与 6.0 节)。
6.2 页面与虚拟地址转换
scss
页面与虚拟地址转换:
+------------------------------------------------------------------+
| |
| 内核直接映射区: |
| virt = __va(pfn << PAGE_SHIFT) |
| pfn = __pa(virt) >> PAGE_SHIFT |
| |
| 低端内存页面: |
| lowmem_page_address(page) = __va(page_to_pfn(page) << PAGE_SHIFT)|
| |
| 高端内存页面: |
| 需要通过kmap()临时映射 |
| page_address(page) 返回映射后的虚拟地址 |
| |
+------------------------------------------------------------------+
转换函数:
static __always_inline void *lowmem_page_address(struct page *page)
{
return __va(page_to_pfn(page) << PAGE_SHIFT);
}
/* 高端内存映射 */
void *kmap(struct page *page); /* 永久映射 */
void *kmap_atomic(struct page *page); /* 临时原子映射 */
void kunmap(struct page *page);
void kunmap_atomic(void *addr);
低端内存(如 x86 的 ZONE_NORMAL)位于内核直接映射区,__va() 给物理地址加一个固定偏移
即得虚拟地址,无需存储;而高端内存 (highmem) 页不在直接映射区内,必须先
kmap()/kmap_atomic() 划出一块内核虚拟地址把它映射进来,再用 page_address() 取回、
用完 kunmap* 释放。这就是为什么 struct page 的 virtual 字段只在 WANT_PAGE_VIRTUAL
(或高端内存哈希映射)时才存在、才有意义------它正好回答了"哪种内存需要、哪种内存不需
要额外记一个虚拟地址"。(x86 32 位下该字段可能被压缩到 16 位,见 mm_types.h 注释。)
6.3 地址转换流程图
diff
地址转换流程:
+------------------------------------------------------------------+
| 用户虚拟地址 |
| 0x7fff1234000 |
+------------------------------------------------------------------+
|
| 页表查找
v
+------------------------------------------------------------------+
| 物理页帧号 |
| PFN = 0x8ABC |
+------------------------------------------------------------------+
|
| PFN -> struct page
v
+------------------------------------------------------------------+
| struct page |
| mem_map + 0x8ABC |
+------------------------------------------------------------------+
|
| page -> phys addr
v
+------------------------------------------------------------------+
| 物理地址 |
| 0x8ABC000 |
+------------------------------------------------------------------+
|
| __va() (直接映射区)
v
+------------------------------------------------------------------+
| 内核虚拟地址 |
| 0xffff88008ABC000 |
+------------------------------------------------------------------+
这张图把三类地址串成一条完整的往返链 :从用户虚拟地址出发,经 MMU 页表 定位到
页帧号 (PFN),由 PFN 找回 struct page(内核内部分配与回收都以它为准),再从 PFN 算
出物理地址,最后经直接映射区的 __va() 得到内核虚拟地址以便读写页内容。反向流程同理。
这条链贯穿 do_page_fault(缺页)、writeback、swap 等几乎所有内存路径------掌握它,就能
在读任何内存相关代码时快速建立"这段虚拟地址到底指向哪一页"的坐标。
七、页面分配与释放API
7.1 页面分配API
c
/* 分配单个页面 */
struct page *alloc_page(gfp_t gfp_mask);
void *get_free_page(gfp_t gfp_mask); /* 返回虚拟地址 */
void *get_zeroed_page(gfp_t gfp_mask); /* 返回清零页面 */
/* 分配多个连续页面 */
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
void *get_free_pages(gfp_t gfp_mask, unsigned int order);
/* 分配示例 */
struct page *page = alloc_page(GFP_KERNEL); /* 1页 */
struct page *pages = alloc_pages(GFP_KERNEL, 2); /* 4页 */
void *addr = get_free_page(GFP_KERNEL); /* 1页虚拟地址 */
void *addr = get_free_pages(GFP_KERNEL, 3); /* 8页虚拟地址 */
7.2 页面释放API
scss
/* 释放页面 */
void __free_page(struct page *page);
void free_page(void *addr);
void __free_pages(struct page *page, unsigned int order);
void free_pages(void *addr, unsigned int order);
/* 释放示例 */
__free_page(page); /* 释放1页 */
__free_pages(pages, 2); /* 释放4页 */
free_page(addr); /* 释放1页(虚拟地址) */
free_pages(addr, 3); /* 释放8页(虚拟地址) */
分配 API 有两种返回形式:*alloc_page* 返回 struct page *,适合内核内部直接操作页
帧(如 pagecache、rmap);get_free_page* 返回内核虚拟地址,适合当作普通内存使用。
需记住几点:
GFP_KERNEL允许睡眠、可能触发回收甚至 OOM;GFP_ATOMIC绝不睡眠(用于中断与持锁
上下文)。选择错误的 GFP 标志是常见崩溃源。- 分配大页要给出
order,即要2^order页物理连续 的块;只有 buddy 空闲链上
存在连续块才能满足,压力下会触发更大块的分裂 (见 7.3 与
02_buddy_system.md)。 - 释放必须用与分配匹配的 order ;且只有当
_count归零时才会真正归还 buddy。
7.3 分配调用流程
sql
alloc_pages(gfp_mask, order) 调用流程:
+------------------------------------------------------------------+
| alloc_pages(gfp_mask, order) |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| alloc_pages_node(numa_node_id(), gfp_mask, order) |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| __alloc_pages_nodemask(gfp_mask, order, zonelist, nodemask) |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| get_page_from_freelist(gfp_mask, nodemask, order, zonelist, |
| alloc_flags, zone, migratetype) |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| buffered_rmqueue(zonelist, zone, order, gfp_flags, migratetype) |
+------------------------------------------------------------------+
|
+------------------+------------------+
| | |
v v v
+---------------+ +---------------+ +---------------+
| Per-CPU缓存 | | Buddy系统 | | 备用Zone |
| pcp->lists | | __rmqueue | | fallback |
+---------------+ +---------------+ +---------------+
真正的分配并非一次调用就落到 buddy 那么"重"。为摊薄锁开销,每个 CPU 先尝试自己的
per-CPU 页缓存 (pageset) ------命中即无锁返回;缓存空了才调用 __rmqueue() 去 buddy
的 free_area 里取,取空了再按水位挑备用 zone (fallback) 继续试。只有当所有 zone
都无页且允许阻塞时,才进入 __alloc_pages_slowpath 的回收 / OOM 慢路径(见
00_overview.md 11.1 节)。理解这条"缓存 → buddy → 回退 → 回收"的调用链路,
是读懂任何 alloc_pages 调用链的主干。
八、页面使用场景
8.1 不同场景下的page字段含义
sql
+------------------------------------------------------------------+
| 使用场景 | flags | mapping | private |
+------------------------------------------------------------------+
| 页缓存(文件) | PG_dirty | address_space| buffer_heads |
| | PG_uptodate | | |
| | PG_lru | | |
+------------------------------------------------------------------+
| 匿名页(进程私有) | PG_swapbacked| anon_vma | swap_entry |
| | PG_dirty | (带ANON标志)| (swapcache时) |
| | PG_lru | | |
+------------------------------------------------------------------+
| Slab页 | PG_slab | NULL | slab指针 |
| | | | 或freelist |
+------------------------------------------------------------------+
| 页表页 | | NULL | NULL |
+------------------------------------------------------------------+
| Buddy空闲页 | PG_buddy | NULL | order值 |
+------------------------------------------------------------------+
| 复合页首页 | PG_head | | destructor |
| 复合页尾页 | PG_tail | NULL | first_page指针 |
+------------------------------------------------------------------+
| Swap缓存页 | PG_swapcache | swapper_space| swap_entry |
+------------------------------------------------------------------+
同一份 struct page,在不同场景下各字段的"身份"完全不同------这正是第二章强调"先看
flags 再解释字段"的地方:
- 页缓存页 :
mapping指向文件的address_space,index是文件内偏移,private
常是buffer_head链,flags挂PG_dirty/PG_uptodate/PG_lru。 - 匿名页 :
mapping最低位被置 1 指向anon_vma;一旦被换出,PG_swapcache置位、
private存swp_entry_t(交换槽位)。 - slab 页 :
PG_slab置位,字段整体交给 slab 管理(freelist与inuse/objects)。 - buddy 空闲页 :
PG_buddy置位,private存 order,lru作空闲链表节点。
所以"看一张 struct page"一定要连同它的 flags 一起读------标志即身份。
九、页面生命周期
lua
页面生命周期图:
+------------------------------------------------------------------+
| 页面生命周期 |
+------------------------------------------------------------------+
+----------+
| 未初始化 | <-- 系统启动时,mem_map数组分配但未初始化
+----------+
|
| free_area_init_core()
v
+----------+
| 空闲页 | <-- 在Buddy系统中,PG_buddy标志
+----------+
|
| alloc_pages()
v
+----------+
| 已分配 | <-- _count = 1,根据用途设置不同字段
+----------+
|
+------------------+------------------+------------------+
| | | |
v v v v
+----------+ +----------+ +----------+ +----------+
| 页缓存页 | | 匿名页 | | Slab页 | | 页表页 |
+----------+ +----------+ +----------+ +----------+
| | | |
| | | |
v v v v
+----------+ +----------+ +----------+ +----------+
| 可能被回收| | 可能被交换| | 对象释放 | | 进程退出 |
| 或回写 | | 到swap | | | | |
+----------+ +----------+ +----------+ +----------+
| | | |
+------------------+------------------+------------------+
|
| free_pages() / __free_page()
v
+----------+
| 回到空闲 | <-- 重新加入Buddy系统
+----------+
页面的一生跨越多个内核子系统,每个阶段对 struct page 的字段有着不同的占用约束:
- 未初始化 :系统启动时
mem_map数组被分配(bootmem),但多数页还没归 buddy。 - 空闲 :
free_area_init_core()把页送进 buddy;此后_count==0、PG_buddy置位、
lru作空闲链表、private存 order------可被任何分配者取走。 - 已分配 :
alloc_pages置_count=1,按用途填mapping/private等,并在相应
LRU 或专用链表挂靠;到这一步"归谁所有"才真正确定。 - 使用/回收 :页缓存页可能被回写或回收,匿名页可能被换出到 swap 再换回;回收成功
则_count最终随引用释放归零。 - 回到空闲 :
free_pages/__free_page归还 buddy,字段被重置回"空闲"约定------一个页
的生命循环就此闭合。
任何在这条链上"获取引用"的代码都必须先 get_page(),防止它在自己手中被并发回收
(put_page 后计数归零,才表示可以真正释放)。这是理解 struct page 与其他子系统
相互关系的一条重要线索。更完整的阶段细节见 page_lifecycle.md。
9.1 字段 lru ------ 双向链表节点
struct list_head lru 是页在链表中的挂载点,在两种状态下被复用:
- 空闲页 (
page_count()==0,在 buddy):lru作为伙伴系统空闲链表 的节点,
page_alloc.c用list_add(&page->lru, ...)/list_del(&page->lru)进出空闲链。 - 已分配且可回收 (pagecache/匿名页):
lru作为 LRU 链表 的节点,挂在所属
zone 的active_list/inactive_list上,由mm/vmscan.c维护,受
zone->lru_lock保护。
与 PG_lru/PG_active 标志协同(mm/vmscan.c):
scss
int active = !!TestClearPageActive(page); /* 读并清除 PG_active */
...
if (PageLRU(page)) /* 必须在 LRU 上才能隔离/回收 */
...
list_add(&page->lru, &ret_pages); /* 加入回收结果链表 */
回收时 lru_to_page() 从链表节点反推出 struct page,与 page_to_pfn 正好反向:
用 container_of 宏从 lru 偏移找回页头。这是"链表节点内嵌进结构体"的经典用法。
9.2 场景示例:追踪一个文件页的完整字段生命史
沿"读文件 /etc/foo → 进程写 → 刷盘 → 回收"这条顺序,依次观察各字段如何协同:
scss
阶段1 首次读:alloc_page() 得到 page
_count = 1 (分配者引用)
mapping = &inode->i_mapping (文件 address_space)
index = 页在文件中的偏移
flags += PG_lru、PG_uptodate (进入 LRU,数据有效)
lru 挂入 zone->active_list
阶段2 进程写:SetPageDirty() 置 PG_dirty(页缓存脏)
阶段3 刷盘:clear_page_dirty_for_io() 清 PG_dirty
flags += PG_writeback
完成后 ClearPageWriteback()
阶段4 换页表映射(mmap/缺页):
rmap: mapping 指向 anon_vma 或 address_space(映射类型)
_mapcount += 1 (每多一个 PTE 映射 +1)
阶段5 回收(LRU 驱逐):
_mapcount 降到无映射 (>=0 -> -1 判断)
page_count 降到 0
若 PG_private:调 releasepage 释放 buffer_head
PG_buddy 置位,private = order,lru 转入伙伴空闲链
9.3 一致性规则小结(谁在维护哪个字段)
| 字段 | 主要维护者 | 受保护/同步方式 |
|---|---|---|
flags |
各子系统(lock、writeback、vmscan...) | 原子位操作(set_bit 等) |
_count |
页缓存 / 引用持有者 / buddy | atomic_inc/dec;空闲页在 buddy 链表上 |
_mapcount |
mm/rmap.c(映射/解除) | atomic_inc_and_test 等 |
mapping |
pagecache 建立 / anon_vma 挂接 | 初始化时设置,之后只读或受锁 |
private |
对应持有者(buffer/swap/buddy) | 由对应子系统锁保护 |
index |
pagecache | tree_lock / zone 锁 |
lru |
vmscan / buddy | zone->lru_lock(LRU);空闲链表随 buddy |
first_page |
复合页创建/销毁 | 生命周期内只写一次 |
易错点 :_count 与 _mapcount 意义完全不同------前者是"内存持有引用",后者是
"被几个页表映射"。判断能否直接回收用 page_mapped,判断内存是否仍被持有用
page_count。二者组合才能回答"这个页现在处于什么状态"。
以上各字段的意义最终都由对应的
PageX()/SetPageX()访问宏和统一入口(
page_mapping/compound_head/page_zone)对外暴露,不要直接裸读写
page->flags/page->private,否则会破坏内核对账(如 dirty/locked 计数)。