Linux内存映射编程与VMA堆栈管理 - 第1节

1.1 什么是 VMA —— 虚拟内存区域

VMA(Virtual Memory Area,虚拟内存区域)是 Linux 内核中描述进程地址空间一段连续虚拟内存的基本数据结构。每个 VMA 对应一个独立的、具有相同访问权限和映射属性的虚拟地址区间。内核使用 struct vm_area_struct 结构体来管理每一个 VMA。

核心要点:VMA 是进程地址空间管理的“原子单元”,所有用户态内存操作(堆、栈、文件映射、匿名映射)最终都体现为对 VMA 链表的增删改查。

struct vm_area_struct 关键字段

字段类型说明
vm_startunsigned longVMA 起始虚拟地址(页对齐)
vm_endunsigned longVMA 结束虚拟地址(页对齐,不包含)
vm_flagsunsigned long访问权限与属性标志(读/写/执行/共享/私有等)
vm_filestruct file *映射的文件对象(匿名映射为 NULL)
vm_opsconst struct vm_operations_struct *VMA 操作函数表(open/close/fault 等)
vm_nextstruct vm_area_struct *链表指针,指向下一个 VMA
vm_mmstruct mm_struct *所属进程的内存描述符

1.2 进程地址空间布局

在 64 位 Linux 系统中,每个用户态进程拥有独立的 128 TB 虚拟地址空间(0x0000000000000000 ~ 0x00007fffffffffff)。内核按照固定布局将地址空间划分为若干区域,每个区域由一个或多个 VMA 构成。

典型 64 位进程地址空间布局

64 位 Linux 用户态进程地址空间布局 栈 (Stack) — 高地址向下增长 mmap 映射区域(文件映射 / 匿名映射 / 共享库) 堆 (Heap) — 低地址向上增长(brk/sbrk) BSS(未初始化全局/静态变量) 数据段(已初始化全局/静态变量) 代码段(.text — 只读可执行) 保留区 / 内核空间(用户态不可见) 高地址 低地址 栈向下 堆向上
布局规律:代码段位于最低地址,栈位于最高地址。堆和栈相向增长,中间大片区域供 mmap 映射使用。这种布局最大化利用了虚拟地址空间,减少了堆栈碰撞的概率。

1.3 mmap 系统调用概述

mmap 是 Linux 中最核心的内存映射系统调用,它允许用户态程序在进程地址空间中创建新的 VMA,将文件或设备内容映射到内存,或者分配匿名内存页。其原型定义在 <sys/mman.h> 中。

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
// 返回值:成功返回映射起始地址,失败返回 MAP_FAILED ((void *) -1)

参数详解

参数说明常用值
addr建议映射起始地址(内核可忽略,通常传 NULL)NULL 或指定地址
length映射长度(字节),必须大于 04096 的整数倍
prot内存保护标志PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE
flags映射类型与行为MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS / MAP_FIXED
fd文件描述符(匿名映射传 -1)有效 fd 或 -1
offset文件偏移量(页对齐)0 或页大小整数倍

mmap 的两种核心用途

  • 文件映射:将磁盘文件的部分内容直接映射到进程地址空间,读写内存即读写文件,无需 read/write 系统调用。
  • 匿名映射:不关联文件,分配一块初始化为零的物理内存页,常用于动态内存分配(malloc 大块内存底层即调用 mmap)。
注意:MAP_FIXED 标志会强制内核在指定地址精确创建映射,若该地址已有 VMA 则覆盖之。使用 MAP_FIXED 必须确保地址区间未被占用,否则可能导致进程崩溃。

1.4 mmap 与 VMA 的创建流程

当用户态调用 mmap 时,内核经过以下步骤完成 VMA 的创建与插入:

  1. 参数校验:检查 length、prot、flags 合法性,对齐 offset 到页边界。
  2. 地址选择:若 addr 为 NULL,内核在进程地址空间中查找空闲区间;若指定 MAP_FIXED,则直接使用 addr。
  3. VMA 分配:调用 kmem_cache_alloc 分配 struct vm_area_struct 实例。
  4. VMA 初始化:填充 vm_start、vm_end、vm_flags、vm_file、vm_ops 等字段。
  5. 插入红黑树与链表:将新 VMA 插入进程的 VMA 红黑树(快速查找)和双向链表(遍历)。
  6. 页表建立:若 flags 包含 MAP_POPULATE,立即分配物理页并建立页表映射;否则仅建立 VMA 结构,物理页在缺页时按需分配。
mmap 系统调用 → VMA 创建流程 用户态 mmap() 内核参数校验 查找空闲地址区间 分配并初始化 VMA 后续操作 • 插入红黑树 + 链表 • 建立页表(可选预分配) • 返回映射起始地址

1.5 本章小结

  • VMA 是进程地址空间的基本管理单元,每个 VMA 描述一段连续的、具有相同属性的虚拟内存。
  • 64 位 Linux 进程地址空间按“代码段 → 数据段 → BSS → 堆 → mmap 区域 → 栈”的经典布局排列。
  • mmap 系统调用是创建 VMA 的主要入口,支持文件映射和匿名映射两种模式。
  • 内核在 mmap 路径中完成参数校验、地址查找、VMA 分配初始化、数据结构插入等关键步骤。