linux 内存分布

Linux 中所有进程 / 内核的内存操作,都是基于虚拟地址,最终由 MMU + 页表映射到物理地址,物理内存是最终的存储载体。

物理内存分布

1. ZONE_DMA(直接内存访问区)

ZONE_DMA 是 Linux 内核物理内存管理中最古老的内存区域划分,专为解决早期硬件 DMA 设备的地址访问限制而设计。

  • 地址范围0 ~ 16MB(x86_64 固定);
  • 核心用途:供老旧的 DMA 设备使用,这类设备不支持 32 位以上的物理地址,只能访问低地址内存;
  • 访问无限制:老式 DMA 设备的地址线宽度通常只有 24 位,最大只能访问 2^24 = 16MB 的物理内存,无法访问更高地址的内存;
  • 内核标志GFP_DMA,分配该区域内存时需指定此标志。
  • 设计初衷:解决老式 DMA 设备的地址瓶颈

DMA(直接内存访问) 的核心原理

  • DMA 设备的作用:绕过 CPU,直接在内存和硬件设备之间传输数据(如磁盘、网卡),提升效率;
  • 早期 DMA 设备的缺陷:受限于硬件成本,地址线宽度只有 24 位 ,只能访问 0 ~ 16MB 的物理内存;
  • 内核的解决方案:划分 ZONE_DMA 区域,强制要求老式 DMA 设备只能使用该区域的内存,避免因访问高地址内存而失败。

不同架构下的 ZONE_DMA 差异

ZONE_DMA 的物理地址范围和存在意义 与硬件架构强相关,主流架构的差异如下:

架构 ZONE_DMA 物理地址范围 是否必需 核心差异点
x86(32 位) 0 ~ 16MB 老式 ISA 总线设备的 DMA 只能访问 16MB 以内内存
x86_64(64 位) 0 ~ 16MB 否(兼容用途) 现代 x86_64 设备支持 64 位 DMA 地址,但内核保留该区域兼容老旧硬件
ARM(32 位) 0 ~ 16MB/32MB(因芯片而异) 部分 ARM 芯片的 DMA 控制器只有 24/25 位地址线
ARM64(64 位) 无(或定义为 0 ~ 0) 现代 ARM64 设备的 DMA 控制器支持 64 位地址,无需 ZONE_DMA
RISC-V 架构设计时已支持 64 位 DMA 地址,无 ZONE_DMA 划分

32 位架构ZONE_DMA 是必需的,用于兼容老式 DMA 设备;

64 位架构ZONE_DMA 大多是兼容保留 ,现代设备已无需依赖该区域,内核更常用 ZONE_DMA32

在 Linux 5.0+ 内核中,ZONE_DMA 的使用场景已经非常有限,仅存两种情况:

  1. 老旧硬件驱动:为古董 ISA 总线设备的驱动提供内存,这类设备几乎已淘汰;
  2. 内核代码兼容 :部分老驱动代码仍保留 GFP_DMA 标志,内核通过 ZONE_DMA 确保这些驱动不报错。

注意事项

  1. 避免滥用 GFP_DMA :现代硬件驱动应优先使用 GFP_DMA32GFP_KERNELGFP_DMA 会限制内存分配范围,导致分配失败概率升高;
  2. 内存大小限制ZONE_DMA 只有 16MB,无法分配大内存块(如超过 1MB 的缓冲区);
  3. 32 位 vs 64 位内核差异 :在 64 位内核中,ZONE_DMA 的页帧会被同时映射到内核虚拟地址空间的 直接映射区 ,访问效率与 ZONE_NORMAL 一致;
  4. 与 eBPF 的关联 :eBPF 程序运行在内核态,若需访问 ZONE_DMA 的内存,需通过 bpf_skb_load_bytes() 等辅助函数,且必须确保对应的硬件 DMA 设备已初始化。

2. ZONE_DMA32

ZONE_DMA32 是 Linux 内核物理内存管理中面向现代 32 位 DMA 设备 的核心内存域,是 ZONE_DMA 的升级替代方案,在 32 位和 64 位内核中都占据重要地位。

ZONE_DMA32 是 Linux 内核为 32 位地址线宽度的 DMA 设备 划分的专用物理内存区域,核心特征如下:

  • 物理地址范围0 ~ 4GB(即 0x00000000 ~ 0xFFFFFFFF),覆盖完整的 32 位物理地址空间;
  • 目标设备:现代 PCI/PCIe 总线的 DMA 设备(如 SATA 磁盘、千兆网卡、USB 控制器等),这类设备的 DMA 控制器地址线宽度为 32 位;
  • 分配标志 :内核代码通过 GFP_DMA32 标志申请该区域内存;
  • 内核地位 :在 64 位内核中,ZONE_DMA32DMA 设备的默认内存域 ,取代了 ZONE_DMA 的主流地位。
  • 重叠区域说明0~16MB 物理内存同时属于 ZONE_DMAZONE_DMA32,内核分配时会优先满足 ZONE_DMA 的需求。

内存分配的核心流程

当内核代码通过 GFP_DMA32 标志申请内存时,分配器的执行逻辑如下:

  1. 优先扫描 ZONE_DMA32 :内核 Buddy 分配器会先遍历 ZONE_DMA32 的空闲页块链表,寻找满足大小的连续页帧;
  2. 降级分配策略 :若 ZONE_DMA32 无足够空闲内存,会根据 gfp_mask 的标志位判断是否允许从其他 zone 分配(如 ZONE_NORMAL),但32 位 DMA 设备无法访问 ZONE_NORMAL(>4GB),因此降级分配通常无实际意义;
  3. 页帧标记与跟踪 :分配的页帧会被标记为 PG_dma32,内核通过 struct pageflags 字段跟踪其所属 zone,避免被错误回收或复用。

ZONE_DMAZONE_DMA32 的核心区别

很多开发者会混淆 ZONE_DMAZONE_DMA32,两者的核心差异在于 DMA 设备的地址线宽度

特性 ZONE_DMA ZONE_DMA32
目标设备 24 位地址线的老式 DMA 设备 32 位地址线的现代 DMA 设备
物理地址范围 0 ~ 16MB(x86) 0 ~ 4GB(x86)
分配标志 GFP_DMA GFP_DMA32
适用场景 古董硬件(如 ISA 总线的磁盘控制器) 现代硬件(如 SATA 磁盘、PCIe 网卡)
64 位内核地位 兼容用途,极少使用 核心 DMA 内存区域,广泛使用

典型场景对比

  • GFP_DMA 申请内存:给 20 年前的 ISA 总线声卡分配 DMA 缓冲区;
  • GFP_DMA32 申请内存:给现代 SATA 磁盘分配 DMA 缓冲区。

注意事项

  1. 避免滥用 GFP_DMA32ZONE_DMA32 是稀缺资源(尤其是 64 位系统,4GB 以下内存占比低),非 DMA 设备的内存分配应使用 GFP_KERNEL
  2. 大内存块分配的限制ZONE_DMA32 中连续大内存块(如 128KB 以上)的分配成功率较低,建议使用 分散 - 聚合(scatter-gather)DMA 替代连续缓冲区;
  3. 64 位 DMA 设备的优化 :对于支持 64 位 DMA 地址的设备(如部分高端网卡),可直接使用 GFP_KERNELZONE_NORMAL 分配内存,无需局限于 ZONE_DMA32
  4. 与 eBPF 的关联 :eBPF 程序若需访问 DMA 缓冲区,需通过 bpf_probe_read_kernel() 辅助函数,且需确保缓冲区位于 ZONE_DMA32 时,内核已完成 DMA 映射。

理论地址范围与理论最大容量

ZONE_DMA32 的核心定义是 「物理地址 0~4GB 的内存区域」,因此理论最大容量的计算方式为:理论最大容量=4GB−ZONE_DMA 占用容量

  • 在 x86 架构中,ZONE_DMA 占用 0~16MB ,因此 ZONE_DMA32理论最大容量为 4GB - 16MB = 3984MB
  • 注意:0~16MB 区域是 ZONE_DMAZONE_DMA32重叠区域 ,该部分内存会优先分配给 ZONE_DMA 的需求,剩余部分才会纳入 ZONE_DMA32 的可分配池。

实际可分配大小的核心公式

ZONE_DMA32实际可分配内存 = 「硬件实际存在的 0~4GB 物理内存」 - 「内核预留内存」 - 「ZONE_DMA 已占用内存」 - 「碎片化不可用内存」拆解为 4 个关键因子:

  1. 硬件物理内存限制 :若机器物理内存 ≤4GB(如 2GB 内存),则 ZONE_DMA32 的最大可用容量就是 物理内存大小 - 16MB
  2. 内核预留内存:内核会在启动时预留一部分低地址内存(如 BIOS 保留区、显卡显存映射区),这部分内存不会被纳入任何 zone 的可分配池;
  3. ZONE_DMA 占用0~16MB 中被老式 DMA 设备占用的内存,无法被 ZONE_DMA32 使用;
  4. 内存碎片化:Buddy 分配器只能分配连续页块,碎片化的小页块可能因无法满足申请大小而被标记为不可用。

关键结论:无论物理内存多大,ZONE_DMA32 的实际可分配容量不会超过 4GB ,超出 4GB 的物理内存会被划分到 ZONE_NORMAL

3. ZONE_NORMAL(普通内存区)

  • 地址范围4GB ~ 最大物理内存
  • 核心用途 :内核和用户进程的主要内存区域,绝大多数内存分配都来自此区域;
  • 内核标志GFP_KERNEL(内核态分配)、GFP_USER(用户态分配)。

补充:ARM64 架构无 ZONE_DMA/ZONE_DMA32 划分,只有 ZONE_NORMAL,因为 ARM64 硬件原生支持 64 位物理地址。

对于 32 位 Linux 内核,物理内存超过 896MB 的部分称为 高端内存(HighMem),内核无法直接映射,需通过动态映射方式访问。

64 位 Linux 内核(x86_64/ARM64)无高端内存限制,因为虚拟地址空间足够大,可直接映射所有物理内存。

虚拟内存分布

Linux 中每个进程都有独立的虚拟地址空间 ,x86_64 架构默认的虚拟地址空间大小为 128TB(内核可配置),分为两大完全隔离的部分:

  • 用户态虚拟地址空间0x0000000000000000 ~ 0x00007FFFFFFFFFFF(共 128TB);
  • 内核态虚拟地址空间0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF(共 128TB)。

核心特性:所有进程的内核态虚拟地址空间完全相同(共享),用户态虚拟地址空间完全独立(隔离)。

用户态虚拟地址空间分布

用户态虚拟地址空间是进程的「私有领地」,进程只能访问自己的用户态虚拟地址,无法直接访问其他进程的用户态地址。x86_64 架构的用户态虚拟地址空间从低地址到高地址,分为 7 个核心区域:

区域 地址范围(x86_64) 核心功能 典型权限
空指针区(NULL 陷阱区) 0x0000000000000000 ~ 0x000000000000FFFF 禁止访问,防止空指针引用 无权限(---
程序代码段(.text) 紧随空指针区,由链接器决定 存储进程的可执行代码(二进制指令) 只读、可执行(r-x
程序数据段(.data + .bss) 紧随代码段 .data:存储已初始化的全局变量;.bss:存储未初始化的全局变量(初始化为 0) 读写、不可执行(rw-
堆(Heap) 从低地址向高地址动态增长 进程动态分配内存的区域(malloc/new 的底层) 读写、不可执行(rw-
内存映射区(MMAP) 从高地址向低地址动态增长 映射文件、共享内存、动态库(libc.so 等) 按需配置(如 r-x/rw-
栈(Stack) 从高地址向低地址动态增长 存储函数调用栈帧、局部变量、函数参数 读写、不可执行(rw-
命令行参数与环境变量 栈的最高地址附近 存储 argv(命令行参数)和 envp(环境变量) 读写、不可执行(rw-

关键特性解析

  1. 堆与栈的增长方向相反:堆是「向上增长」(地址变大),栈是「向下增长」(地址变小),中间是内存映射区,避免地址冲突;
  2. 栈的大小有限制 :默认大小为 8MB(可通过 ulimit -s 修改),超过会触发栈溢出(Stack Overflow);
  3. 内存映射区是核心 :动态库(如 libc.so)、文件映射(mmap 系统调用)、共享内存都在此区域,是进程间共享数据的核心方式;
  4. 所有区域的权限由内核严格控制:比如代码段是「只读可执行」,防止进程修改自身代码(安全防护);栈是「不可执行」,防止栈溢出攻击。

内核态虚拟地址空间分布(所有进程共享)

所有进程的内核态虚拟地址空间完全相同,内核通过此区域访问物理内存、硬件设备、内核数据结构。x86_64 架构的内核态虚拟地址空间分为 4 个核心区域:

1. 物理内存直接映射区(Linear Mapping)

  • 地址范围0xFFFF800000000000 ~ 0xFFFF87FFFFFF0000(对应物理内存 0 ~ 128GB,可配置);
  • 核心功能 :将物理内存一对一映射到内核虚拟地址,公式:

{内核虚拟地址} = {物理地址} + PAGE_OFFSET}

x86_64 架构的 PAGE_OFFSET = 0xFFFF800000000000

  • 核心价值 :内核通过此区域直接访问所有物理内存,无需动态映射,是内核最核心的内存区域。

2. vmalloc 区

  • 地址范围:紧随直接映射区之后;
  • 核心功能 :内核动态分配非连续物理内存 的区域,通过 vmalloc() 函数分配;
  • 特性:虚拟地址连续,物理地址不连续,适合分配大内存块(如内核模块)。

3. 设备映射区(I/O 映射)

  • 地址范围:紧随 vmalloc 区之后;
  • 核心功能:映射硬件设备的 I/O 地址空间(如显卡、网卡的寄存器),内核通过此区域访问硬件设备;
  • 实现方式 :通过 ioremap() 函数将设备的物理地址映射到内核虚拟地址。

4. 固定映射区(Fixmap)

  • 地址范围:内核态虚拟地址空间的最高地址附近;
  • 核心功能 :映射内核的特殊数据结构(如页表、struct page 数组),地址固定,访问速度快。

内核态和用户态内存的核心区别

维度 用户态内存 内核态内存
地址空间 每个进程独立,互不干扰 所有进程共享,完全相同
访问权限 进程只能访问自己的用户态内存,无法直接访问内核态内存 内核可以访问所有进程的用户态内存 + 内核态内存
分配函数 用户态:malloc/calloc/new;系统调用:brk/mmap 内核态:kmalloc/kzalloc/vmalloc/__get_free_pages
生命周期 随进程退出而释放 随内核启动而存在,随内核关闭而释放;或由内核手动释放
缺页处理 触发用户态缺页异常,内核分配物理内存并建立页表映射 触发内核态缺页异常,内核自行处理(如分配物理页)

内存映射机制

Linux 虚拟内存的核心是 「虚拟地址→物理地址的映射」 ,由 MMU + 多级页表 实现,x86_64 架构默认使用 4 级页表,内核 5.11+ 支持 5 级页表。

4 级页表的结构(x86_64)

x86_64 的 64 位虚拟地址被划分为 5 个部分,用于索引 4 级页表:

页表级别 虚拟地址位段 作用
PGD(页全局目录) 第 47~39 位(9 位) 索引 PGD 表项,指向 PUD 表的物理地址
PUD(页上级目录) 第 38~30 位(9 位) 索引 PUD 表项,指向 PMD 表的物理地址
PMD(页中间目录) 第 29~21 位(9 位) 索引 PMD 表项,指向 PTE 表的物理地址
PTE(页表项) 第 20~12 位(9 位) 索引 PTE 表项,指向物理页帧的物理地址
页内偏移 第 11~0 位(12 位) 物理页帧内的字节偏移,对应 4KB 页大小

地址转换流程(硬件级)

  1. 进程访问一个虚拟地址,CPU 将虚拟地址发送给 MMU;
  2. MMU 从 CPU 的 CR3 寄存器中读取当前进程的 PGD 表物理地址;
  3. MMU 用虚拟地址的 PGD 位段索引 PGD 表,得到 PUD 表的物理地址;
  4. 依次索引 PUD 表、PMD 表,最终得到 PTE 表项;
  5. PTE 表项中存储了物理页帧的物理地址,加上页内偏移,得到最终的物理地址;
  6. MMU 将物理地址发送给内存控制器,读取物理内存数据。

核心优化:CPU 内置 TLB(快表),缓存常用的虚拟地址→物理地址映射,避免每次都遍历多级页表,大幅提升地址转换速度。

核心要点

  1. Linux 内存的两大维度:物理内存(硬件载体,内核以页帧管理)、虚拟内存(进程视角,分为用户态 / 内核态);
  2. 用户态虚拟地址空间:每个进程独立,分为空指针区、代码段、数据段、堆、MMAP 区、栈、命令行参数区,堆向上增长,栈向下增长;
  3. 内核态虚拟地址空间:所有进程共享,分为直接映射区、vmalloc 区、设备映射区、固定映射区,直接映射区是核心;
  4. 虚拟→物理地址转换:由 MMU + 多级页表实现,x86_64 是 4 级页表,TLB 缓存提升转换速度;
  5. 核心区别:用户态内存进程私有,内核态内存进程共享;内核可访问所有内存,用户态只能访问自己的虚拟地址。

内存关键宏

  • CONFIG_PHYS_ADDR_T_BITS:编译时配置,决定内核支持的最大物理地址位宽和物理内存容量,需与硬件匹配;
  • PAGE_OFFSET:架构相关宏,定义内核虚拟地址空间的起始地址,是物理内存直接映射的核心公式参数;
  • 关联关系CONFIG_PHYS_ADDR_T_BITS 限制直接映射的物理内存范围,PAGE_OFFSET 定义映射的虚拟地址起始点,两者共同决定内核的物理内存访问能力;
  • 配置原则CONFIG_PHYS_ADDR_T_BITS 不超过 CPU 物理地址位宽,PAGE_OFFSET 不修改默认值。

CONFIG_PHYS_ADDR_T_BITSPAGE_OFFSET 是 Linux 内核中与物理地址 / 虚拟地址映射强相关的核心配置项,直接决定了内核对物理内存的寻址能力、虚拟地址空间的划分规则。

内核直接映射的物理内存上限由 CONFIG_PHYS_ADDR_T_BITS 决定,而映射后的虚拟地址上限则由 PAGE_OFFSET + 最大物理地址决定,以 x86_64 为例:

  • CONFIG_PHYS_ADDR_T_BITS=39(512GB),PAGE_OFFSET=0xFFFF800000000000
  • 直接映射虚拟地址上限 = 0xFFFF800000000000 + 0x7FFFFFFFFFF = 0xFFFF87FFFFFFFFFF
  • 超过 512GB 的物理内存,无法通过直接映射访问,需使用 vmalloc 动态映射。

CONFIG_PHYS_ADDR_T_BITS

物理地址宽度的内核配置

核心定义

CONFIG_PHYS_ADDR_T_BITS 是 Linux 内核的编译时配置项 ,用于指定内核支持的 物理地址总线宽度(单位:比特) ,本质是定义了 phys_addr_t 类型的有效位数。

  • 内核源码位置:arch/xxx/Kconfig(如 arch/x86/Kconfigarch/arm64/Kconfig);
  • 作用:决定内核能支持的最大物理内存容量,公式为:{最大支持物理内存} = 2^{{CONFIG_PHYS_ADDR_T_BITS}}

核心作用

  1. 限制物理内存寻址范围:内核无法识别超过该配置位宽的物理内存,例如配置为 39 位时,即使硬件插了 1TB 内存,内核也只能使用前 512GB;
  2. 优化内存管理数据结构 :更小的位宽可以减少页表项、struct page 等数据结构的内存占用;
  3. 硬件兼容性:必须与 CPU 的物理地址总线宽度匹配,否则会导致内存访问错误。

PAGE_OFFSET

PAGE_OFFSET 是 Linux 内核中一个编译期定义的常量宏 ,定义在 <asm/memory.h>(不同架构路径不同,如 arm64: arch/arm64/include/asm/memory.h);

PAGE_OFFSET 是内核虚拟地址空间的「起始边界」,也是内核态与用户态虚拟地址的「分水岭」

核心本质

32 位 / 64 位 Linux 系统,都会把整个 CPU 的虚拟地址空间做「一刀两断」的硬性划分:

  • 虚拟地址从 0PAGE_OFFSET - 1用户态虚拟地址空间,归属进程私有,每个进程独立可见;
  • 虚拟地址从 PAGE_OFFSET(2^BIT) - 1内核态虚拟地址空间,内核独占,所有进程进入内核态后,看到的内核虚拟地址空间完全相同。

核心价值

实现「内核态 + 用户态」虚拟地址空间的严格隔离;

实现 物理内存到内核虚拟地址的直接映射,这是内核高效访问物理内存的关键机制;

CPU 有 用户态(EL0)内核态(EL1/EL2/EL3) 特权级区分,PAGE_OFFSET地址层面固化这种隔离:

  • 用户进程的代码只能访问 [0, PAGE_OFFSET-1] 区间,永远无法直接触碰内核地址,杜绝用户态程序恶意篡改内核数据,是系统安全的第一道防线;
  • 只有触发系统调用 / 中断,CPU 切到内核态后,才能访问 [PAGE_OFFSET, TOP] 的内核地址空间。

内核地址空间「全局共享」的基础

内核虚拟地址空间对所有进程是全局唯一、完全共享 的:无论哪个进程进入内核态,看到的 PAGE_OFFSET 之后的地址映射关系都是相同的,内核的代码段、数据段、页表、驱动模块等,在所有进程中地址一致。

这个特性让内核的上下文切换成本极低,也让内核可以高效管理所有进程的资源。

物理内存映射的核心锚点

内核最核心的工作之一是管理物理内存,而 PAGE_OFFSET 是内核把物理内存直接映射到虚拟内存的「起始锚点」,这是内核能直接访问物理内存的核心机制。

不同架构的 PAGE_OFFSET 取值

架构 位数 虚拟地址空间划分 PAGE_OFFSET 取值 内核态虚拟地址范围
x86_64 64 用户态:0~0x7FFFFFFFFFFF(128TB)内核态:0xFFFF800000000000~0xFFFFFFFFFFFFFFFF(128TB) 0xFFFF800000000000 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF
ARM64 64 用户态:0~0x0000FFFFFFFFFFFF(48 位)内核态:0xFFFF000000000000~0xFFFFFFFFFFFFFFFF(48 位) 0xFFFF000000000000 0xFFFF000000000000 ~ 0xFFFFFFFFFFFFFFFF
32 位 x86 32 用户态:0~0xBFFFFFFF(3GB)内核态:0xC0000000~0xFFFFFFFF(1GB) 0xC0000000(3GB) 0xC0000000 ~ 0xFFFFFFFF

关键补充:x86_64 的 PAGE_OFFSET 可通过内核配置 CONFIG_PAGE_OFFSET 调整,例如部分内核将其设置为 0xFFFF880000000000,但主流默认值为 0xFFFF800000000000

虚拟地址空间的利用率

PAGE_OFFSET 划分的内核态虚拟地址空间大小,必须大于等于 CONFIG_PHYS_ADDR_T_BITS 支持的最大物理内存,否则会导致部分物理内存无法映射。

  • 例如 x86_64 内核态虚拟地址空间为 128TB,远大于 CONFIG_PHYS_ADDR_T_BITS=46 支持的 64TB,因此可以完全映射所有物理内存;
  • 若 32 位 x86 内核配置 CONFIG_PHYS_ADDR_T_BITS=36(64GB),但 PAGE_OFFSET 划分的内核态虚拟地址只有 1GB,则只能直接映射 1GB 物理内存,剩余 63GB 需通过 PAE 动态映射。
  • CONFIG_PHYS_ADDR_T_BITS 配置过大 :例如 CPU 仅支持 39 位物理地址,却配置为 46 位,会导致内核访问不存在的物理内存,触发 page fault
  • CONFIG_PHYS_ADDR_T_BITS 配置过小:例如硬件有 1TB 内存,却配置为 39 位(512GB),内核只能识别 512GB,剩余 512GB 无法使用;
  • PAGE_OFFSET 与虚拟地址空间不匹配 :例如修改 x86_64 的 PAGE_OFFSET0xFFFF000000000000,会导致直接映射区域与 vmalloc 区域重叠,内核崩溃。
  • ZONE_DMA32 的地址范围由 CONFIG_PHYS_ADDR_T_BITS 间接限制 :若 CONFIG_PHYS_ADDR_T_BITS 配置为 32 位,则物理内存最大为 4GB,ZONE_DMA32 覆盖全部物理内存;
  • 直接映射区包含所有内存域ZONE_DMA/ZONE_DMA32/ZONE_NORMAL 的物理内存,都会通过 PAGE_OFFSET 映射到内核虚拟地址空间,内核可以直接访问;
  • 高端内存的映射依赖配置 :若物理内存超过 CONFIG_PHYS_ADDR_T_BITS 限制,无法被映射,自然也无法划分到任何内存域。

物理内存的探测与初始化

内核启动时的第一个核心任务就是探测物理内存 ,该过程分为 架构相关的硬件探测内核通用的内存初始化:

硬件层面的内存探测(BIOS/UEFI 提供信息)

内核自身无法直接扫描物理内存,需依赖固件(BIOS/UEFI)提供的内存布局信息,核心机制是 e820 表(x86)设备树(ARM/ARM64)

x86 架构:e820 表

  • e820 表 :BIOS/UEFI 在启动时扫描物理内存,生成的内存区域描述表,包含每个内存区域的 起始物理地址、大小、类型
  • 区域类型
    • E820_TYPE_RAM:可用物理内存(内核可管理);
    • E820_TYPE_RESERVED:硬件保留内存(内核不可用);
    • E820_TYPE_ACPI:ACPI 表占用的内存;
  • 内核获取方式 :内核启动时通过 int 0x15 中断调用,从 BIOS 读取 e820 表,存储在 struct e820_entry 数组中。

ARM/ARM64 架构:设备树(DTB)

  • 无 BIOS/UEFI,内存布局信息通过 设备树(Device Tree Blob) 传递;
  • 设备树的 memory 节点包含物理内存的起始地址和大小,例如:

memory@80000000 {

device_type = "memory";

reg = <0x0 0x80000000 0x0 0x80000000>; // 起始地址 0x80000000,大小 2GB

};

内核层面的内存初始化

内核获取硬件内存布局后,执行通用初始化流程,核心目标是 构建物理页帧的管理数据结构

步骤 1:初始化 memblock 分配器

memblock:内核启动早期的临时内存分配器,用于在 Buddy 分配器初始化前分配内存;

核心操作

  • 内核根据 e820 表 / 设备树,将 E820_TYPE_RAM 区域标记为 memblock可用区域
  • 将保留区域(如内核镜像、硬件预留)标记为 memblock保留区域,避免被分配;

作用 :为后续 struct page 数组、页表等核心数据结构的创建提供内存。

步骤 2:创建 struct page 数组(mem_map

  • struct page:内核描述单个物理页帧的核心结构体,记录页帧的状态(空闲 / 已分配、所属进程、权限等);
  • mem_map :全局数组,每个物理页帧对应一个 struct page 实例,是内核管理物理内存的基石;

创建流程

  • 内核通过 memblock_alloc 申请连续内存,用于存储 mem_map 数组;
  • 数组大小 = 总物理页帧数 × sizeof(struct page)
  • 总物理页帧数 = 可用物理内存大小 / PAGE_SIZE(默认 4KB);
  • 初始化每个 struct page 的字段,标记页帧的所属内存域(ZONE_DMA/ZONE_DMA32/ZONE_NORMAL)。

步骤 3:初始化内存域(zone

内核根据物理地址范围,将 mem_map 中的页帧划分到不同的 内存域(struct zone ,对应你之前关注的 ZONE_DMA/ZONE_DMA32/ZONE_NORMAL

  1. 遍历所有可用物理页帧,根据物理地址判断所属 zone;
  2. 初始化每个 zone 的 free_area 链表(Buddy 分配器的空闲页块链表);
  3. 设置 zone 的 zone_start_pfn(起始页帧号)、spanned_pages(总页帧数)等核心字段。

步骤 4:初始化 Buddy 分配器

  • Buddy 分配器 :内核运行时的核心物理内存分配器,以 2^n 个页为单位管理空闲页块;
  • 初始化操作 :将每个 zone 中的空闲页帧,按页块大小(1 页、2 页、4 页...)挂载到对应的 free_area 链表中;
  • 核心作用 :响应内核态(kmalloc/__get_free_pages)和用户态(brk/mmap)的内存分配请求。

内核运行时:物理内存的管理与分配

内核启动完成后,通过 Buddy 分配器 + 页表映射 管理物理内存,核心流程分为 物理内存分配虚拟地址映射 两步。

物理内存分配流程

当内核或进程需要内存时,分配流程如下:

  1. 用户态进程分配 (如 malloc):
    • 进程通过 brk/mmap 系统调用,向内核申请虚拟地址空间;
    • 内核为虚拟地址空间分配对应的物理页帧(Buddy 分配器);
    • 内核更新进程的页表,建立 虚拟地址 → 物理地址 的映射。
  2. 内核态分配 (如驱动申请 DMA 缓冲区):
    • 驱动通过 kmalloc(小内存)/__get_free_pages(大内存)申请物理内存;
    • 内核根据分配标志(GFP_DMA32/GFP_KERNEL),从指定 zone 分配物理页帧;
    • 分配的物理页帧通过 PAGE_OFFSET 直接映射到内核虚拟地址空间,内核可直接访问。

物理内存的回收与复用

当物理内存不足时,内核会触发 内存回收机制

  1. 页缓存回收:释放文件系统的页缓存(如磁盘数据的内存缓存),优先回收非脏页;
  2. 匿名页交换 :将进程的匿名页(无文件映射的内存,如堆 / 栈)写入交换分区(swap),释放物理页帧;
  3. OOM 杀手:若内存回收失败,内核会触发 OOM(Out Of Memory)机制,杀死占用内存最多的进程,释放物理内存。

查看内核识别的实际物理内存

/proc/meminfo 是内核内存状态的核心导出接口,包含物理内存、交换内存的详细信息;

/proc/iomem 展示物理地址空间的详细布局(物理内存布局),包含可用内存和保留内存的地址范围;

/proc/zoneinfo 展示每个内存域的物理内存使用情况,可查看 ZONE_DMA32/ZONE_NORMAL 的实际大小;

相关推荐
东城绝神2 小时前
《Linux运维总结:基于ARM64+X86_64架构使用docker-compose一键离线部署MySQL8.0.43 NDB Cluster容器版集群》
linux·运维·mysql·架构·高可用·ndb cluster
creator_Li3 小时前
即时通讯项目--(1)环境搭建
linux·运维·ubuntu
Mr'liu4 小时前
MongoDB 7.0 副本集高可用部署
linux·mongodb
文静小土豆4 小时前
Rocky Linux 二进制 安装K8S-1.35.0高可用集群
linux·运维·kubernetes
暮云星影5 小时前
二、linux系统 应用开发:整体Pipeline流程
linux·arm开发
weixin_430750936 小时前
OpenMediaVault debian Linux安装配置企业私有网盘(三) 静态ip地址配置
linux·服务器·debian·nas·网络存储系统
403240736 小时前
[Jetson/Ubuntu 22.04] 解决挂载 exFAT 硬盘报错 “unknown filesystem type“ 及只读权限问题的终极指南
linux·运维·ubuntu
Source.Liu6 小时前
【沟通协作软件】使用 Rufus 制作 Ubuntu 启动盘的详细过程
linux·ubuntu
Love丶伊卡洛斯6 小时前
Ubuntu 部署 STUN服务端
linux·运维·ubuntu