[内存管理] page数据结构

一、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_privateprivate 里有文件系统数据)、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_1PG_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_countpage_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_pagepage_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 pagevirtual 字段只在 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_spaceindex 是文件内偏移,private
    常是 buffer_head 链,flagsPG_dirty/PG_uptodate/PG_lru
  • 匿名页mapping 最低位被置 1 指向 anon_vma;一旦被换出,PG_swapcache 置位、
    privateswp_entry_t(交换槽位)。
  • slab 页PG_slab 置位,字段整体交给 slab 管理(freelistinuse/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 的字段有着不同的占用约束:

  1. 未初始化 :系统启动时 mem_map 数组被分配(bootmem),但多数页还没归 buddy。
  2. 空闲free_area_init_core() 把页送进 buddy;此后 _count==0PG_buddy 置位、
    lru 作空闲链表、private 存 order------可被任何分配者取走。
  3. 已分配alloc_pages_count=1,按用途填 mapping/private 等,并在相应
    LRU 或专用链表挂靠;到这一步"归谁所有"才真正确定。
  4. 使用/回收 :页缓存页可能被回写或回收,匿名页可能被换出到 swap 再换回;回收成功
    _count 最终随引用释放归零。
  5. 回到空闲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.clist_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 计数)。

相关推荐
Awh-1 小时前
ARM 裸机开发 day02 学习笔记
linux·arm开发·arm
牢姐与蒯2 小时前
Linux进程间通信(一).进程间通信概念&&环境切换(着重强调vscode)
linux·运维·服务器·ubuntu
M78佐菲2 小时前
ARM学习笔记(四)
linux·arm开发·笔记·嵌入式硬件·学习
东鹏特饮2 小时前
redhat9 配置yum 阿里云
linux·运维·redhat·yum
昌原的儿子LEO2 小时前
i.MX6ULL 裸机开发 C 语言驱动与 BSP 工程管理学习总结
linux·arm开发·学习
lqg_zone2 小时前
CentOS 7 虚拟机磁盘 I/O 卡顿排查实录:从 iostat 异常到虚拟盘后端故障的完整定位
linux·服务器·mysql·centos
kkkkkkkkkk_Z2 小时前
单片机|学习日记:单片机底层架构与寄存器操作全解析
linux·笔记·单片机·学习·51单片机
xiaoye-duck2 小时前
《Linux 网络编程》深入理解 IP 协议(一):网络层基础与 IP 协议头详解
linux·网络·ip
ao-weilai2 小时前
Linux网络编程:Linux Socket TCP
linux·服务器·网络