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 的使用场景已经非常有限,仅存两种情况:
- 老旧硬件驱动:为古董 ISA 总线设备的驱动提供内存,这类设备几乎已淘汰;
- 内核代码兼容 :部分老驱动代码仍保留
GFP_DMA标志,内核通过ZONE_DMA确保这些驱动不报错。
注意事项
- 避免滥用
GFP_DMA:现代硬件驱动应优先使用GFP_DMA32或GFP_KERNEL,GFP_DMA会限制内存分配范围,导致分配失败概率升高; - 内存大小限制 :
ZONE_DMA只有 16MB,无法分配大内存块(如超过 1MB 的缓冲区); - 32 位 vs 64 位内核差异 :在 64 位内核中,
ZONE_DMA的页帧会被同时映射到内核虚拟地址空间的 直接映射区 ,访问效率与ZONE_NORMAL一致; - 与 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_DMA32是 DMA 设备的默认内存域 ,取代了ZONE_DMA的主流地位。 - 重叠区域说明 :
0~16MB物理内存同时属于ZONE_DMA和ZONE_DMA32,内核分配时会优先满足ZONE_DMA的需求。
内存分配的核心流程
当内核代码通过 GFP_DMA32 标志申请内存时,分配器的执行逻辑如下:
- 优先扫描
ZONE_DMA32:内核 Buddy 分配器会先遍历ZONE_DMA32的空闲页块链表,寻找满足大小的连续页帧; - 降级分配策略 :若
ZONE_DMA32无足够空闲内存,会根据gfp_mask的标志位判断是否允许从其他 zone 分配(如ZONE_NORMAL),但32 位 DMA 设备无法访问ZONE_NORMAL(>4GB),因此降级分配通常无实际意义; - 页帧标记与跟踪 :分配的页帧会被标记为
PG_dma32,内核通过struct page的flags字段跟踪其所属 zone,避免被错误回收或复用。
ZONE_DMA 与 ZONE_DMA32 的核心区别
很多开发者会混淆 ZONE_DMA 和 ZONE_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 缓冲区。
注意事项
- 避免滥用
GFP_DMA32:ZONE_DMA32是稀缺资源(尤其是 64 位系统,4GB 以下内存占比低),非 DMA 设备的内存分配应使用GFP_KERNEL; - 大内存块分配的限制 :
ZONE_DMA32中连续大内存块(如 128KB 以上)的分配成功率较低,建议使用 分散 - 聚合(scatter-gather)DMA 替代连续缓冲区; - 64 位 DMA 设备的优化 :对于支持 64 位 DMA 地址的设备(如部分高端网卡),可直接使用
GFP_KERNEL从ZONE_NORMAL分配内存,无需局限于ZONE_DMA32; - 与 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_DMA和ZONE_DMA32的重叠区域 ,该部分内存会优先分配给ZONE_DMA的需求,剩余部分才会纳入ZONE_DMA32的可分配池。
实际可分配大小的核心公式
ZONE_DMA32 的实际可分配内存 = 「硬件实际存在的 0~4GB 物理内存」 - 「内核预留内存」 - 「ZONE_DMA 已占用内存」 - 「碎片化不可用内存」拆解为 4 个关键因子:
- 硬件物理内存限制 :若机器物理内存 ≤4GB(如 2GB 内存),则
ZONE_DMA32的最大可用容量就是物理内存大小 - 16MB; - 内核预留内存:内核会在启动时预留一部分低地址内存(如 BIOS 保留区、显卡显存映射区),这部分内存不会被纳入任何 zone 的可分配池;
- ZONE_DMA 占用 :
0~16MB中被老式 DMA 设备占用的内存,无法被ZONE_DMA32使用; - 内存碎片化: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-) |
关键特性解析
- 堆与栈的增长方向相反:堆是「向上增长」(地址变大),栈是「向下增长」(地址变小),中间是内存映射区,避免地址冲突;
- 栈的大小有限制 :默认大小为 8MB(可通过
ulimit -s修改),超过会触发栈溢出(Stack Overflow); - 内存映射区是核心 :动态库(如
libc.so)、文件映射(mmap系统调用)、共享内存都在此区域,是进程间共享数据的核心方式; - 所有区域的权限由内核严格控制:比如代码段是「只读可执行」,防止进程修改自身代码(安全防护);栈是「不可执行」,防止栈溢出攻击。
内核态虚拟地址空间分布(所有进程共享)
所有进程的内核态虚拟地址空间完全相同,内核通过此区域访问物理内存、硬件设备、内核数据结构。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 页大小 |
地址转换流程(硬件级)
- 进程访问一个虚拟地址,CPU 将虚拟地址发送给 MMU;
- MMU 从 CPU 的
CR3寄存器中读取当前进程的 PGD 表物理地址; - MMU 用虚拟地址的 PGD 位段索引 PGD 表,得到 PUD 表的物理地址;
- 依次索引 PUD 表、PMD 表,最终得到 PTE 表项;
- PTE 表项中存储了物理页帧的物理地址,加上页内偏移,得到最终的物理地址;
- MMU 将物理地址发送给内存控制器,读取物理内存数据。
核心优化:CPU 内置 TLB(快表),缓存常用的虚拟地址→物理地址映射,避免每次都遍历多级页表,大幅提升地址转换速度。
核心要点
- Linux 内存的两大维度:物理内存(硬件载体,内核以页帧管理)、虚拟内存(进程视角,分为用户态 / 内核态);
- 用户态虚拟地址空间:每个进程独立,分为空指针区、代码段、数据段、堆、MMAP 区、栈、命令行参数区,堆向上增长,栈向下增长;
- 内核态虚拟地址空间:所有进程共享,分为直接映射区、vmalloc 区、设备映射区、固定映射区,直接映射区是核心;
- 虚拟→物理地址转换:由 MMU + 多级页表实现,x86_64 是 4 级页表,TLB 缓存提升转换速度;
- 核心区别:用户态内存进程私有,内核态内存进程共享;内核可访问所有内存,用户态只能访问自己的虚拟地址。
内存关键宏
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_BITS 和 PAGE_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/Kconfig、arch/arm64/Kconfig); - 作用:决定内核能支持的最大物理内存容量,公式为:{最大支持物理内存} = 2^{{CONFIG_PHYS_ADDR_T_BITS}}
核心作用
- 限制物理内存寻址范围:内核无法识别超过该配置位宽的物理内存,例如配置为 39 位时,即使硬件插了 1TB 内存,内核也只能使用前 512GB;
- 优化内存管理数据结构 :更小的位宽可以减少页表项、
struct page等数据结构的内存占用; - 硬件兼容性:必须与 CPU 的物理地址总线宽度匹配,否则会导致内存访问错误。
PAGE_OFFSET
PAGE_OFFSET 是 Linux 内核中一个编译期定义的常量宏 ,定义在 <asm/memory.h>(不同架构路径不同,如 arm64: arch/arm64/include/asm/memory.h);
PAGE_OFFSET 是内核虚拟地址空间的「起始边界」,也是内核态与用户态虚拟地址的「分水岭」。
核心本质
32 位 / 64 位 Linux 系统,都会把整个 CPU 的虚拟地址空间做「一刀两断」的硬性划分:
- 虚拟地址从
0到PAGE_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_OFFSET为0xFFFF000000000000,会导致直接映射区域与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:
- 遍历所有可用物理页帧,根据物理地址判断所属 zone;
- 初始化每个 zone 的
free_area链表(Buddy 分配器的空闲页块链表); - 设置 zone 的
zone_start_pfn(起始页帧号)、spanned_pages(总页帧数)等核心字段。
步骤 4:初始化 Buddy 分配器
- Buddy 分配器 :内核运行时的核心物理内存分配器,以
2^n个页为单位管理空闲页块; - 初始化操作 :将每个 zone 中的空闲页帧,按页块大小(1 页、2 页、4 页...)挂载到对应的
free_area链表中; - 核心作用 :响应内核态(
kmalloc/__get_free_pages)和用户态(brk/mmap)的内存分配请求。
内核运行时:物理内存的管理与分配
内核启动完成后,通过 Buddy 分配器 + 页表映射 管理物理内存,核心流程分为 物理内存分配 和 虚拟地址映射 两步。
物理内存分配流程
当内核或进程需要内存时,分配流程如下:
- 用户态进程分配 (如
malloc):- 进程通过
brk/mmap系统调用,向内核申请虚拟地址空间; - 内核为虚拟地址空间分配对应的物理页帧(Buddy 分配器);
- 内核更新进程的页表,建立 虚拟地址 → 物理地址 的映射。
- 进程通过
- 内核态分配 (如驱动申请 DMA 缓冲区):
- 驱动通过
kmalloc(小内存)/__get_free_pages(大内存)申请物理内存; - 内核根据分配标志(
GFP_DMA32/GFP_KERNEL),从指定 zone 分配物理页帧; - 分配的物理页帧通过
PAGE_OFFSET直接映射到内核虚拟地址空间,内核可直接访问。
- 驱动通过
物理内存的回收与复用
当物理内存不足时,内核会触发 内存回收机制:
- 页缓存回收:释放文件系统的页缓存(如磁盘数据的内存缓存),优先回收非脏页;
- 匿名页交换 :将进程的匿名页(无文件映射的内存,如堆 / 栈)写入交换分区(
swap),释放物理页帧; - OOM 杀手:若内存回收失败,内核会触发 OOM(Out Of Memory)机制,杀死占用内存最多的进程,释放物理内存。
查看内核识别的实际物理内存
/proc/meminfo 是内核内存状态的核心导出接口,包含物理内存、交换内存的详细信息;
/proc/iomem 展示物理地址空间的详细布局(物理内存布局),包含可用内存和保留内存的地址范围;
/proc/zoneinfo 展示每个内存域的物理内存使用情况,可查看 ZONE_DMA32/ZONE_NORMAL 的实际大小;