K02 —— 进程地址空间:虚拟地址的管理和 VMA

金句:每个进程的地址空间,都是一张独立的地图——而地图上每一个区域,都有一个 VMA 在描述它。


什么是 VMA(Virtual Memory Area)

VMA(Virtual Memory Area)是 Linux 内核用来描述进程虚拟地址空间中连续区域的数据结构。

Bash
VMA 的定义:

struct vm_area_struct {
    struct mm_struct *vm_mm;     // 所属进程
    unsigned long vm_start;     // 起始地址(包含)
    unsigned long vm_end;       // 结束地址(不包含)
    pgprot_t vm_page_prot;      // 页面保护(r/w/x)
    unsigned long vm_flags;     // 标志位(VM_READ/VM_WRITE/VM_EXEC等)

    struct file *vm_file;       // 如果是文件映射,记录文件
    unsigned long vm_pgoff;     // 文件内的页偏移

    struct vm_operations_struct *vm_ops; // 操作函数指针
    unsigned long vm_private_data;       // 私有数据
};

一个典型的进程地址空间包含多个 VMA:

  0x0000000000400000 ~ 0x0000000000401000 : r-xp (代码段,elf)
  0x0000000000600000 ~ 0x0000000000601000 : rw-p (数据段)
  0x00007f0000000000 ~ 0x00007f0000200000 : rw-p [heap] (堆)
  0x00007fffc8c00000 ~ 0x00007fffc8c20000 : rw-p [stack] (栈)

VMA 的标志位(vm_flags)

Bash
关键 vm_flags:

VM_READ / VM_WRITE / VM_EXEC:
  - 页面的读写执行权限
  - 影响 PTE 中的 protection 位

VM_SHARED / VM_PRIVATE:
  - SHARED:映射对所有进程可见(文件映射 / 共享内存)
  - PRIVATE:写时复制(fork 后的 COW)

VM_GROWSDOWN:
  - 栈区域,栈可以向下扩展(glibc 的动态栈扩展)

VM_IO:
  - 设备 I/O 区域(ioremap)

VM_MAYREAD / VM_MAYWRITE / VM_MAYEXEC:
  - 权限上限(允许设置的权限上限)
  - 即使 vm_flags 中没有 READ,但如果 MAYREAD=1,可以通过 mprotect 添加

VM_DENYWRITE:
  - 拒绝写入(映射文件时设置)

mmap 和 VMA 的关系

Bash
mmap 系统调用的作用:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)

1. 创建新的 VMA
   - 内核分配一个新的 VMA 结构
   - 设置 vm_start ~ vm_end(基于 addr, length, flags)
   - 设置 vm_flags(基于 prot, flags)

2. 建立地址映射
   - 如果是文件映射(MAP_FILE):vm_file = fd 对应的 file
   - 如果是匿名映射(MAP_ANON):vm_file = NULL
   - 物理页面按需分配(demand paging)

3. 返回起始地址
   - 内核返回 vm_start 给用户程序

例子:
  mmap(NULL, 4096, PROT_READ|PROT_WRITE,
       MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

  → 创建匿名私有映射
  → VMA: vm_start=0x7f..., vm_end=vm_start+4096
  → vm_flags: VM_READ|VM_WRITE|VM_PRIVATE
  → vm_file: NULL

Bash
VMA 链表和红黑树:

Linux 中每个进程的 mm_struct 维护 VMA:

struct mm_struct {
    struct vm_area_struct *mmap;          // VMA 链表(按地址排序)
    struct rb_root mm_rb;                  // VMA 红黑树(O(log n) 查找)
    unsigned long mmap_base;               // mmap 基址
    unsigned long start_code, end_code;    // 代码段范围
    unsigned long start_data, end_data;    // 数据段范围
    unsigned long start_brk, brk;          // 堆范围
    unsigned long start_stack;             // 栈顶
};

查找:
  - 快速查找(给定地址):从红黑树 O(log n)
  - 遍历所有 VMA:从链表(用于 proc 映射读取)

brk 和堆的管理

Bash
进程的堆(Heap):

堆的起始地址 = start_brk
堆的结束地址 = brk

程序启动时(C runtime):
  - start_brk = end_data(数据段结束)
  - brk = start_brk

malloc 底层:
  - < 128KB:brk() 扩展堆(调用 sbrk)
  - >= 128KB:mmap() 匿名映射(更灵活)

brk 系统调用:

int brk(void *addr);
  - 设置 brk = addr
  - 如果 addr > old brk:分配新的 VMA 或扩展现有 VMA
  - 如果 addr < old brk:收缩 VMA,释放页面

sbrk 系统调用:
  void *sbrk(int increment);
  - brk = brk + increment
  - 返回 old brk
  - glibc malloc 用 sbrk 实现小对象分配

堆的 VMA:
  0x0000000000600000 ~ 0x0000000000601000
  ↕
  start_brk                        brk
  VMA: rw-p, [heap]

栈的自动扩展

Bash
栈的 VMA 特点:

栈 VMA 有特殊标志 VM_GROWSDOWN:
  - 允许栈向下扩展(向低地址)
  - 访问页面不在 VMA 内时,触发 page fault

栈扩展流程:

  程序访问 0x00007fffc8c0f000(栈)
  → 不在任何 VMA 内
  → 找到相邻的 VMA(栈)vm_flags & VM_GROWSDOWN
  → 检查扩展是否合法(不超过 RLIMIT_STACK)
  → 扩展 VMA:vm_start -= PAGE_SIZE
  → 分配物理页面,建立页表映射
  → 返回

RLIMIT_STACK 控制栈的最大大小:
  $ ulimit -s      // 查看当前限制(KB)
  $ ulimit -s 8192 // 设置为 8MB

/proc/self/maps 详解

Bash
查看进程地址空间:

$ cat /proc/self/maps

00400000-00404000 r-xp /bin/cat                    ← 代码段(可执行,私有)
00600000-00601000 rw-p 00000000 00:00 0            ← 数据段(读写,私有)
7ffd8c000000-7ffd8c020000 rw-p 00000000 00:00 0    ← 堆(匿名,私有,可读写)
7ffd8c020000-7ffd8c0a0000 ---p 00000000 00:00 0    ← 保护页(guard page,无权限)
7fffc8c00000-7fffc8c02000 rw-p [stack]             ← 栈(可读写,私有)
ffff8800000000-ffff8880000000 r-xp 00000000 00:00 0 [vsyscall] ← vsyscall(64-bit)

格式:
  start-end    rwx p offset  major:minor  inode  pathname

字段解释:
  start ~ end:VMA 地址范围
  rwx:vm_flags(r=读, w=写, x=执行, p=private, s=shared)
  offset:文件映射的偏移
  major:minor:设备号(文件映射)
  inode:文件的 inode(文件映射)
  pathname:映射的文件或匿名区域

匿名映射:[heap], [stack], [vsyscall], [vdso], [vvar]

总结

  • VMA:Virtual Memory Area,描述进程虚拟地址空间中连续区域的数据结构
  • vm_flags:VM_READ/VM_WRITE/VM_EXEC/VM_SHARED/VM_PRIVATE/VM_GROWSDOWN 等
  • mmap:创建 VMA,建立虚拟地址到文件或匿名区域的映射
  • VMA 管理:链表(遍历)+ 红黑树(快速查找)
  • 堆(brk):start_brk ~ brk,sbrk/brk 系统调用,小对象用 brk,大对象用 mmap
  • :VM_GROWSDOWN 标志,访问越界自动扩展,受 RLIMIT_STACK 限制

下篇预告(K03):进程fork后,地址空间是怎么复制到子进程的?copy_page_range、COW(写时复制)、以及 vfork 的特殊处理。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年6月5日

作者

评论

发表评论

您的邮箱地址不会被公开。