Linux 适用于多种体系结构,需用体系结构无关方式描述内存。本章介绍影响 VM 行为的内存簇、页面和标志位结构。
- 非一致内存访问(NUMA):在 VM 中,大型机器内存分簇,依簇与处理器距离,访问代价不同。可将内存簇指派给处理器或设备。每个簇是一个节点,Linux 中
struct pg_data_t
体现此概念,用于一致内存访问(UMA)体系结构,系统节点链接成pgdat_list
链表,节点通过pg_data_tnode_next
字段连接。像 PC 采用 UMA 结构的机器,使用contig_page_data
的静态pg_data_t
结构。 - 管理区(zone)
-
- 内存中节点分成管理区,由
struct zone_struct
描述,类型为zone_t
,包括ZONE_DMA
、ZONE_NORMAL
、ZONE_HIGHMEM
。不同管理区适用于不同用途。 ZONE_DMA
指低端物理内存,供某些 ISA 设备使用,x86 机器中存放前 16MB 内存。ZONE_NORMAL
是内核直接映射到线性地址空间的较高部分,x86 机器范围是 16MB - 896MB ,许多内核操作依赖此区,对系统性能影响重大。ZONE_HIGHMEM
是系统预留的可用内存空间,不被内核直接映射,x86 机器从 896MB 到末尾 。
- 内存中节点分成管理区,由
- 页面帧:系统内存划分成页面帧,由
struct page
描述,结构存储在全局mem_map
数组,通常在ZONE_NORMAL
首部或小内存系统预留区域。后续章节将讨论其细节及内存结构间基本关系。
2.2 管理区
在Linux系统中,每个管理区由struct zone_t
描述,zone_structs
用于跟踪页面使用情况统计数、空闲区域信息和锁信息等 ,在<linux/mmzone.h>
中声明如下:
typedef struct zone_struct {spinlock_t lock;unsigned long free_pages;unsigned long pages_min, pages_low, pages_high;int need_balance;free_area_t free_area[MAX_ORDER];wait_queue_head_t * wait_table;unsigned long wait_table_size;unsigned long wait_table_shift;struct pglist_data *zone_pgdat;struct page *zone_mem_map;unsigned long zone_start_paddr;unsigned long zone_start_mapnr;char *name;unsigned long size;
} zone_t;
- lock:并行访问时保护该管理区的自旋锁。
- free_pages:管理区中空闲页面的总数。
- pages_min, pages_low, pages_high:管理区极值,用于跟踪管理区压力情况 。
- need_balance:标志位,通知页面换出kswapd平衡管理区,当可用页面数量达到管理区极值某值时,需平衡管理区。
- free_area:空闲区域位图,由伙伴分配器使用。
- wait_table:等待队列的哈希表,由等待页面释放的进程组成,对
wait_on_page()
和unlock_page()
重要 。 - wait_table_size:哈希表大小,是2的幂。
- wait_table_shift:定义为一个
long
型对应的位数减去该表大小的二进制对数。 - zone_pgdat:指向父
pg_data_t
。 - zone_mem_map:涉及的管理区在全局
mem_map
中的第一页。 - zone_start_paddr:同
node_start_paddr
。 - zone_start_mapnr:同
node_start_mapnr
。 - name:管理区字符串名字,如“DMA”“Normal”或“HighMem” 。
- size:管理区大小,以页面数计算。
以下是关于Linux内核mmzone.h
头文件的内存管理架构解析(整理为中文):
一、Linux内存管理与mmzone.h概述
Linux内核将物理内存划分为节点(Node,适用于NUMA系统)和区域(Zone,基于内存类型)。
mmzone.h
定义了管理这些内存区域的核心数据结构和常量,尤其是分区伙伴分配器(zoned buddy allocator)——内核用于分配物理内存页的主要机制。
核心架构概念:
- 内存节点(Memory Nodes)
在NUMA系统中,物理内存按节点划分,每个节点与一个或一组处理器关联,通过pg_data_t
结构管理。 - 内存区域(Memory Zones)
每个节点内的内存进一步按用途划分为区域(如DMA、Normal、Highmem),用于处理硬件限制(如DMA设备的地址范围限制)或性能特性。 - 伙伴分配器(Buddy Allocator)
内核通过伙伴系统管理每个区域的空闲页,将页按2的幂次块(Order)分组,减少内存碎片。 - NUMA感知(NUMA Awareness)
内核优化内存分配,优先使用本地节点内存以提升性能。 - 页面回收(Page Reclamation)
区域跟踪活跃(Active)和非活跃(Inactive)页,用于内存不足时的回收(如交换或释放页)。
mmzone.h
是这些机制的核心,定义了内存的组织、分配和回收逻辑。
二、mmzone.h中的关键数据结构
a. struct free_area
struct free_area {struct list_head free_list; // 同大小空闲块链表(按Order分组)unsigned long nr_free; // 该大小的空闲块数量
};
- 作用:表示伙伴分配器中特定大小(Order)的空闲内存块列表。
- 架构角色:每个区域维护一个
free_area
数组(每个Order对应一项),伙伴系统通过分裂/合并块来管理内存,减少外部碎片。 - 面试要点:解释伙伴系统如何通过分裂(如将4页块拆分为2个2页块)和合并块满足分配请求。
b. struct per_cpu_pages
struct per_cpu_pages {int count; // 缓存中的页数int low, high; // 触发缓存填充/清空的水位线int batch; // 单次操作添加/移除的页数struct list_head list; // 页链表
};
- 作用:管理 per-CPU 页缓存,减少分配时对全局区域锁的竞争。
- 架构角色:CPU可直接从本地缓存分配/释放页,避免锁竞争。热缓存(Hot)和冷缓存(Cold)区分近期使用页,优化缓存局部性。
- 面试要点:讨论per-CPU缓存如何提升SMP系统性能,以及缓存空间与锁竞争的权衡。
c. struct per_cpu_pageset
struct per_cpu_pageset {struct per_cpu_pages pcp[2]; // 0: 热页,1: 冷页
#ifdef CONFIG_NUMAunsigned long numa_hit; // 在目标节点分配成功数unsigned long numa_miss; // 在非目标节点分配数// 其他NUMA统计字段
#endif
} ____cacheline_aligned_in_smp;
- 作用:扩展per-CPU页缓存,增加NUMA统计和热/冷页分离。
- 架构角色:通过NUMA统计(如
numa_hit
)优化节点分配策略,缓存行对齐(____cacheline_aligned_in_smp
)减少SMP系统中的伪共享(False Sharing)。 - 面试要点:解释NUMA对内存分配的影响,内核如何优先本地节点分配以降低延迟。
d. struct zone
struct zone {unsigned long free_pages; // 区域空闲页数struct free_area free_area[]; // 伙伴分配器的空闲块数组spinlock_t lock; // 保护伙伴分配器的自旋锁struct per_cpu_pageset pageset[]; // per-CPU页缓存struct list_head active_list, inactive_list; // LRU链表// 其他字段(回收统计、水位线、节点关联等)
} ____cacheline_maxaligned_in_smp;
- 作用:表示节点内的一个内存区域(如DMA、Normal、Highmem)。
- 核心功能:
-
- 分配:通过
free_area
和lock
管理伙伴系统。 - 回收:通过LRU链表(
active_list
/inactive_list
)和kswapd
线程实现页面回收。 - 元数据:
zone_start_pfn
(区域起始页帧号)、lowmem_reserve
(保留低内存防止耗尽)。
- 分配:通过
- 架构角色:区域是节点内存管理的核心,平衡分配效率与硬件约束(如DMA的地址限制)。
- 面试要点:说明区域如何处理硬件限制(如Highmem在32位系统的作用),以及
lowmem_reserve
如何保障关键区域内存。
e. struct zonelist
struct zonelist {struct zone *zones[MAX_NUMNODES * MAX_NR_ZONES + 1]; // 按优先级排序的区域列表(NULL结尾)
};
- 作用:定义内存分配时的区域优先级列表(如优先本地节点,再 fallback 其他节点)。
- 架构角色:结合GFP标志(如
__GFP_DMA
)实现NUMA感知分配和故障转移策略。 - 面试要点:解释
zonelist
如何支持NUMA分配,以及不同分配标志(如DMA/Highmem)如何影响区域选择。
f. struct pglist_data (pg_data_t)
typedef struct pglist_data {struct zone node_zones[]; // 节点内的所有区域(如DMA、Normal)struct zonelist node_zonelists[]; // 分配时的区域优先级列表struct page *node_mem_map; // 节点页结构数组struct task_struct *kswapd; // 节点的页面回收线程// 其他字段(节点ID、内存范围、启动内存数据等)
} pg_data_t;
- 作用:表示NUMA系统中的一个内存节点,封装节点内的所有内存管理逻辑。
- 架构角色:链接区域(
node_zones
)和分配策略(node_zonelists
),通过kswapd
线程实现节点级内存回收。 - 面试要点:说明
pg_data_t
如何支持NUMA,以及节点链表(pgdat_next
)如何遍历所有节点。
三、关键常量与宏
MAX_ORDER
-
- 定义伙伴分配器的最大块大小(默认11,即2^11页,约8MB/4KB页),可通过
CONFIG_FORCE_MAX_ZONEORDER
配置。 - 面试要点:讨论增大
MAX_ORDER
的权衡(支持大分配 vs. 增加碎片风险)。
- 定义伙伴分配器的最大块大小(默认11,即2^11页,约8MB/4KB页),可通过
- 区域类型(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM)
-
- DMA:适用于ISA设备(<16MB,x86系统)。
- Normal:内核直接映射的内存(16MB~896MB,x86 32位)。
- Highmem:内核无法永久映射的高端内存(>896MB,仅32位系统需要)。
- 面试要点:解释Highmem存在的原因(32位内核地址空间限制)及管理方式(动态映射如
kmap
)。
GFP_ZONEMASK
与GFP_ZONETYPES
-
GFP_ZONEMASK
:分配标志中指定目标区域的位掩码(如0x03)。GFP_ZONETYPES
:计算所需zonelist
数量,优化内存使用。- 面试要点:说明GFP标志如何影响区域选择(如
__GFP_HIGHMEM
允许从Highmem分配)。
四、架构设计原则
- 模块化与抽象
-
- 通过
zone
和pg_data_t
抽象节点和区域,支持跨架构(NUMA/UMA、32/64位)移植。 zonelist
解耦分配逻辑与硬件细节。
- 通过
- 性能优化
-
- Per-CPU缓存:减少锁竞争,提升SMP系统吞吐量。
- 缓存行对齐:通过
____cacheline_aligned_in_smp
避免伪共享。 - NUMA感知:优先本地节点分配,降低内存访问延迟。
- 可扩展性
-
- 伙伴分配器的对数复杂度(与
MAX_ORDER
相关)支持大内存系统。 - NUMA支持扩展至多节点架构(
MAX_NODES_SHIFT
)。
- 伙伴分配器的对数复杂度(与
- 可靠性
-
lowmem_reserve
保障关键区域(如DMA)内存,避免饥饿。- LRU链表和
kswapd
线程防止OOM(内存不足)。
- 灵活性
-
- 通过宏(如
CONFIG_DISCONTIGMEM
、CONFIG_NUMA
)适配不同内存架构。 wait_table
支持进程等待页可用,缓解分配竞争。
- 通过宏(如
五、面试常见问题与解答
- 区域(Zone)在内存管理中的作用?
-
- 答:区域按硬件约束划分内存(如DMA的地址限制、Highmem的内核映射限制),每个区域独立管理分配(伙伴系统)和回收(LRU链表),提升硬件兼容性和分配效率。
- 伙伴分配器如何工作?
MAX_ORDER
的意义?
-
- 答:伙伴系统将空闲页按2的幂次块分组(Order 0~MAX_ORDER-1),分配时分裂大块,释放时合并相邻块以减少碎片。
MAX_ORDER
决定最大块大小,平衡大分配支持与碎片风险。
- 答:伙伴系统将空闲页按2的幂次块分组(Order 0~MAX_ORDER-1),分配时分裂大块,释放时合并相邻块以减少碎片。
- Linux如何处理NUMA系统的内存分配?
-
- 答:通过
pg_data_t
管理节点,zonelist
优先本地节点区域分配,利用numa_hit
/numa_miss
统计优化策略,kswapd
线程按节点独立回收内存,降低跨节点访问延迟。
- 答:通过
lowmem_reserve
的作用?
-
- 答:在低内存区域(如DMA、Normal)预留内存,防止高内存区域(如Highmem)的分配耗尽关键区域内存,保障硬件设备(如DMA设备)的正常运行。
- 内核如何减少内存分配的锁竞争?
-
- 答:使用per-CPU页缓存(
per_cpu_pageset
)避免全局锁,缓存行对齐减少伪共享,自旋锁(如lock
、lru_lock
)保护共享结构,缩小锁持有时长。
- 答:使用per-CPU页缓存(
六、深入理解要点
- 缓存行对齐:
____cacheline_maxaligned_in_smp
确保关键结构(如自旋锁)独占缓存行,避免多CPU访问不同变量时的缓存一致性开销。 - Highmem挑战:32位系统中,Highmem页需动态映射(如
kmap
),增加管理复杂度,而64位系统因地址空间充足可直接映射。 - 回收策略:LRU链表通过
prev_priority
调整扫描优先级,内存压力大时加强回收,避免OOM。
七、面试准备建议
- 阅读关联代码:结合
mm/page_alloc.c
(分配逻辑)和mm/vmscan.c
(回收逻辑)理解mmzone.h
结构的实际应用。 - 掌握GFP标志:理解
__GFP_DMA
、__GFP_HIGHMEM
等标志如何与区域和zonelist
交互。 - 图示辅助解释:用示意图描述节点-区域-
zonelist
的层级关系,或伙伴系统的块分裂过程。 - 硬件约束分析:明确DMA、Highmem等区域存在的硬件背景(如ISA设备限制、32位地址空间限制)。
八、总结
mmzone.h
是Linux内存管理的核心,通过节点、区域、伙伴系统和NUMA感知设计,实现了跨架构的内存分配与回收。面试中需重点掌握:
- 数据结构角色:
zone
、pg_data_t
、zonelist
的职责。 - 性能优化机制:per-CPU缓存、伙伴系统、NUMA感知分配。
- 硬件与架构适配:区域如何处理DMA/Highmem等限制,32位与64位系统的差异。
思考:linux 内核如何感知硬件是否是 NUMA 架构?
思考:mmzone.h 中和伙伴系统相关的数据结构和函数是什么?
思考: per_cpu_pages 是什么意思?有什么用?为什么需要 per_cpu_pages?
以下是对你的三个问题的详细解答,结合 mmzone.h
头文件,从 Linux 内核内存管理的架构视角进行分析,旨在帮助你深入理解并为面试做好准备。答案将使用中文,并尽量清晰、简洁,同时涵盖技术细节。
问题 1:Linux 内核如何感知硬件是否是 NUMA 架构?
Linux 内核通过以下机制感知硬件是否是 NUMA(Non-Uniform Memory Access)架构:
- 硬件初始化阶段的探测:
-
- ACPI(高级配置与电源接口):现代系统通常通过 ACPI 表(如 SRAT 和 SLIT)获取 NUMA 拓扑信息。SRAT(System Resource Affinity Table)描述了内存和处理器的亲和性,列出每个 NUMA 节点的内存范围和 CPU 关联;SLIT(System Locality Information Table)提供节点间访问延迟信息。
- BIOS 或固件:在系统启动时,BIOS 或 UEFI 固件会通过 ACPI 或其他机制向内核传递硬件拓扑信息。内核通过解析这些表来识别 NUMA 节点及其内存分布。
- 设备树(Device Tree):在嵌入式系统或非 ACPI 系统(如某些 ARM 架构)上,设备树描述硬件拓扑,包括 NUMA 节点的内存和 CPU 分布。
- 内核初始化代码:
-
- 内核在启动过程中会调用与 NUMA 相关的初始化函数(如
mm/numa.c
中的numa_init
),根据 ACPI 或设备树信息构建pglist_data
结构(对应每个 NUMA 节点)。 - 如果系统检测到多个 NUMA 节点(
MAX_NUMNODES > 1
),内核会启用 NUMA 支持,初始化每个节点的pg_data_t
结构,并填充node_zones
和node_zonelists
。 - 如果只有一个节点(UMA 系统),内核会退化为单一节点模式,
pglist_data
只包含一个实例(如contig_page_data
)。
- 内核在启动过程中会调用与 NUMA 相关的初始化函数(如
- 宏和配置选项:
-
CONFIG_NUMA
:内核编译时通过配置选项启用 NUMA 支持。如果未启用,内核假设 UMA(Uniform Memory Access)架构。numa_node_id()
:返回当前 CPU 所属的 NUMA 节点 ID,基于硬件探测结果(如cpu_to_node
映射)。MAX_NUMNODES
和MAX_NODES_SHIFT
:定义支持的最大节点数,限制 NUMA 系统的规模(32 位系统最多 64 节点,64 位系统最多 1024 节点)。
- 运行时验证:
-
- 内核通过
node_present_pages(nid)
和node_spanned_pages(nid)
检查每个节点的内存是否存在和大小,确认 NUMA 拓扑。 - 运行时统计(如
numa_hit
和numa_miss
)帮助内核监控 NUMA 分配效率,进一步验证拓扑的有效性。
- 内核通过
总结:
Linux 内核通过 ACPI 表、设备树或固件信息在启动时探测 NUMA 拓扑,初始化 pglist_data
结构来管理每个节点。如果只有一个节点或未启用 CONFIG_NUMA
,内核按 UMA 模式运行。面试时,可以强调 ACPI 的 SRAT/SLIT 表和 numa_init
函数的角色,以及 NUMA 对性能优化的重要性。
🌍 思考:mmzone.h 中和伙伴系统相关的数据结构和函数是什么?
伙伴系统(Buddy Allocator)是 Linux 内核用于管理物理内存页的主要分配器,mmzone.h
定义了与其相关的核心数据结构和少量函数。
相关数据结构
struct free_area:
struct free_area {struct list_head free_list;unsigned long nr_free;
};
- 作用:表示特定大小(order)的空闲页面块列表,用于伙伴系统的内存管理。
- 字段:
-
free_list
:一个双向链表,存储同一 order(2^order 页)的空闲页面块。nr_free
:记录该 order 下空闲块的数量。
- 在伙伴系统中的角色:
-
- 每个
zone
包含一个free_area
数组(free_area[MAX_ORDER]
),从 order 0(1 页)到 orderMAX_ORDER-1
(默认 2^10 页,即 4MB)。 - 伙伴系统通过
free_area
管理空闲页面,分配时从合适 order 的free_list
获取,释放时合并相邻块(伙伴)到更高 order。
- 每个
struct zone(部分字段):
struct zone {unsigned long free_pages;spinlock_t lock;struct free_area free_area[MAX_ORDER];...
};
- 相关字段:
-
free_pages
:跟踪区域内总的空闲页面数。lock
:自旋锁,保护free_area
和free_pages
的并发访问。free_area
:伙伴系统的核心,存储不同 order 的空闲页面列表。
- 在伙伴系统中的角色:
-
zone
是伙伴系统的工作单元,每个区域独立管理自己的空闲页面。- 分配请求通过
zone
的free_area
查找合适大小的块,必要时分裂大块。 lock
确保多核系统下的线程安全。
struct per_cpu_pages(间接相关):
struct per_cpu_pages {int count;int low;int high;int batch;struct list_head list;
};
- 作用:虽然主要是 per-CPU 缓存,伙伴系统通过它快速分配/释放页面。
- 在伙伴系统中的角色:
-
- 当 per-CPU 缓存不足(
count < low
),从free_area
批量获取页面(batch
控制批量大小)。 - 当缓存过满(
count > high
),将页面批量归还到free_area
。(这段代码在哪里?)
- 当 per-CPU 缓存不足(
相关函数
mmzone.h
本身主要是数据结构定义,伙伴系统的核心逻辑在 mm/page_alloc.c
中实现,但 mmzone.h
中定义了以下与伙伴系统相关的函数:
- zone_watermark_ok:
int zone_watermark_ok(struct zone *z, int order, unsigned long mark,int alloc_type, int can_try_harder, int gfp_high);
-
- 作用:检查区域是否满足分配请求的内存水位要求。
- 参数:
-
-
z
:目标区域。order
:请求的页面块大小(2^order 页)。mark
:水位标记(如pages_low
、pages_high
)。alloc_type
:分配类型(如 GFP 标志)。can_try_harder
:是否允许更激进的分配尝试。gfp_high
:是否为高优先级分配。
-
-
- 在伙伴系统中的角色:
-
-
- 在分配页面之前,检查区域是否有足够的空闲页面(
free_pages
)和满足水位条件,防止过度分配导致内存耗尽。 - 如果水位不足,可能触发
kswapd
回收页面。
- 在分配页面之前,检查区域是否有足够的空闲页面(
-
- wakeup_kswapd:
void wakeup_kswapd(struct zone *zone, int order);
-
- 作用:唤醒
kswapd
线程以回收页面,增加区域的空闲页面。 - 在伙伴系统中的角色:
- 作用:唤醒
-
-
- 当伙伴系统无法满足分配请求(例如,
free_area[order]
为空),触发kswapd
回收页面,补充free_area
的空闲块。
- 当伙伴系统无法满足分配请求(例如,
-
数据结构:free_area
是伙伴系统的核心,管理空闲页面块;zone
提供分配环境;per_cpu_pages
优化分配性能。
函数:zone_watermark_ok
确保分配安全,wakeup_kswapd
支持内存回收以补充空闲页面。
面试建议:准备解释伙伴系统的分裂与合并机制,如何通过 free_area
查找页面,以及水位检查如何防止内存耗尽。
🌍 思考:per_cpu_pages 是什么意思?有什么用?为什么需要 per_cpu_pages?
per_cpu_pages 是什么?
per_cpu_pages
是一个数据结构,定义在 mmzone.h
中,用于实现每个 CPU 的页面缓存:
struct per_cpu_pages {int count; /* 缓存中的页面数量 */int low; /* 低水位,触发补充 */int high; /* 高水位,触发归还 */int batch; /* 批量补充/归还的页面数量 */struct list_head list; /* 缓存的页面链表 */
};
结构说明:
- 每个 CPU 核有一个
per_cpu_pages
实例,嵌入在per_cpu_pageset
中(pcp[0]
为热页面,pcp[1]
为冷页面)。 list
存储缓存的页面,count
跟踪数量。low
和high
是水位标记,控制缓存的填充和清空。batch
决定从伙伴系统批量获取或归还的页面数。
per_cpu_pages
的主要作用是:
提高分配效率:
- 通过为每个 CPU 维护一个本地页面缓存,内核可以在不获取全局
zone->lock
的情况下快速分配/释放页面。 - 热页面(
pcp[0]
)优先分配以优化缓存局部性(cache locality),提高性能。
减少锁竞争:
- 在多核(SMP)系统中,多个 CPU 同时访问
zone->lock
会导致严重的锁竞争,降低分配性能。 per_cpu_pages
允许 CPU 在本地缓存中操作,减少对全局锁的依赖。
NUMA 优化:
- 在 NUMA 系统下,
per_cpu_pages
与per_cpu_pageset
的 NUMA 统计字段(如numa_hit
)结合,优先从本地节点分配页面,降低跨节点访问的延迟。
批量操作:
- 通过
batch
参数,内核可以批量从伙伴系统获取或归还页面,减少频繁的锁操作和伙伴系统交互。
为什么需要 per_cpu_pages?
多核系统中的锁竞争:
-
- 在 SMP 系统中,多个 CPU 可能同时请求页面,频繁获取
zone->lock
会导致性能瓶颈。 per_cpu_pages
提供本地缓存,CPU 大多数时候可以直接从缓存分配页面,避免锁竞争。
- 在 SMP 系统中,多个 CPU 可能同时请求页面,频繁获取
性能优化:
-
- 内存分配是内核的热点路径(hot path),尤其在高负载场景(如服务器或虚拟化)。
- 本地缓存减少了访问全局数据结构的开销,热页面优先分配利用 CPU 缓存,提高效率。
NUMA 局部性:
-
- NUMA 系统要求尽量从本地节点分配内存以降低延迟。
per_cpu_pages
配合per_cpu_pageset
的 NUMA 统计,确保分配优先考虑本地页面。
- NUMA 系统要求尽量从本地节点分配内存以降低延迟。
动态内存管理:
-
- 水位机制(
low
和high
)动态调整缓存大小,避免缓存过多占用内存或过少导致频繁访问伙伴系统。 - 批量操作(
batch
)平衡了分配效率和内存使用率。
- 水位机制(
分配流程:
当 CPU 需要分配页面时,先检查 per_cpu_pages
的 list
。如果缓存中有页面(count > 0
),直接从 list
分配。
如果缓存不足(count < low
),从 zone->free_area
批量获取 batch
个页面,需获取 zone->lock
。
释放流程:
-
- 释放页面时,优先放入
per_cpu_pages
的list
。 - 如果缓存过满(
count > high
),批量归还页面到zone->free_area
,可能触发伙伴合并。
- 释放页面时,优先放入
NUMA 支持:
per_cpu_pageset
中的 NUMA 统计字段(如numa_hit
、numa_miss
)跟踪分配是否来自目标节点,帮助优化 NUMA 策略。
🌍 思考:struct page 和 Zone 是如何联系到一起的?
mm/page_alloc.c
文件
这段代码是 Linux 内核内存管理子系统中与页面分配和回收相关的核心实现,位于 mm/page_alloc.c
文件中,结合 mmzone.h
头文件,实现了 zoned buddy allocator(分区伙伴分配器),这是 Linux 内核管理物理内存页的主要机制。以下从顶层设计、设计思想、页面分配与回收的流程以及相关函数的角度,详细解答你的问题,力求清晰、全面,并帮助你为面试准备。
1. 顶层设计
Linux 内核的内存管理基于 节点(Node) 和 分区(Zone) 的分层架构,使用 伙伴系统(Buddy Allocator) 进行页面分配和回收,结合 per-CPU 页面缓存 和 NUMA 优化 来提升性能。以下是顶层设计的要点:
- 内存分层:
-
- 节点(Node):在 NUMA 架构中,物理内存被划分为节点(
struct pglist_data
),每个节点对应一个 CPU 或 CPU 组及其本地内存。 - 分区(Zone):每个节点内进一步划分为分区(如 ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM),以适应不同的硬件约束(如 DMA 的 <16MB 限制)。
- 页面(Page):内存的最小分配单位是页面(通常 4KB),通过
struct page
表示。
- 节点(Node):在 NUMA 架构中,物理内存被划分为节点(
- 伙伴系统:
-
- 伙伴系统是核心分配机制,将内存组织为 2order 大小的块(order 从 0 到
MAX_ORDER-1
,默认最大 210 页,即 4MB)。 - 每个分区(
struct zone
)维护一个free_area
数组,存储不同 order 的空闲页面块链表。
- 伙伴系统是核心分配机制,将内存组织为 2order 大小的块(order 从 0 到
- Per-CPU 缓存:
-
- 每个 CPU 维护一个
per_cpu_pageset
结构,包含热(hot)和冷(cold)页面缓存,用于快速分配和释放页面,减少对全局zone->lock
的竞争。
- 每个 CPU 维护一个
- NUMA 优化:
-
- 使用
zonelist
优先从本地节点分配内存,fallback 到其他节点,优化 NUMA 系统中的内存访问延迟。 - 统计字段(如
numa_hit
、numa_miss
)跟踪分配效率。
- 使用
- 页面回收:
-
- 当内存不足时,
kswapd
线程或直接回收机制(try_to_free_pages
)通过 LRU(Least Recently Used)列表回收页面,补充空闲页面。 - 水位线(
pages_min
、pages_low
、pages_high
)控制分配和回收的触发。
- 当内存不足时,
- 初始化:
-
- 内核启动时通过
free_area_init
和build_all_zonelists
初始化节点、分区和 zonelist,确保内存管理子系统就绪。
- 内核启动时通过
顶层设计图示:
[Node 0 (pg_data_t)]├── Zone DMA: free_area[0..MAX_ORDER-1], pageset[CPU0..CPUn], LRU lists├── Zone Normal: free_area[0..MAX_ORDER-1], pageset[CPU0..CPUn], LRU lists├── Zone Highmem: free_area[0..MAX_ORDER-1], pageset[CPU0..CPUn], LRU lists└── Zonelist: [Zone Normal (Node 0), Zone DMA (Node 0), Zone Normal (Node 1), ...]
[Node 1 (pg_data_t)]└── ...
2. 设计思想
Linux 内核页面分配与回收的设计思想围绕 性能、可扩展性、可靠性 和 灵活性 展开:
- 性能优化:
-
- Per-CPU 缓存:通过
per_cpu_pages
减少锁竞争,快速分配/释放页面。 - NUMA 局部性:优先分配本地节点内存,降低跨节点访问延迟。
- Cacheline 对齐:
zone
和per_cpu_pageset
使用 cacheline 对齐,减少 SMP 系统中的伪共享。 - 批量操作:批量分配/释放页面(如
rmqueue_bulk
、free_pages_bulk
)减少锁操作。
- Per-CPU 缓存:通过
- 可扩展性:
-
- 分层架构:节点和分区设计支持 NUMA 和 UMA 系统,适应不同硬件规模。
- 伙伴系统:通过 power-of-2 块管理内存,适合大内存系统,复杂度为 O(log n)。
- 模块化宏:通过
CONFIG_NUMA
和CONFIG_DISCONTIGMEM
等宏支持不同架构。
- 可靠性:
-
- 水位线管理:
pages_min
、pages_low
、pages_high
确保内存分配不会耗尽关键区域(如 ZONE_DMA)。 - 低内存保护:
lowmem_reserve
防止低端内存被高阶分配耗尽。 - 错误检查:
bad_page
和bad_range
检测页面状态异常,增强健壮性。
- 水位线管理:
- 灵活性:
-
- GFP 标志:通过
gfp_mask
(如__GFP_DMA
、__GFP_HIGHMEM
)支持不同分配需求。 - 可配置参数:
sysctl_lowmem_reserve_ratio
和min_free_kbytes
允许动态调整内存策略。 - 支持大页面:通过
CONFIG_HUGETLB_PAGE
支持复合页面(compound pages)。
- GFP 标志:通过
- 内存回收:
-
- 使用 LRU 列表(
active_list
、inactive_list
)和kswapd
线程动态回收页面,平衡内存压力。 - 优先级机制(
prev_priority
、temp_priority
)根据内存压力调整回收强度。
- 使用 LRU 列表(
3. 页面分配的流程和函数
页面分配的核心函数是 __alloc_pages
,它协调伙伴系统、per-CPU 缓存和 zonelist 来满足分配请求。以下是流程和关键函数:
分配流程
- 入口:
__alloc_pages(gfp_mask, order, zonelist)
:
-
- 根据
gfp_mask
(分配标志,如__GFP_DMA
)和order
(请求的页面块大小,2^order 页)选择合适的 zonelist。 - 遍历 zonelist 中的分区,检查水位线(
zone_watermark_ok
)以确保有足够空闲页面。
- 根据
- 尝试分配:
-
- 调用
buffered_rmqueue(zone, order, gfp_mask)
从目标分区分配页面:
- 调用
-
-
- order == 0:优先从 per-CPU 缓存(
per_cpu_pages
)分配单页。 - order > 0:直接从
zone->free_area
的伙伴系统分配。
- order == 0:优先从 per-CPU 缓存(
-
-
- 如果 per-CPU 缓存不足,调用
rmqueue_bulk
从伙伴系统补充页面。
- 如果 per-CPU 缓存不足,调用
- 水位检查:
-
zone_watermark_ok
检查分区空闲页面是否满足pages_low
或pages_min
(根据gfp_mask
和can_try_harder
调整)。- 如果水位不足,唤醒
kswapd
(wakeup_kswapd
)回收页面。
- 回收和重试:
-
- 如果分配失败,调用
try_to_free_pages
进行同步回收,释放页面到free_area
。 - 如果仍失败,可能触发 OOM(Out of Memory)杀进程(
out_of_memory
)或返回 NULL。
- 如果分配失败,调用
- 页面准备:
-
- 分配成功后,调用
prep_new_page
初始化页面(清除标志、设置引用计数)。 - 如果需要(
__GFP_ZERO
或__GFP_COMP
),调用prep_zero_page
清零页面或prep_compound_page
设置复合页面。
- 分配成功后,调用
关键分配函数
__alloc_pages(gfp_mask, order, zonelist)
:
-
- 核心分配函数,遍历 zonelist,调用
buffered_rmqueue
分配页面,处理水位检查、回收和 NUMA 统计。
- 核心分配函数,遍历 zonelist,调用
buffered_rmqueue(zone, order, gfp_mask)
:
-
- 从 per-CPU 缓存(order == 0)或伙伴系统(order > 0)分配页面,处理页面初始化。
__rmqueue(zone, order)
:
-
- 从伙伴系统的
free_area
分配页面,必要时分裂大块(通过expand
)。
- 从伙伴系统的
rmqueue_bulk(zone, order, count, list)
:
-
- 批量分配页面到指定链表,用于补充 per-CPU 缓存。
prep_new_page(page, order)
:
-
- 初始化新分配的页面,清除标志,设置引用计数。
prep_compound_page(page, order)
:
-
- 为大页面(compound pages)设置标志和元数据(如
PG_compound
)。
- 为大页面(compound pages)设置标志和元数据(如
zone_watermark_ok(z, order, mark, classzone_idx, can_try_harder, gfp_high)
:
-
- 检查分区是否满足分配的水位要求,调整空闲页面计算。
4. 页面回收的流程和函数
页面回收通过释放页面回伙伴系统或 per-CPU 缓存完成,涉及以下流程和函数:
回收流程
- 入口:
__free_pages(page, order)
:
-
- 检查页面是否可释放(
put_page_testzero
),如果是 order == 0,调用free_hot_page
;否则调用__free_pages_ok
.
- 检查页面是否可释放(
- 释放到 per-CPU 缓存:
-
- 对于单页(order == 0),
free_hot_cold_page(page, cold)
将页面加入 per-CPU 缓存(per_cpu_pages
的 hot 或 cold 链表)。 - 如果缓存超过
high
水位,调用free_pages_bulk
批量归还页面到伙伴系统。
- 对于单页(order == 0),
- 释放到伙伴系统:
-
__free_pages_bulk(page, zone, order)
将页面归还到zone->free_area[order].free_list
,尝试与伙伴合并到更高 order。- 如果伙伴也是空闲的,递归合并(通过
__page_find_buddy
和__find_combined_index
)。
- 状态检查:
-
free_pages_check
检查页面状态(映射、引用计数、标志等),防止释放非法页面。
- 特殊场景:
-
- 在电源管理(
CONFIG_PM
)或 CPU 热插拔(CONFIG_HOTPLUG_CPU
)时,drain_local_pages
和__drain_pages
清空 per-CPU 缓存,归还页面到伙伴系统。
- 在电源管理(
关键回收函数
__free_pages(page, order)
:
-
- 页面释放的入口,决定调用
free_hot_page
或__free_pages_ok
。
- 页面释放的入口,决定调用
free_hot_cold_page(page, cold)
:
-
- 将单页释放到 per-CPU 缓存,区分热/冷页面。
__free_pages_ok(page, order)
:
-
- 处理多页释放,检查页面状态后调用
free_pages_bulk
。
- 处理多页释放,检查页面状态后调用
free_pages_bulk(zone, count, list, order)
:
-
- 批量释放页面到伙伴系统,更新
free_area
和free_pages
。
- 批量释放页面到伙伴系统,更新
__free_pages_bulk(page, zone, order)
:
-
- 单页面块的释放逻辑,执行伙伴合并,更新
free_area
。
- 单页面块的释放逻辑,执行伙伴合并,更新
page_is_buddy(page, order)
:
-
- 检查页面是否是空闲伙伴,可用于合并。
__page_find_buddy(page, page_idx, order)
:
-
- 查找页面的伙伴页面,用于合并。
destroy_compound_page(page, order)
:
-
- 释放复合页面,清除
PG_compound
标志。
- 释放复合页面,清除
5. 关键函数总结
以下是页面分配与回收的核心函数及其作用:
函数 | 作用 | 场景 |
| 核心分配函数,协调 zonelist 和水位检查 | 分配任意 order 的页面 |
| 从 per-CPU 缓存或伙伴系统分配页面 | 单页或多页分配 |
| 从伙伴系统分配页面,执行分裂 | 多页分配 |
| 批量分配页面到链表 | 补充 per-CPU 缓存 |
| 初始化新分配页面 | 分配后准备 |
| 设置复合页面元数据 | 大页面分配 |
| 检查分区水位是否满足分配 | 分配前检查 |
| 页面释放入口 | 释放任意 order 页面 |
| 释放单页到 per-CPU 缓存 | 单页释放 |
| 释放多页到伙伴系统 | 多页释放 |
| 批量释放页面到伙伴系统 | 批量回收 |
| 单页面块释放,执行伙伴合并 | 伙伴系统回收 |
| 清空 per-CPU 缓存 | 电源管理/CPU 热插拔 |
6. 页面分配与回收的详细流程
分配流程示例
假设请求分配 2 页(order=1),GFP 标志为 GFP_KERNEL
:
- 调用
__alloc_pages(GFP_KERNEL, 1, zonelist)
。 - 遍历
zonelist->zones
,检查每个分区(优先 ZONE_NORMAL)。 zone_watermark_ok
确认分区有足够空闲页面(free_pages
>pages_low
)。- 调用
buffered_rmqueue(zone, 1, GFP_KERNEL)
:
-
- 因 order > 0,直接调用
__rmqueue(zone, 1)
。 __rmqueue
从free_area[1]
获取 2 页块,若为空,从更高 order 分裂(expand
)。
- 因 order > 0,直接调用
- 初始化页面(
prep_new_page
),设置复合页面(prep_compound_page
如果需要)。 - 更新 NUMA 统计(
zone_statistics
),返回页面。
回收流程示例
释放 2 页(order=1):
调用 __free_pages(page, 1)
,检查引用计数(put_page_testzero
)。
调用 __free_pages_ok(page, 1)
:
-
free_pages_check
验证页面状态。- 调用
free_pages_bulk(zone, 1, list, 1)
。
free_pages_bulk
调用 __free_pages_bulk(page, zone, 1)
:
-
- 将页面加入
free_area[1].free_list
。 - 检查伙伴页面(
__page_find_buddy
),若空闲,合并到free_area[2]
,递归直到无法合并。
- 将页面加入
更新 zone->free_pages
和 free_area[order].nr_free
。
7. 面试准备建议
- 核心概念:
-
- 理解伙伴系统的分裂与合并(
expand
和__free_pages_bulk
)。 - 掌握 per-CPU 缓存的作用(
buffered_rmqueue
和free_hot_cold_page
)。 - 熟悉 NUMA 优化的 zonelist 和统计机制(
zone_statistics
)。
- 理解伙伴系统的分裂与合并(
- 常见问题:
-
- 如何处理内存不足? 答:通过
wakeup_kswapd
和try_to_free_pages
回收页面,必要时触发 OOM。 - GFP 标志的作用? 答:控制分配的分区(
__GFP_DMA
、__GFP_HIGHMEM
)和行为(__GFP_ZERO
、__GFP_NOFAIL
)。 - 伙伴系统如何减少碎片? 答:通过 power-of-2 块和合并机制,减少外部碎片。
- 如何处理内存不足? 答:通过
- 代码分析:
-
- 熟悉
__alloc_pages
和__free_pages
的逻辑,结合mmzone.h
的数据结构(如free_area
、per_cpu_pages
)。 - 理解水位线(
zone_watermark_ok
)和低内存保护(lowmem_reserve
)的实现。
- 熟悉
- 实践建议:
-
- 阅读
mm/page_alloc.c
和mm/vmscan.c
的相关代码。 - 用图示解释分配流程(如 zonelist -> zone -> free_area)。
- 准备讨论 NUMA 和 SMP 场景下的优化。
- 阅读
总结
mm/page_alloc.c
实现了 Linux 内核的页面分配与回收,基于伙伴系统、per-CPU 缓存和 NUMA 优化的分层架构。核心函数包括 __alloc_pages
(分配)、__free_pages
(回收)、buffered_rmqueue
(缓存分配)、free_hot_cold_page
(缓存回收)等。设计思想强调性能(缓存、NUMA 局部性)、可扩展性(节点/分区)、可靠性(水位线、错误检查)和灵活性(GFP 标志)。通过理解这些机制和函数,你可以深入掌握 Linux 内存管理的架构,并在面试中自信应对相关问题。
2.2.1 管理区极值
系统可用内存少时,守护程序kswapd被唤醒释放页面。内存压力大时,进程会同步释放内存(direct - reclaim路径)。影响页面换出行为的参数与FreeBSD、Solaris、MM01中所用参数类似。
每个管理区有三个极值pages_low
、pages_min
和pages_high
:
- pages_low:空闲页面数达到该值时,伙伴分配器唤醒kswapd释放页面,默认值是
pages_min
的两倍 。 - pages_min:达到该值时,分配器同步启动kswapd ,在Solaris中无等效参数。
- pages_high:kswapd被唤醒并开始释放页面后,在释放
pages_high
个页面以前,不认为管理区“平衡”,达到该极值后,kswapd再次睡眠,默认值是pages_min
的三倍 。 这些极值用于决定页面换出守护程序或页面换出进程释放页面的频繁程度。
2.2.2 计算管理区大小
- 计算位置与流程
管理区大小在setup_memory()
函数中计算。图2.3展示了setup_memory
的调用关系,其调用链包含find_max_pfn
、find_max_low_pfn
、init_bootmem
、register_bootmem_low_pages
、find_smp_config
等多个函数,各函数在内存初始化等环节发挥作用。 - 关键变量及含义
-
- PFN(物理页帧号):用于物理内存映射,以页面计算偏移量。系统中第一个可用的PFN(
min_low_pfn
)分配在被导入的内核映象末尾_end
后的第一个页面位置,存储在mm/bootmem.c
文件中,与引导内存分配器配合使用。 max_low_pfn
:在x86情况下,通过find_max_pfn()
函数计算出ZONE_NORMAL
管理区的结束位置值。此管理区是内核可直接访问的物理内存,通过PAGE_OFFSET
标记内核/用户空间划分,该值存储在mm/bootmem.c
文件。max_pfn
:表示系统中最后一个页面帧的编号。在内存少的机器上,max_pfn
值等于max_low_pfn
。
- PFN(物理页帧号):用于物理内存映射,以页面计算偏移量。系统中第一个可用的PFN(
- 计算用途
通过min_low_pfn
、max_low_pfn
和max_pfn
这三个变量,可确定高端内存起始和结束位置(相关变量highstart_pfn
和highend_pfn
存储在arch/i286/mm/init.c
中 ),这些值被物理页面分配器用于初始化高端内存页面。
2.2.3 管理区等待队列表
- 等待队列机制原理
当页面进行I/O操作(如页面换入/换出 )时,为防止访问不一致数据,使用页面的进程需在I/O访问前通过wait_on_page()
添加到等待队列,I/O完成后页面经UnlockPage()
解锁,等待队列上进程被唤醒。理论上每个页面都应有等待队列,但这会耗费大量内存,Linux将等待队列存储在zone_t
中。 - 解决惊群效应
若管理区只有一个等待队列,页面解锁时所有等待进程都会被唤醒,产生惊群效应。Linux的解决办法是将等待队列的哈希表存储在zone_t->wait_table
中,虽哈希冲突时仍可能有无故唤醒进程,但冲突频率降低。 - 哈希表相关计算
-
wait_table_size
计算:在free_area_init_core()
时初始化,通过wait_table_size()
计算 ,存储在zone_t->wait_table_size
。等待队列表最多4096个,其大小取NoPages/PAGE_PER_WAITQUEUE
个队列数和2的幂次方的最小值,计算公式为:
[ wait_table_size = \log_2\left(\frac{NoPages \times 2}{PAGE_PER_WAITQUEUE} - 1\right) ]
其中NoPages
是管理区页面数 ,PAGE_PER_WAITQUEUE
定义为256。zone_t->wait_table_shift
计算:通过页面地址的比特数右移得到其在表中的索引 。page_waitqueue()
函数:用于返回某管理区中一个页面对应的等待队列,一般采用基于已被哈希的struct page
虚拟地址的乘积哈希算法,通常需用GOLDEN_RATIO_PRIME
乘以该地址,并将结果右移得到在哈希表中的索引,GOLDEN_RATIO_PRIME [Lev00]
是系统中最接近所能表达的最大整数的golden ratio[Knu68]的最大素数。
2.3 管理区初始化
- 初始化时机
管理区的初始化在内核页表通过paging_init()
完全建立起来后进行 ,页表初始化在3.6节涉及。不同系统执行此任务方式有别,但目标一致,即向UMA结构中的free_area_init()
或NUMA结构中的free_area_init_node()
传递合适参数。 - 所需参数
-
nid
:被初始化管理区中节点的逻辑标识符(NodeID )。pgdat
:指向pg_data_t
,在UMA结构里为contig_page_data
。pmap
:被free_area_init_core()
函数用于设置指向分配给节点的局部lmem_map
数组的指针 。在UMA结构中常被忽略,因NUMA将mem_map
处理为起始于PAGE_OFFSET
的虚拟数组,而UMA中该指针指向全局mem_map
变量,目前相关mem_map
都在UMA中被初始化。zones_sizes
:包含每个管理区大小的数组,管理区大小以页面为单位计算。zone_start_paddr
:每个管理区的起始物理地址。zone_holes
:包含管理区中所有空闲洞大小的数组。
- 核心函数作用
核心函数free_area_init_core()
用于向每个zone_t
填充相关信息,并为节点分配mem_map
数组 。此时不考虑释放管理区中哪些页面的信息,该信息在引导内存分配器用完之后才可知,将在第5章讨论。
2.4 初始化mem_map
- 初始化方式
-
- NUMA系统:全局
mem_map
被视为起始于PAGE_OFFSET
的虚拟数组。free_area_init_node()
函数由每个活动节点调用,在节点初始化时分配数组的一部分 。 - UMA系统:
free_area_init()
使用contig_page_data
作为节点,将全局mem_map
当作该节点的局部mem_map
。
- NUMA系统:全局
- 核心函数及内存分配
核心函数free_area_init_core()
为已初始化的节点分配局部lmem_map
,其内存通过引导内存分配器中的alloc_bootmem_node()
分配(第5章介绍 )。在UMA结构中,新分配内存成为全局mem_map
,与NUMA结构存在差异。 - NUMA与UMA的差异
-
- NUMA:分配给
lmem_map
的内存位于各自内存节点。全局mem_map
未明确分配,被处理成虚拟数组。局部映射地址存储在pg_data_t->node_mem_map
和虚拟mem_map
中。对节点内每个管理区,虚拟mem_map
中管理区地址存储在zone_t->zone_mem_map
,其余节点将mem_map
视为真实数组,仅有效管理区被节点使用。
- NUMA:分配给
2.5 页面
在Linux系统中,每个物理页面都由struct page
关联记录状态,在内核2.2版本中,该结构在<linux/mm.h>
中声明如下:
typedef struct page {struct list_head lru;struct list_head list;struct address_space *mapping;unsigned long index;struct page *next_hash;atomic_t count;unsigned long flags;struct list_head lru;struct page *pprev_hash;struct buffer_head *buffers;# if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL)void *virtual;# endif /* CONFIG_HIGHMEM || WANT_PAGE_VIRTUAL */mem_map_t;
}
各字段介绍:
list
:页面可属于多个列表,此字段用作列表首部。在slab分配器中,存储指向管理页面的slab和高速缓存结构指针 ,用于链接空闲页面块。mapping
:若文件或设备映射到内存,其索引节点有相关address_space
。若页面属于该文件,此字段指向该address_space
;匿名页面且设置了mapping
,则address_space
是交换地址空间的swapper_space
。index
:有两个用途,若是文件映射部分,是页面在文件中的偏移;若是交换高速缓存部分,是在交换地址空间中swapper_space
的偏移量。若包含页面的块被释放,释放块的顺序(被释放页面2的幂 )存于其中,在free_pages_ok()
中设置。next_hash
:属于文件映射并散列到索引节点及偏移中的页面,将共享相同哈希桶的页面链接一起。count
:页面被引用数目,减到0时释放,被多个进程使用或内核用到时增大。flags
:描述页面状态标志位,在<linux/mm.h>
声明,部分在表2.1列出 ,许多已定义宏用于测试、清空和设置,最有用的是SetPageUptodate()
,若设置位前已定义,会调用体系结构相关函数arch_set_page_uptodate()
。lru
:根据页面替换策略,可能被交换出内存的页面存于page_alloc.c
声明的active_list
或inactive_list
中,是最近最少使用(LRU )链表的链表首部。pprev_hash
:是next_hash
的补充,使哈希链表可双向链表工作。buffers
:若页面有相关块设备缓冲区,此字段跟踪buffer_head
。匿名页面有后援交换文件时,进程映射的该匿名页面也有相关buffer_head
,缓冲区必不可少,因页面须与后援存储器中的文件系统块同步。virtual
:通常情况下,只有来自ZONE_NORMAL
的页面才由内核直接映射。为定位ZONE_HIGHMEM
中的页面,kmap()
用于为内核映射页面(第9章讨论 ),只有一定数量页面会被映射,页面被映射时,这是其虚拟地址。mem_map_t
:是对struct page
的类型定义,在mem_map
数组中方便使用。
表2.1 描述页面状态的标志位
位名 | 描述 |
| 位于LRU |
| 直接从代码中引用,是体系结构相关的页面状态位,一般代码保证第一次进入页面高速缓存时清除,使体系结构可延迟到页面被进程映射后才进行D - Cache刷盘 |
| 仅由Ext2文件系统使用 |
| 显示页面是否需刷新到磁盘上,磁盘上对应页面进行写操作后设置,保证脏页面写出前不会被释放 |
| 在磁盘I/O发生错误时设置 |
| 被文件系统保留,目前只有NFS用它表示一个页面是否和远程服务器同步 |
| 高端内存的页面标记,高端内存页面在 |
| 只对页面替换策略重要,VM要换出页面时设置,并调用 |
| 页面在磁盘I/O所在内存时设置,I/O启动时设置,I/O完成时释放 |
| 当页面存在于 |
| 页面被映射且被映射的索引哈希表引用时设置,在LRU链表上移动页面做页面替换时使用 |
| 禁止换出的页面,系统初始化时由引导内存分配器设置,标志页面或不存在 |
| 被slab分配器使用的页面 |
| 某些Space体系结构用于跳过部分地址空间,现在不再使用 |
| 表示没有被使用 |
| 当一个页面从磁盘无错误读出时设置 |
表2.2 用于测试、设置和清除page->flags的宏
位名 | 设置 | 测试 | 清除 |
|
|
|
|
| None | None | None |
|
|
| None |
|
|
|
|
|
|
|
|
| None |
| None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| None | None | None |
|
|
|
|
| None | None | None |
|
|
|
|
2.6 页面映射到管理区
- 页面管理区映射方式演变
在内核2.4.18版本,struct page
曾存储指向对应管理区的指针page->zone
,但因大量struct page
存在时该指针消耗内存多,在更新内核版本中被删除,改用page->flags
的最高ZONE_SHIFT
位(x86下是8位 )记录页面所属管理区。 - 管理区相关数据结构及函数
-
zone_table
:在linux/page_alloc.c
中声明,用于建立管理区 ,定义如下:
zone_t * zone_table[MAX_NR_ZONES * MAX_NR_NODES];
EXPORT_SYMBOL(zone_table);
其中MAX_NR_ZONES
是一个节点中能容纳管理区的最大数(如3个 ),MAX_NR_NODES
是可存在节点的最大数。EXPORT_SYMBOL()
函数使zone_table
能被载入模块访问,该表类似多维数组。
- free_area_init_core()
函数:在该函数中,一个节点中的所有页面会被初始化。设置zone_table
值的方式为:
zone_table[nid * MAX_NR_ZONES + j] = zone;
这里nid
是节点ID,j
是管理区索引号,zone
是zone_t
结构。
- set_page_zone()
函数:用于为每个页面设置所属管理区,调用方式为:
set_page_zone(page, nid * MAX_NR_ZONES + j);
其中page
是要设置管理区的页面 ,通过这种方式,zone_table
中的索引被显式存储在页面中。
2.7 高端内存
- 引入原因:内核
ZONE_NORMAL
中可用地址空间有限,所以Linux内核支持高端内存概念。 - 32位x86系统阈值及相关限制
-
- 4 GB阈值:与32位物理地址定位的内存容量有关。为访问1 GB - 4 GB之间内存,内核通过
kmap()
将高端内存页面临时映射成ZONE_NORMAL
,第9章深入讨论。 - 64 GB阈值:与物理地址扩展(PAE )有关,PAE是Intel发明用于允许32位系统使用RAM的技术,通过附加4位定位内存地址,实现2⁶⁴字节(64 GB)内存定位 。
- 4 GB阈值:与32位物理地址定位的内存容量有关。为访问1 GB - 4 GB之间内存,内核通过
- 理论与实际情况差异
理论上PAE允许处理器寻址到64 GB,但Linux中进程虚拟地址空间仍是4 GB,试图用malloc()
分配所有RAM的用户会失望。 - 地址扩展空间(PAE)的影响
PAE不允许内核自身拥有大量可用RAM。描述页面的struct page
需44字节,占用ZONE_NORMAL
内核虚拟地址空间 。描述1 GB内存需约11 MB内核内存,描述16 GB内存需176 MB,给ZONE_NORMAL
带来巨大压力。像页面表项(PTE )这类小数据结构最坏情况下也需约16 MB。所以在x86机器上Linux可用物理内存被限制为16 GB,若需访问更多内存,建议使用64位机器。
1