Linux 内核 struct page 超详细深度解析

struct page 是 Linux 内核内存管理子系统的绝对核心。系统中每一个物理内存页(Page Frame,通常大小为 4KB),在内存中都对应一个 struct page 结构体实例。

内核并不是直接通过物理地址操作内存,而是通过操作这些 struct page 对象来管理页的分配、引用、缓存、锁定、回收等全生命周期。


一、 核心定义与宏观视角

1. 基本定义

在经典的 Linux 内核版本(如 2.6 系列)中,这个结构体被定义为 mem_map_ttypedef struct page mem_map_t)。

在源码中,所有物理页的 struct page 实例组成一个全局数组 mem_map。这意味着:物理内存的第一页(物理地址 0x0)对应 mem_map[0],第二页对应 mem_map[1],以此类推。

2. 结构体的演变

随着内核版本迭代(从 2.6 到 5.x/6.x),这个结构体发生了大幅变化:

  • 精简:移除了 age 等冗余字段。

  • 位域优化:使用位操作更紧密地存储信息。

  • 联合体:通过 union 复用字段空间,减少内存占用。

  • 抽象增强:新增 _mapcount 等字段,更精细地管理页表引用。

但无论怎么变,核心逻辑不变


二、 逐字段 360° 深度解剖

1. struct list_head list;

  • 本质:这是一个通用双向链表节点

  • 作用:因为一个页在不同时期有不同身份(空闲页、被分配给进程的页、脏页等),它需要被加入不同的“队列”。

    • 伙伴系统 (Buddy System):空闲页挂入 free_area 链表,用于快速分配。

    • SLAB 分配器:内核小对象分配时,页挂入 slab 的内部链表。

  • 区别:它与下面的 lru 是独立的。list 是通用接口,lru 专用于回收。

2. struct address_space *mapping;

  • 本质页的“户口本”或“归属地”

  • 作用:指向该页所属的地址空间。这是 Linux 内存管理中最关键的指针之一,它直接决定了页的类型。

    • 文件页 (Page Cache)mapping 指向磁盘上某个文件的 address_space。表示这页缓存是文件的内容。

    • 匿名页 (Anonymous Page)mapping 指向 anon_vma(或特殊的交换空间)。表示这页是进程的堆、栈、数据,没有对应磁盘文件。

    • 内核页mapping 通常为 NULL

3. unsigned long index;

  • 本质页在地址空间中的位置索引

  • 作用:与 mapping 配合使用。

    • 文件页:表示该页在文件中的偏移量(以页为单位)。例如 index=5 代表文件的第 20KB 处(5 * 4KB)。

    • 匿名页:表示虚拟地址空间中的偏移量,用于缺页中断时找回数据。

4. struct page *next_hash; struct page **pprev_hash;

  • 本质哈希表指针

  • 作用:为了在海量页缓存中快速找到文件页,内核使用了哈希表。

    • 哈希表的 Key 是 (mapping, index)

    • next_hash:指向哈希冲突链中的下一个页。

    • pprev_hash:指向前一个节点的 next_hash 指针。这是一个二级指针,实现了 O(1) 时间复杂度的删除操作,非常高效。

5. atomic_t count; 核心中的核心

  • 本质引用计数器(原子操作)

  • 作用:记录当前有多少个内核组件/进程在引用这个页。它是决定页生死的关键。

  • 状态机

    • count > 0:页正在被使用,绝对不能被回收

    • count = 0:页空闲,无人使用,可以被分配给新任务或回收。

    • count < 0:内核 Bug,引用计数失衡。

  • 操作:通过 get_page() (原子加1) 和 put_page() (原子减1) 操作。

6. unsigned long flags;

  • 本质状态标志位

  • 作用:用一个整数的每一位(bit)来表示页的状态。支持原子操作。

  • 常见标志 (Flags)

    • PG_locked:页被锁定。正在进行 IO 操作,其他进程需等待。

    • PG_dirty:脏页。内容已修改,需要写回磁盘。

    • PG_uptodate:数据已就绪。文件页已从磁盘加载完成,可以使用。

    • PG_referenced / PG_active:LRU 回收标记。表示页最近被访问过,处于活跃状态。

7. struct list_head lru;

  • 本质LRU 链表节点

  • 作用:专用于内存回收 (Swap/Reclaim)

  • 原理:内核维护两个大链表: 页通过 lru 字段在这两个链表间移动。当内存不足时,优先回收 inactive_list 头部的页(最久未用)。

    1. active_list:活跃页列表,最近频繁使用。

    2. inactive_list:不活跃页列表,久未使用。

8. unsigned long age;

  • 本质:页的“年龄”。

  • 作用:在旧内核中,用于判断页的活跃程度。访问时年龄加一,定期扫描时年龄减一。

  • 现状:在现代内核(3.x+)中,这个字段已被移除。其功能完全整合到 flagsPG_referenced 位中。

9. wait_queue_head_t wait;

  • 本质等待队列头

  • 作用:配合 PG_locked 使用。

  • 场景:当一个进程想访问被锁定(IO 中)的页时,它不能忙等。进程会把自己加入到 page->wait 这个等待队列中睡眠。当 IO 完成,页解锁,内核唤醒这个队列中的进程继续执行。

10. struct buffer_head * buffers;

  • 本质块设备缓冲区关联

  • 作用:早期内核用于块设备(磁盘)读写。一页内存通常包含 8 个磁盘扇区。buffers 指向页内第一个 buffer_head,每个 buffer_head 描述一个磁盘块的状态。

  • 现状:现代内核多用 bio 结构来批量处理 IO,此字段逐渐过时,但仍保留用于兼容。

11. void *virtual;

  • 本质内核虚拟地址

  • 作用:对于高端内存(Highmem)页,内核无法直接通过物理地址访问,必须建立临时的内核虚拟地址映射。

    • 32 位系统:virtual 非空表示通过 kmap() 临时映射了。

    • 64 位系统:虚拟地址空间极大,所有物理内存都永久映射,此字段基本无用。

12. struct zone_struct *zone;

  • 本质内存域 (Zone) 指针

  • 作用:指向该页所属的内存管理区域。Linux 将物理内存划分为不同区域(Zone)以满足硬件限制。

    • ZONE_DMA:低地址内存(<16MB),用于老旧 DMA 设备。

    • ZONE_NORMAL:普通内存,内核直接映射。

    • ZONE_HIGHMEM:高端内存,仅用户态可用。

  • 价值:内存分配时必须指定从哪个 Zone 分配。


三、 核心逻辑与生命周期

1. 页的一生

  1. 启动/初始化:内核启动时,物理内存被扫描,所有页初始化,count=0,挂入 free_area 空闲链表。

  2. 分配 (Alloc):伙伴系统从 free_area 摘下一页。count 设为 1,zone 指向所属域。

  3. 映射 (Map)

    • 进程页:建立页表映射,mapping 指向 anon_vmacount 原子加 1。

    • 文件页:读入文件内容,mapping 指向文件 address_space,加入页缓存哈希表。

  4. 访问 (Access):CPU 访问页,触发 MMU,设置 PG_referenced 标志。

  5. 修改 (Modify):修改页内容,设置 PG_dirty 标志。

  6. IO 操作 (IO):页数据需要写入磁盘,设置 PG_locked,数据写入后清除标志并解锁。

  7. 回收 (Reclaim):内存不足时,扫描 LRU 链表。若 count 降至 0 或为不可驻留页,将其清空并重新放回 free_area

2. 关键机制总结

  • 字段精简位域压缩匿名页优化映射计数分离句柄

(注:文档部分内容可能由 AI 生成)