在计算机的世界里,内存就像一座神秘的大厦,而 Linux 内核的物理内存模型则是这座大厦的基石。今天,我们将一起走进 Linux 内核物理内存模型的世界,探索它是如何管理和分配内存资源的。这就像是一场穿越时空的旅行,从最基础的内存架构到复杂的内存管理机制,每一步都将为我们揭开 Linux 内核的神秘面纱。
无论是对计算机技术感兴趣的初学者,还是想要深入了解内核的专业人士,都能在这里找到属于自己的收获。让我们开启这场内存管理的奇妙之旅,去发现 Linux 内核物理内存模型的无限魅力。
一、物理内存模型概述
在 Linux 系统的庞大架构里,内存管理无疑是一块关键基石。它肩负着保障系统稳定运行、实现资源高效利用以及提升应用程序性能等多重重任,犹如一位幕后英雄,默默支撑着整个系统的运转。当我们同时开启多个应用程序,比如一边听音乐、一边浏览网页,还后台运行着文件下载任务,此时内存管理就要像一位精明的管家,合理分配内存资源,让每个程序都能顺畅运行,互不干扰。
这背后靠的就是内存管理对进程内存空间的精细划分与调度,确保每个进程都有专属的 “内存领地”,避免数据混乱与冲突。再者,对于资源有限的嵌入式设备,如智能手环、智能家居控制器等,高效的内存管理更是决定设备性能优劣的关键。通过优化内存使用,系统能够快速响应操作指令,避免卡顿,为用户带来流畅体验。毫不夸张地说,深入探究 Linux 内核内存管理机制,是解锁系统潜能、优化应用性能的必经之路。而物理内存模型作为内存管理的根基,更是值得我们深入挖掘。
操作系统是构建在硬件架构之上的,Linux 自然也不能幸免。目前,主要有两种类型的物理内存架构:UMA(Uniform Memory Access,一致性内存访问)架构和 NUMA (Non-Uniform Memory Access,非一致性内存访问)架构。UMA 将可用内存以连续的方式组织起来,系统中各 CPU 到内存的距离相同,访问时间一致;NUMA 架构将系统中的内存和 CPU 分成不同的组(节点),每个 CPU 访问本节点的内存(称为本地内存,local memory)比访问其它节点的内存(称为非本节点内存 non-local memory 或远端内存 remote memory)速度要快。
在这两种内存架构的基础上,分为三种内存模型,分别是:平坦内存模型、非连续内存模型和稀疏内存模型。平坦内存模型对应着内核配置选项 FLATMEM,非连续内存模型对应着内核配置选项 DISCONTIGMEM,稀疏内存模型对应着内核配置选项 SPARSEMEM 或者 SPARSEMEM_VMEMMAP。
二、体系架构:多样的内存布局选择
2.1UMA 架构
在单处理器时期,架构如下图所示:
图片
随着多处理器时代的来临,架构演变成如下结构:
图片
在这种架构下,所有的 CPU 位于总线的一侧,而所有的内存条组成的整块内存位于总线的另一侧。任何 CPU 想要访问内存都要经过总线,而且距离都是一样的,这种架构称为 SMP(Symmetric Multiprocessing,对称多处理器)架构。在 SMP 架构下,任何处理器访问内存的距离是相同的,所以其访问内存的速度是一致的。这种架构也被成为基于 SMP 的 UMA (Uniform Memory Access,一致性内存访问)架构。
UMA 架构的特点是简单,但是有一个显著的缺点:由于所有处理器访问内存都要经过总线,当处理器数量很多时,总线就会成为整个系统的瓶颈。
2.2NUMA 架构
在现代多处理器系统的舞台上,非一致内存访问(NUMA)架构宛如一颗闪耀的明星,占据着中高端服务器领域的主流地位。它的设计理念犹如一场对传统内存访问模式的革新,将物理内存巧妙地划分成多个独立的节点,每个节点都紧密簇拥着一组处理器、本地内存以及 I/O 设备,宛如一个个自给自足的 “小王国”。
以一台配备 NUMA 架构的高性能服务器为例,当处理器在执行任务时,若所需数据存于本地节点的内存中,便能以极快的速度获取,仿若闪电般迅速;然而,一旦数据位于其他节点的内存,就需通过高速互连通道长途跋涉去调取,这期间的延迟便如同蜗牛爬行,明显增加。有实验数据表明,在某些复杂的计算任务中,处理器访问本地内存的耗时可能仅在几十纳秒,而访问远端节点内存的耗时却会飙升至数百纳秒,甚至更多。这种差异在大规模数据处理、高性能计算等对内存访问速度要求苛刻的场景中,影响可谓深远。
正因如此,诸多企业级应用,像大型数据库管理系统、科学计算软件等,纷纷精心优化内存分配策略,竭力让数据尽可能靠近使用它的处理器,以充分挖掘 NUMA 架构的性能潜力,确保系统高效运转。每个节点都有自己的内存(称为本地内存),并可包含一个或多个处理器。节点和节点之间通过 QPI(Intel QuickPath Interconnect)完成互联,其架构如下图所示:
图片
注:这里的 Core 指的是物理核,HT(Hyper-Threading,超线程)指的是逻辑核。
在 NUMA 架构下,任意一个 CPU 都可以访问所有节点的内存,访问自己节点的本地内存是最快的,但访问其他节点的内存就会慢很多,这就导致了 CPU 访问内存的速度不一致,所以叫做非一致性内存访问架构。
NUMA 架构严格意义上来讲不属于 SMP 的范畴,但是由于其每个处理器访问内存的模式是一致的,所以在逻辑上属于对称多处理 (SMP) 架构的扩展。
对称多处理器(SMP)
对称多处理器(SMP)架构恰似一位秉持公平原则的协调者,在其构建的系统世界里,所有处理器地位平等,共享同一物理内存,无论访问内存中的哪个地址,所消耗的时间都如同复制粘贴般完全一致。这种一致性使得内存管理在某些场景下显得简洁明了,易于掌控。就拿常见的小型服务器或工作站来说,它们处理的任务相对单一,负载较轻,SMP 架构便能轻松应对,充分发挥资源共享的优势,让系统流畅运行。
不过,SMP 架构也并非完美无缺,随着处理器数量的逐步增加,它的短板开始显现。由于所有处理器都紧紧依赖同一条内存总线去访问内存,如同众人争抢独木桥,内存访问冲突愈发激烈,导致内存带宽迅速成为系统性能提升的瓶颈,限制了系统的进一步扩展。在内核初始化阶段,SMP 架构也展现出独特的一面,0 号处理器勇挑重担,担当引导处理器,负责完成内核的初始化工作,而其他处理器则如同乖巧的学生,静静等待初始化完成,之后才一同参与系统的运行,携手并肩处理各项任务。
混合体系结构
在现实复杂多变的应用场景中,一种融合的智慧应运而生 —— 混合体系结构。它巧妙地将 NUMA 与 SMP 的优势合二为一,恰似一位博采众长的智者,根据不同的应用需求灵活调配资源,实现性能的优化升华。比如,在大型数据中心的服务器集群里,整体架构采用 NUMA 架构,充分利用其扩展性强、内存访问局部性好的优势,应对海量数据的存储与处理挑战;
而在每个 NUMA 节点内部,则引入 SMP 架构,让节点内的多个处理器能够平等、高效地共享本地内存,协同处理任务,进一步提升执行效率。再如,一些对实时性要求极高的工业控制系统,通过精细的配置,使关键任务在 SMP 模式下的处理器上紧密运行,确保响应的及时性;同时,将非关键任务合理分配至 NUMA 架构的其他节点,实现资源的优化利用,保障整个系统的稳定与高效。这种混合体系结构,凭借其灵活多变的特性,宛如一把万能钥匙,能够解锁各种复杂应用场景下的性能密码,为系统的高效运行保驾护航。
三、内存模型:应对复杂的物理内存
3.1平坦内存(Flat Memory)
在计算机系统的世界里,平坦内存(Flat Memory)模型宛如一位简洁高效的组织者,适用于那些物理内存连续或近乎连续的非 NUMA 系统场景。想象一下,在一些小型的嵌入式设备或者早期相对简单的个人计算机系统中,它们的物理内存布局规整,没有过多复杂的 “缝隙” 与 “空洞”,Flat Memory 模型便能大显身手。
在这种模型下,内核运用一个全局的 mem_map 数组来精心管理整个物理内存,如同一位严谨的图书管理员,将每一页物理内存都按照顺序在 mem_map 数组中安排得井井有条,数组的下标即为对应的物理页框号(PFN)。举例来说,若系统的物理内存从地址 0 开始,依次递增,毫无间断,那么 PFN 为 0 的物理页,恰好对应 mem_map 数组的第 0 个元素,以此类推,这种一一对应的线性关系使得内存管理变得直观易懂。
不过,现实中的系统架构偶尔会存在一些特殊情况,即便内存整体连续,但某些区域可能因硬件设计、预留等原因无法被正常使用,这些区域就如同图书馆书架上的 “空位”,虽占据位置却不存放实际书籍。在 mem_map 数组中,它们同样拥有对应的条目,只不过与之对应的 struct page 对象如同尚未书写内容的空白纸张,不会被完全初始化,处于一种特殊的待命状态。
当系统需要分配内存时,只需基于 mem_map 数组进行简单的偏移计算,就能迅速定位到可用的物理页,就像读者在图书馆中根据书架编号快速找到所需书籍一般,高效便捷。
在这种模型下,处理器将物理内存看做是一个连续的,没有空洞的地址空间。内核定义了一个全局的struct page数组mem_map,用于保存所有的struct page对象。由于struct page对象和 PFN 是一一对应的,所以每个 PFN 对应着mem_map中的一个成员。
⑴全局变量 mem_map
mem_map 是一个全局变量,表示 struct page 数组,其声明如下:
// file: mm/memory.c
#ifndef CONFIG_NEED_MULTIPLE_NODES
......
struct page *mem_map;
......
#endif
mem_map 在 alloc_node_mem_map 函数中被初始化。在 NUMA 架构下,每个节点由 struct pglist_data 结构体表示,其中 pglist_data->node_mem_map 字段指示该节点对应的 struct page 数组的起始位置。
// file: include/linux/mmzone.h
typedef struct pglist_data {
......
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map;
......
#endif
} pg_data_t;
由于平坦内存模型相当于只有一个节点,所以 mem_map 对应于节点 0 的 pglist_data->node_mem_map 的值。
// file: mm/page_alloc.c
static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat)
{
/* Skip empty nodes */
/*
* pgdat->node_spanned_pages 指示节点中的物理页数量(包括内存空洞)
* 如果该值为 0,说明该节点没有内存,直接跳过
*/
if (!pgdat->node_spanned_pages)
return;
/*
* 配置选项 CONFIG_FLAT_NODE_MEM_MAP 指示将每个节点的内存当做平坦模型来看待
* 对应着非稀疏内存模型
* config FLAT_NODE_MEM_MAP
* def_bool y
* depends on !SPARSEMEM
*/
#ifdef CONFIG_FLAT_NODE_MEM_MAP
/* ia64 gets its own node_mem_map, before this, without bootmem */
/*
* pgdat->node_mem_map 指示节点对应的 struct page 数组的起始地址
* 如果该值为 NULL,说明还未进行初始化,那么就需要对其进行初始化
*/
if (!pgdat->node_mem_map) {
unsigned long size, start, end;
struct page *map;
/*
* The zone's endpoints aren't required to be MAX_ORDER
* aligned but the node_mem_map endpoints must be in order
* for the buddy allocator to function correctly.
*/
/*
* MAX_ORDER 指示伙伴系统中的最大分配阶,扩展为 11,
* 表示伙伴系统支持 2 的 0 次方到 2 的 10 次方共 11 种内存分配大小
* #define MAX_ORDER 11
* MAX_ORDER_NR_PAGES 指示最大分配阶下每次分配的物理页数量,扩展为 1 << 10 即 1024 个页,对应 4MB 的内存
* #define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))
*
* pgdat->node_start_pfn 表示节点的起始页帧号
* pgdat_end_pfn(pgdat) 获取节点的结束页帧号(包括内存空洞)
* 二者都需要向上对齐到 MAX_ORDER,这是伙伴系统的要求
* size 计算出节点对应的 struct page 数组占用的空间
* 接下来,从节点内存中分配 size 大小的内存用于保存 struct page 数组,内存的起始地址保存到变量 map 中
* 最后,根据对齐结果,修正 map 的值,并赋值给 pgdat->node_mem_map,用作 struct page 数组的起始地址
*
*/
start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
end = pgdat_end_pfn(pgdat);
end = ALIGN(end, MAX_ORDER_NR_PAGES);
size = (end - start) * sizeof(struct page);
map = alloc_remap(pgdat->node_id, size);
if (!map)
map = alloc_bootmem_node_nopanic(pgdat, size);
pgdat->node_mem_map = map + (pgdat->node_start_pfn - start);
}
/*
* 内核配置选项 CONFIG_NEED_MULTIPLE_NODES 表示是否需要多个节点,默认为 yes
* 配置了该选项意味着是非连续内存模型(DISCONTIGMEM)或者 NUMA 架构
************************************************************
* config NEED_MULTIPLE_NODES
* def_bool y
* depends on DISCONTIGMEM || NUMA
************************************************************
* 如果没有设置该选项,说明只有一个节点,对应于平坦内存模型
*/
#ifndef CONFIG_NEED_MULTIPLE_NODES
/*
* With no DISCONTIG, the global mem_map is just set as node 0's
*/
/*
* 对于平坦内存模型,将 mem_map 设置为节点 0 的 node_mem_map 的值
* 宏 NODE_DATA 用于获取指定节点的 struct pglist_data 实例
*/
if (pgdat == NODE_DATA(0)) {
mem_map = NODE_DATA(0)->node_mem_map;
/*
* 内核配置选项 CONFIG_HAVE_MEMBLOCK_NODE_MAP 指示对于 memblock 内存块是否需要区分不同的节点,
* memblock 用于启动时内存管理
*
* x86 架构下,该配置项默认为 yes
* 此时,当 mem_map 对应的页帧号不等于节点的起始页帧号时,还需要进一步调整
*/
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
if (page_to_pfn(mem_map) != pgdat->node_start_pfn)
mem_map -= (pgdat->node_start_pfn - ARCH_PFN_OFFSET);
#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */
}
#endif
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}
alloc_node_mem_map 函数的完整调用流程如下:
start_kernel() -> setup_arch() -> pagetable_init() -> x86_init.paging.pagetable_init() -> native_pagetable_init() -> paging_init() -> zone_sizes_init() -> free_area_init_nodes() -> free_area_init_node() -> alloc_node_mem_map()。
⑵pfn 和 page 的相互转换
在平坦内存模型下,pfn 和 struct page 的转换逻辑相对简单,如下所示:
// file: include/asm-generic/memory_model.h
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)
可以看出,pfn 和 struct page 实际是线性对应的关系,PFN - ARCH_PFN_OFFSET 就是 mem_map 数组的索引 。其中,mem_map 就是上文提到的全局数组,ARCH_PFN_OFFSET 是与处理器架构相关的页帧偏移量,其定义了物理内存起始地址不为 0 的系统的第一个页帧号。对于 x86 架构而言,ARCH_PFN_OFFSET 的值为 0。
为了保证内容的完整性,我们来看下 ARCH_PFN_OFFSET 是如何定义的。
在设置了内核配置选项 CONFIG_FLATMEM 的前提下,如果没有定义 ARCH_PFN_OFFSET,那么就会将 ARCH_PFN_OFFSET 定义为 0。
// file: include/asm-generic/memory_model.h
#if defined(CONFIG_FLATMEM)
#ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET (0UL)
#endif
实际上,ARCH_PFN_OFFSET 在 include/asm-generic/page.h 文件中是有定义的,但其依赖于 PAGE_OFFSET 和 PAGE_SHIFT 的实现。
// file: include/asm-generic/page.h
#ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET (PAGE_OFFSET >> PAGE_SHIFT)
#endif
正如我们上文所说的,在 4KB 页的情况下,宏 PAGE_SHIFT 扩展为 12;而宏 PAGE_OFFSET 依赖于内核配置选项 CONFIG_KERNEL_RAM_BASE_ADDRESS。由于 x86 架构不会配置该选项,所以 ARCH_PFN_OFFSET 最终扩展为 0。
// file: include/asm-generic/page.h
#ifdef CONFIG_KERNEL_RAM_BASE_ADDRESS
#define PAGE_OFFSET (CONFIG_KERNEL_RAM_BASE_ADDRESS)
#else
#define PAGE_OFFSET (0)
#endif
另外,在 x86 架构下,alloc_remap 函数是一个空函数,所以 mem_map 数组内存的分配实际上是在 alloc_bootmem_node_nopanic 函数中进行的。
// file: include/linux/bootmem.h
static inline void *alloc_remap(int nid, unsigned long size)
{
return NULL;
}
在 alloc_bootmem_node_nopanic 函数的调用链中,最终会调用到 __alloc_memory_core_early 函数,该函通过 memblock_find_in_range_node 函数分配到物理内存后,使用宏 phys_to_virt 将物理内存地址转换成虚拟内存地址。
// file: mm/nobootmem.c
static void * __init __alloc_memory_core_early(int nid, u64 size, u64 align,
u64 goal, u64 limit)
{
......
addr = memblock_find_in_range_node(goal, limit, size, align, nid);
......
ptr = phys_to_virt(addr);
......
return ptr;
}
而 phys_to_virt 函数实际等同于 __va 宏。
// file: arch/x86/include/asm/io.h
static inline void *phys_to_virt(phys_addr_t address)
{
return __va(address);
}
__va(x) 和 __pa(x) 这两个宏我们在以前的文章中也多次提到过,其功能就是将物理内存地址和直接映射区的虚拟地址进行转换。换句话说,mem_map 数组位于虚拟地址的直接映射区。平坦内存模型下,struct page与 物理页对应关系示意图:
图片
3.2不连续内存(Discontiguous Memory)
当系统的物理内存空间出现了 “裂缝”,不再是连续的整体,不连续内存(Discontiguous Memory)模型便闪亮登场,成为解决这类复杂内存布局的得力助手。在非一致内存访问(NUMA)架构的系统中,由于内存被划分成多个节点,节点之间的内存地址往往存在间断,就好比一座城市被河流、山脉分隔成多个区域,各个区域内部建筑紧密相连,但区域之间有明显间隔。此时,Discontiguous Memory 模型便能充分发挥优势,对这些存在空洞的内存空间进行高效管理。
每个节点都由一个 struct pglist_data 结构体来精心打理,其内部的 node_mem_map 成员如同指向各个区域宝藏的地图,指向本节点内物理页描述符数组,确保在这片不连续的内存 “拼图” 中,每个节点内部的内存管理依然有序。
举例来说,在一个拥有多个处理器、且内存分布于不同插槽的服务器系统里,每个插槽对应的内存可视为一个独立节点,处理器访问本地插槽内存时速度较快,访问其他插槽内存则相对较慢。Discontiguous Memory 模型通过对不同节点的细致区分,让系统在面对复杂的内存访问需求时,能够精准调度,优先使用本地节点内存,减少跨节点数据传输带来的延迟,如同快递员优先派送本地包裹,避免长途转运,提升整体效率。从 PFN 到 struct page 的转换过程,需先依据 PFN 定位到所属节点,再通过节点内的 node_mem_map 找到对应的 struct page,这一过程虽然相较平坦内存模型多了一步节点定位,但却为复杂内存环境下的精准管理提供了可能。
在平坦内存模型下,处理器将物理内存看做一段连续的地址空间。但是,物理内存可能会存在空洞。特别是在 NUMA 架构下,各个节点的物理内存地址不再连续,这样在节点和节点之间就会出现较大的内存空洞。对于大多数架构,内存空洞在 mem_map 数组中都有对应的 struct page 对象。
也就是说,有些 page 对象实际映射到的是内存空洞。而映射到内存空洞的 struct page 对象永远不会完全初始化,也无法使用,所以这些 struct page 对象所占用的空间就被白白浪费掉了。
为了解决这个问题,引入了非连续内存模型。在非连续内存模型下,系统将每个节点的内存看做是一段单独的地址连续的平坦内存,然后在每个节点对应的pglist_data 结构体实例的 node_mem_map 字段中,保存着该节点对应的 struct page 数组的基地址。这样,就将一段不连续的内存空间分割成了多个连续的内存区间,每段区间都对应着平坦内存模型。
图片
在非连续内存模型下,PFN 和 struct page 之间的转换逻辑如下所示:
// file: include/asm-generic/memory_model.h
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
unsigned long __nid = arch_pfn_to_nid(__pfn); \
NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg)); \
(unsigned long)(__pg - __pgdat->node_mem_map) + \
__pgdat->node_start_pfn; \
})
每个节点的struct page 数组的起始地址由 pglist_data->node_mem_map 表示(相当于平坦内存模型的 mem_map ),PFN 到节点起始页帧号( pglist_data->node_start_pfn )的偏移量对应着数组pglist_data->node_mem_map 的索引值,两者相加就能得到 PFN 对应的 struct page 的地址。
在此之前,先要获取到 PFN 对应的节点 id(通过 arch_pfn_to_nid),然后通过宏 NODE_DATA 获取到节点 id 对应的 struct pglist_data 实例及其 node_mem_map 字段的值。宏 arch_local_page_offset 计算指定 PFN 相对于节点起始页帧的偏移量,即数组的索引。
// file: include/asm-generic/memory_model.h
#ifndef arch_pfn_to_nid
#define arch_pfn_to_nid(pfn) pfn_to_nid(pfn)
#endif
#ifndef arch_local_page_offset
#define arch_local_page_offset(pfn, nid) \
((pfn) - NODE_DATA(nid)->node_start_pfn)
#endif
注意:非连续内存模型已经废弃不用了。非连续内存模型可以看做稀疏内存模型的一种特例,而且经测试其负载比稀疏内存模型还要高,所以在 x86-64 架构下该内存模型已经被稀疏内存模型所替代。
在 v5.14 之后的内核版本中,CONFIG_DISCONTIGMEM相关的代码已经被移除了。
3.3稀疏内存(Sparse Memory)
稀疏内存(Sparse Memory)模型作为 Linux 内存管理中的 “全能选手”,专为应对那些物理内存地址空间存在大量空洞,且需要支持内存热插拔等高级功能的复杂系统而生。在现代大型服务器、云计算平台等场景中,随着硬件架构的不断升级与业务需求的动态变化,系统常常需要在运行过程中灵活增减内存模块,就如同搭建积木城堡时,随时可以添加或移除积木块。Sparse Memory 模型将物理内存巧妙地划分为多个区段,每个区段用 mem_section 结构体来精细描述,如同将一片广袤的土地划分成多个地块,每个地块都有详细的规划图。
区段内包含 section_mem_map,从逻辑上讲,它如同指向 struct page 阵列的 “指南针”,指引着内核找到对应物理页的描述信息。在区段划分方面,不同架构依据自身对物理内存支持的上限以及对内存管理粒度的需求,通过 SECTION_SIZE_BITS 和 MAX_PHYSMEM_BITS 等常量来精心定义区段大小和最大区段数。例如,在某款为大数据处理定制的服务器架构中,区段大小设置为 2^27 字节(即 128MB),如此一来,面对海量数据的存储与处理,内核能够以区段为单位灵活调配内存,避免小块内存频繁分配回收带来的性能损耗。对于 PFN 转换为 struct page,Sparse Memory 模型提供了两种精妙的方式:“classic sparse” 和 “sparse vmemmap”。
前者如同古老而可靠的地图,在 page-flags 中巧妙编码页面的段号,并利用 PFN 的高位信息精准访问映射该页框的段,在区段内,PFN 则作为指向页数组的索引,指引内核找到目标;后者则像是引入了智能导航系统,借助虚拟映射的内存映射优化 pfn_to_page 和 page_to_pfn 操作,通过一个全局的 struct page *vmemmap 指针,指向虚拟连续的 struct page 对象阵列,此时 PFN 摇身一变成为该数组的索引,struct page 从 vmemmap 的偏移量恰好就是该页的 PFN,这种方式大大加速了内存管理操作的速度,为系统高效运行提供了有力保障。
随着内存热插拔技术的出现,不止节点间内存地址不连续,单个节点内的内存地址不连续也成了常态。这时候,再用非连续内存模型就不合适了,于是又引入了稀疏内存模型 SPARSEMEM。稀疏内存模型是 Linux 中最通用的内存模型,其实不管是平坦内存模型还是非连续内存模型,都可以看做是稀疏内存模型的一种特殊状态。
稀疏内存模型使用 section 来管理 struct page 数组,section 替代了非连续内存模型中的节点的角色。由于每个 section 管理的 struct page 数量比节点要少的多,所以管理的粒度更细,更适合大块空洞很多的场景。
section 由 struct mem_section 表示,其中的 section_mem_map 字段在逻辑上是指向 struct page 数组的指针。
// file: include/linux/mmzone.h
struct mem_section {
unsigned long section_mem_map;
......
};
类比非连续内存模型,稀疏内存模型示意如下:
图片
是不是跟非连续内存模型非常相似?当然,上图只是一个简单的示意,并没有展示出 section 的组织方式。另外,在稀疏内存模型中, struct page 在虚拟地址中的布局也分为两种,上图也只展示了一种。
四、三级结构:精细的内存组织框架
4.1内存节点(Node)
在 Linux 内核的内存管理体系中,内存节点(Node)宛如一座宏伟建筑的基石,处于最顶层架构,是内存管理的关键起点。在非一致内存访问(NUMA)系统里,内存节点的划分犹如一场精心策划的布局,依据处理器与内存的亲密程度而定。每个处理器都有自己的 “近邻” 内存,它们紧密协作,组成一个内存节点,就像一个个以处理器为核心的 “小部落”。
如此一来,当处理器处理任务时,优先访问本地节点内存,能大幅缩短数据获取时间,如同在自家仓库取物般便捷高效,极大提升系统性能。而在具有不连续内存的统一内存访问(UMA)系统中,内存节点的界定则侧重于物理地址的连续性。那些连续的内存区域被划分成独立节点,如同将一片广袤但有间断的土地,按照连续的地块划分成不同区域,各自管理。
内存节点通过一个名为 pglist_data 的结构体来精准描述内存布局,这个结构体如同一位尽职的管家,事无巨细地记录着内存节点的各项信息。以一个典型的 NUMA 节点为例,node_zones 成员如同一个收纳盒,存放着多个内存区域(Zone)结构体,将内存按不同特性细分;node_zonelists 则像是一张备用路线图,指向备用的内存区域列表,以备不时之需;nr_zones 记录着该节点内内存区域的数量,如同清点家中房间数量般清晰明了。
再看 node_mem_map 成员,它仿若指向宝藏的指针,指向页描述符数组,每个物理页都有对应的页描述符,就像图书馆里每本书都有专属的目录卡片,详细记录着书籍信息。不过,它可能并非指向数组的首元素,这背后是为了满足页分配器对内存对齐的严格要求,确保内存分配的高效与稳定,如同建筑工人按照标准尺寸砌墙,保障墙体坚固整齐。
4.2内存区域(Zone)
深入到内存节点内部,我们会发现它被进一步细分为多个内存区域(Zone),就像一座城市被划分成不同功能区。内核精心定义了多种区域类型,每种都有其独特使命。ZONE_DMA,作为直接内存访问(DMA)区域,宛如一条为特定设备开辟的专属通道。在一些老旧的工业标准体系结构(ISA)设备中,由于硬件限制,它们只能在特定的低地址内存区域(通常是 0 - 16MB)进行 DMA 操作,这个区域就是它们的 “舞台”,确保数据能够顺畅传输,避免因地址不匹配导致的传输故障。
ZONE_DMA32 则像是为 64 位系统中的 “特殊居民” 准备的专属领地,它适用于那些仅支持 32 位地址寻址进行 DMA 操作的设备,为它们在低 4GB 的内存空间里预留位置,保障设备与内存的协同工作。ZONE_NORMAL 是内存区域中的 “主力军”,常规可直接映射到内核空间的内存大多在此区域,如同城市的中心商业区,承担着主要的内存使用任务。
对于 ARM 处理器,虽然内核空间与物理内存存在映射关系,但仍需借助页表进行精细的地址转换,如同通过详细的地图导航才能找到目的地;而 MIPS 处理器在这方面则相对简便,部分情况下无需复杂的页表映射,就能快速定位内存地址。在 32 位时代,ZONE_HIGHMEM 作为高端内存区域,是应对内存寻址局限的无奈之举。当时内核地址空间仅有 1GB,对于高于 896MB 的物理内存,无法直接映射,只能将其归入此区域,后续通过特殊映射方式来使用,如同将高处的物品通过特殊工具搬运下来。
到了 64 位系统,内核虚拟地址空间得到极大拓展,如同拥有了一个巨大的仓库,不再需要 ZONE_HIGHMEM 这个 “临时储物间”,所有内存都能得到妥善安置。此外,还有 ZONE_MOVABLE 这个特殊的 “机动部队”,它像是内存管理中的 “润滑剂”,通过灵活迁移页面,有效防止内存碎片的产生,保障内存分配的连续性;ZONE_DEVICE 则专为支持持久内存热插拔而设立,为设备驱动与内存的交互提供稳定接口,确保设备在热插拔过程中内存使用的稳定性。
4.3页(Page)
在 Linux 内核眼中,物理内存是以页(Page)为基本单位进行精细管理的,页就如同建筑中的砖块,是构成内存大厦的基础单元。每一个物理页面对应着一个 struct page 结构体,这个结构体如同一位忠诚的卫士,时刻跟踪着页面的使用情况。在处理器内部,有一个神奇的 MMU(内存管理单元)硬件,它如同一位翻译官,负责处理虚拟内存到物理内存的映射,将程序使用的虚拟地址转换为实际的物理地址,而页表就是它的翻译 “词典”。
在常见的系统中,页的大小通常为 4KB,这是综合考虑内存利用效率、硬件寻址能力等多方面因素后的权衡结果。就像将一块大的内存 “蛋糕” 切成大小合适的 “小块”,方便分配与管理。以一个运行多个应用程序的系统为例,当程序请求内存时,内核会以页为单位进行分配,若程序需要 8KB 内存,内核会分配两个连续的 4KB 页面,确保内存分配的规整与高效。
同时,页框(Page Frame)作为物理内存的存储单元,与页一一对应,页框号(PFN)则像每个页框的 “身份证号”,唯一标识每个页框。通过 PFN,内核能够快速定位到对应的物理页,如同通过身份证号查找人员信息般迅速准确。在内存分配回收、页面置换等一系列内存管理操作中,struct page 结构体记录着页面的状态信息,如是否空闲、是否被锁定、属于哪个进程等,为内核的决策提供关键依据,保障系统内存的合理利用与稳定运行。
五、实战案例:物理内存模型的应用展现
在科学计算领域,物理内存模型的优势尽显无遗。就拿大规模的天体物理模拟运算来说,科学家们需要处理海量的数据,这些数据如同浩瀚宇宙中的繁星,数量极其庞大。在采用 NUMA 架构的超级计算机上运行模拟程序时,物理内存模型充分发挥作用。它将内存划分为多个节点,每个节点紧密关联着一组处理器。
模拟程序在运行过程中,那些频繁交互的数据被巧妙地分配到各个处理器对应的本地内存节点中。就像一个分工明确的科研团队,每个成员专注处理自己手头与本地资源紧密相关的数据,大幅减少了数据跨节点传输的延迟。原本可能需要耗费数小时甚至数天的模拟计算,借助物理内存模型对内存的精细管理,运算时间大幅缩短,让科学家们能够更快地探索宇宙的奥秘,推动科学研究的步伐。
再看云计算环境,众多用户的虚拟机如同一个个 “虚拟房客”,共享着物理服务器的资源。这里的物理内存模型就像一位智慧的房东,要合理分配内存,满足不同 “房客” 的需求。当某个时间段内,部分虚拟机运行着对内存需求较大的企业级应用,如大型数据库管理系统、电商平台的后台服务等,物理内存模型会依据系统负载情况,动态调整内存分配。
对于那些处理关键业务、实时性要求高的虚拟机,优先保障其内存需求,将内存资源从相对空闲的区域调配过来,确保它们运行流畅,避免因内存不足导致的卡顿或服务中断。这就如同在繁忙的酒店里,优先满足重要客人的房间需求,合理安排空房资源,让整个酒店的运营有条不紊,为云计算用户提供稳定可靠的服务体验。
在嵌入式系统领域,以智能汽车的车载控制系统为例,物理内存模型同样发挥着关键作用。智能汽车内部有众多的电子控制单元(ECU),像发动机控制、自动驾驶辅助、车载娱乐等系统,它们都运行在有限的嵌入式芯片上。这些芯片采用的物理内存模型针对嵌入式系统的特点进行优化,在内存布局上,将与车辆安全、实时操控紧密相关的功能模块,如刹车控制、转向助力等,对应的内存区域设置为高优先级,确保数据读写的及时性。
同时,对于车载娱乐等非关键系统,在内存使用紧张时,适当限制其内存占用,优先保障行车安全相关功能的稳定运行。这种精准的内存管理,就像一位经验丰富的驾驶员,在复杂路况下合理分配精力,确保车辆安全平稳地行驶在道路上,为驾乘人员保驾护航。
六、回顾总结
回顾 Linux Kernel 物理内存模型,从多样的体系架构,到应对复杂物理内存的不同模型,再到精细的三级结构,每一处设计都凝聚着开发者的智慧,旨在应对不同硬件平台与应用场景的挑战,保障系统高效稳定运行。如今,随着技术浪潮滚滚向前,内存模型也在持续演进。
硬件层面,新型非易失性内存不断涌现,如 3D XPoint 技术带来的傲腾内存,兼具高速读写与持久存储特性,促使内核调整内存管理策略,以充分释放其潜能;软件应用领域,容器化技术、实时系统对内存隔离性、确定性提出更高要求,推动内存模型优化改进。展望未来,Linux Kernel 物理内存模型必将在创新与需求的双重驱动下,持续进化,为系统性能提升、功能拓展筑牢根基。希望本文能成为您探索 Linux 内核的得力助手,激发您深入钻研内核奥秘的热情,一同见证 Linux 技术生态的蓬勃发展。