linux VMA创建场景详解

在开发工作中,经常要与内存管理系统打交道,有一天突然想出一个问题:VMA会在什么时候创建。

本文档总结了工作中遇到的Linux内核中VMA(Virtual Memory Area)的创建场景,请大家补充遗漏情形。


目录

  • [1. mmap() - 最常见的VMA创建方式](#1. mmap() - 最常见的VMA创建方式)
  • [2. malloc() - 大内存分配](#2. malloc() - 大内存分配)
  • [3. 程序和库加载](#3. 程序和库加载)
  • [4. mprotect() - 改变保护属性](#4. mprotect() - 改变保护属性)
  • [5. 线程创建](#5. 线程创建)
  • [6. GPU/设备内存映射](#6. GPU/设备内存映射)
  • [7. 其他操作](#7. 其他操作)
  • [8. 实际示例](#8. 实际示例)

1. mmap() - 最常见的VMA创建方式

匿名映射(分配内存)

c 复制代码
// 创建匿名VMA
void *ptr = mmap(NULL, 4096, 
                 PROT_READ|PROT_WRITE,
                 MAP_PRIVATE|MAP_ANONYMOUS, 
                 -1, 0);

结果 :创建1个新VMA [ptr, ptr+4096),权限为可读写。

文件映射

c 复制代码
// 将文件映射到内存
int fd = open("file.dat", O_RDONLY);
void *mapped = mmap(NULL, 8192, 
                    PROT_READ,
                    MAP_PRIVATE, 
                    fd, 0);

结果:创建1个新VMA,关联到文件,权限为只读。

共享内存

c 复制代码
// 创建共享内存对象
int shm_fd = shm_open("/myshm", O_CREAT|O_RDWR, 0666);
ftruncate(shm_fd, 1024);
void *shm = mmap(NULL, 1024, 
                 PROT_READ|PROT_WRITE,
                 MAP_SHARED, 
                 shm_fd, 0);

结果:创建1个新VMA,多进程可共享。


2. malloc() - 大内存分配

glibc malloc行为

c 复制代码
// 小分配:使用现有堆VMA(不创建新VMA)
void *small = malloc(100);  // 从堆VMA分配

// 大分配:调用mmap创建新VMA(通常>128KB)
void *large = malloc(1024 * 1024);  // 创建新VMA

内核行为机制

复制代码
glibc malloc阈值:MMAP_THRESHOLD (默认128KB)

分配大小 <= 128KB:
    └─> brk()/sbrk() 扩展堆VMA
        └─> 不创建新VMA,只是扩展现有堆

分配大小 > 128KB:
    └─> mmap(MAP_PRIVATE|MAP_ANONYMOUS)
        └─> 创建独立的匿名VMA

示例对比

c 复制代码
#include <stdlib.h>
#include <stdio.h>

int main() {
    // 小分配:不创建新VMA
    for (int i = 0; i < 100; i++) {
        malloc(1000);  // 扩展堆VMA
    }
    
    // 大分配:每次创建新VMA
    for (int i = 0; i < 10; i++) {
        malloc(1024 * 1024);  // 创建10个新VMA
    }
    
    getchar();  // 暂停,可查看 /proc/<pid>/maps
    return 0;
}

3. 程序和库加载

这个是大家所熟悉的程序启动时,为各个段创建VMA 。

execve() - 加载可执行文件

c 复制代码
// 执行新程序
execve("./app", argv, envp);

创建的VMA

复制代码
1. Text段VMA   (r-xp): 可执行代码
2. Rodata段VMA (r--p): 只读数据
3. Data段VMA   (rw-p): 已初始化数据
4. BSS段VMA    (rw-p): 未初始化数据
5. 堆VMA       (rw-p): 动态分配
6. 栈VMA       (rw-p): 函数调用栈
7. VDSO VMA    (r-xp): 虚拟动态共享对象
8. VVAR VMA    (r--p): 内核变量

查看程序VMA布局

bash 复制代码
# 查看当前进程的内存映射
cat /proc/self/maps

# 示例输出:
00400000-00401000 r-xp 00000000 08:01 123  /bin/ls  # Text段
00600000-00601000 r--p 00000000 08:01 123  /bin/ls  # Rodata
00601000-00602000 rw-p 00001000 08:01 123  /bin/ls  # Data
01234000-01255000 rw-p 00000000 00:00 0    [heap]   # 堆
7ffff7a00000-...  r-xp 00000000 08:01 456  /lib/x86_64-linux-gnu/libc.so.6
7ffffffde000-...  rw-p 00000000 00:00 0    [stack]  # 栈

dlopen() - 动态加载库

c 复制代码
// 加载共享库
void *handle = dlopen("libfoo.so", RTLD_LAZY);

创建的VMA

  • 库的text段VMA(代码)
  • 库的data段VMA(数据)
  • 库的bss段VMA(未初始化数据)

4. mprotect() - 改变保护属性

VMA分割场景

c 复制代码
// 创建一个8KB的VMA
void *mem = mmap(NULL, 8192, 
                 PROT_READ|PROT_WRITE,
                 MAP_PRIVATE|MAP_ANONYMOUS, 
                 -1, 0);

// 此时:1个VMA [mem, mem+8192), RW权限
// 可通过 /proc/<pid>/maps 查看:
// 7f1234567000-7f1234569000 rw-p ...

// 改变后半部分的保护属性
mprotect(mem + 4096, 4096, PROT_READ);

// 结果:分割成2个VMA
// VMA1: [mem, mem+4096)        RW
// VMA2: [mem+4096, mem+8192)   R-

/proc/maps中的变化

bash 复制代码
# 执行mprotect前:
7f1234567000-7f1234569000 rw-p 00000000 00:00 0

# 执行mprotect后:
7f1234567000-7f1234568000 rw-p 00000000 00:00 0  # VMA1
7f1234568000-7f1234569000 r--p 00000000 00:00 0  # VMA2(注意权限变化)

常见分割场景

操作 分割原因 结果
mprotect(中间部分) 权限不同 3个VMA
madvise(MADV_DONTNEED) 建议不同 可能分割
mbind() NUMA策略不同 可能分割

5. 线程创建

pthread_create() 创建栈VMA

c 复制代码
#include <pthread.h>

void *thread_func(void *arg) {
    // 线程代码
    return NULL;
}

int main() {
    pthread_t thread;
    
    // 创建线程,默认栈大小8MB
    pthread_create(&thread, NULL, thread_func, NULL);
    
    // 结果:为新线程创建栈VMA
    // 7f1230000000-7f1230800000 rw-p ... [stack:12345]
    
    pthread_join(thread, NULL);
    return 0;
}

自定义栈大小

c 复制代码
pthread_t thread;
pthread_attr_t attr;

// 初始化线程属性
pthread_attr_init(&attr);

// 设置栈大小为2MB
pthread_attr_setstacksize(&attr, 2 * 1024 * 1024);

// 创建线程(创建2MB的栈VMA)
pthread_create(&thread, &attr, thread_func, NULL);

pthread_attr_destroy(&attr);

6. 设备内存映射

设备文件mmap

c 复制代码
// 打开GPU设备
int fd = open("/dev/kfd", O_RDWR);

// 映射设备内存到用户空间
void *gpu_mem = mmap(NULL, 4096,
                     PROT_READ|PROT_WRITE,
                     MAP_SHARED,
                     fd, gpu_offset);

// 创建设备映射VMA,标志包括:
// - VM_IO: I/O映射
// - VM_PFNMAP: 物理帧号映射

7. 其他操作

操作对比表

操作 创建VMA 说明 示例
brk()/sbrk() 扩展现有或新建 扩展堆VMA,堆不存在时创建 brk(current + 4096)
mremap() 可能 调整VMA大小,可能移动并创建新VMA mremap(addr, old_size, new_size, MREMAP_MAYMOVE)
fork() 复制所有VMA 子进程获得父进程VMA的副本(写时复制) fork()
madvise() 可能分割 某些建议会导致VMA分割 madvise(addr, len, MADV_DONTNEED)
munmap() 删除或分割 取消映射,可能分割VMA munmap(addr + 4096, 4096)
remap_file_pages() 已弃用,修改现有VMA N/A

brk()示例

c 复制代码
#include <unistd.h>

void *old_brk = sbrk(0);  // 获取当前堆顶

// 扩展堆(扩展现有堆VMA)
brk(old_brk + 4096);

// 如果堆VMA不存在,则创建新的堆VMA

mremap()示例

c 复制代码
// 创建初始VMA
void *addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
                  MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

// 扩展VMA到8KB
void *new_addr = mremap(addr, 4096, 8192, 
                        MREMAP_MAYMOVE);

// 如果原地扩展失败,创建新VMA并删除旧VMA

8. 实际示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

void print_maps(const char *label) {
    char cmd[512];
    printf("\n=== %s ===\n", label);
    // 匿名映射特征:设备号00:00,inode为0,无文件路径
    sprintf(cmd, "cat /proc/%d/maps | awk '/heap|stack|00:00 0[[:space:]]*$/' | tail -5", 
            getpid());
    system(cmd);
}

void show_address_vmas(unsigned long start_addr, unsigned long size) {
    char cmd[512];
    unsigned long end_addr = start_addr + size;
    // 显示包含该地址范围的所有VMA(通过十六进制匹配)
    sprintf(cmd, "cat /proc/%d/maps | awk 'BEGIN {s=strtonum(\"0x%lx\"); e=strtonum(\"0x%lx\")} "
                 "{split($1,a,\"-\"); vs=strtonum(\"0x\"a[1]); ve=strtonum(\"0x\"a[2]); "
                 "if((vs>=s && vs<e) || (ve>s && ve<=e) || (vs<=s && ve>=e)) print}'",
            getpid(), start_addr, end_addr);
    system(cmd);
}

int main() {
    char cmd[512];
    printf("进程ID: %d\n", getpid());
    
    // 1. 初始状态
    print_maps("1. 初始状态");
    
    // 2. 小内存分配(使用现有堆VMA)
    printf("\n执行: malloc(100)");
    void *small = malloc(100);
    print_maps("2. malloc(100) 后");
    
    // 3. 大内存分配(创建新VMA)
    printf("\n执行: malloc(1MB)");
    void *large = malloc(1024 * 1024);
    print_maps("3. malloc(1MB) 后 - 注意新增VMA");
    
    // 4. mmap匿名映射(创建新VMA)
    printf("\n执行: mmap(4KB)");
    void *mapped = mmap(NULL, 4096, 
                        PROT_READ|PROT_WRITE,
                        MAP_PRIVATE|MAP_ANONYMOUS, 
                        -1, 0);
    print_maps("4. mmap(4KB) 后 - 注意新增VMA");
    
    // 5. mprotect分割VMA
    printf("\n执行: mmap(8KB) 然后 mprotect(后半部分)");
    void *mem = mmap(NULL, 8192,
                     PROT_READ|PROT_WRITE,
                     MAP_PRIVATE|MAP_ANONYMOUS,
                     -1, 0);
    printf("\nmmap地址: %p\n", mem);
    
    printf("\nmmap后(1个VMA):\n");
    show_address_vmas((unsigned long)mem, 8192);
    
    mprotect(mem + 4096, 4096, PROT_READ);
    printf("\nmprotect后(应该分割成2个VMA,注意权限变化):\n");
    show_address_vmas((unsigned long)mem, 8192);
    
    // 暂停,允许手动检查
    printf("\n按Enter继续...");
    getchar();
    
    // 清理
    free(small);
    free(large);
    munmap(mapped, 4096);
    munmap(mem, 8192);
    
    return 0;
}

编译和运行

bash 复制代码
# 编译
gcc -o vma_demo vma_demo.c

# 运行
./vma_demo

# 在另一个终端实时查看
watch -n 1 "cat /proc/\$(pgrep vma_demo)/maps"

9. 核心总结

产生新VMA的根本原因

复制代码
1. 显式请求映射
   └─> mmap(), shm_open(), shmget()+shmat()

2. 大内存分配
   └─> malloc/new 超过阈值(通常128KB)
   └─> 触发内部mmap调用

3. 加载代码/数据
   └─> execve(): 加载程序
   └─> dlopen():  加载共享库

4. 属性不兼容
   └─> mprotect(): 权限变化导致分割
   └─> madvise():  某些建议导致分割

5. 线程栈
   └─> pthread_create(): 为每个线程创建栈VMA

6. 设备映射
   └─> 设备文件mmap(): GPU、DMA等

VMA创建频率排序

复制代码
最频繁:
1. malloc大内存      (应用程序频繁分配)
2. mmap              (文件I/O、匿名内存)
3. mprotect          (权限变化、JIT代码)

中等频率:
4. dlopen            (动态库加载)
5. pthread_create    (多线程程序)

较少频率:
6. execve            (程序启动时一次)
7. GPU设备映射       (GPU应用)

判断是否创建新VMA的方法

bash 复制代码
# 方法1: 比较/proc/maps
cat /proc/<pid>/maps > before.txt
# ... 执行操作 ...
cat /proc/<pid>/maps > after.txt
diff before.txt after.txt  # 新增行 = 新VMA

# 方法2: 使用strace追踪系统调用
strace -e mmap,mprotect,brk ./your_program

# 方法3: 使用bpftrace实时监控
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_mmap { 
    printf("mmap by PID %d\n", pid); 
}'

VMA属性标志

c 复制代码
// /proc/maps中的权限标志
r : PROT_READ    - 可读
w : PROT_WRITE   - 可写
x : PROT_EXEC    - 可执行
s : MAP_SHARED   - 共享映射
p : MAP_PRIVATE  - 私有映射(写时复制)

// 内核VMA标志(vm_flags)
VM_READ          - 可读
VM_WRITE         - 可写
VM_EXEC          - 可执行
VM_SHARED        - 共享
VM_MAYSHARE      - 可能共享
VM_GROWSDOWN     - 向下增长(栈)
VM_PFNMAP        - 物理帧号映射(设备)
VM_IO            - I/O映射
VM_DONTEXPAND    - 不能扩展
VM_DONTCOPY      - fork时不复制

调试技巧

bash 复制代码
# 1. 查看进程VMA数量
cat /proc/<pid>/maps | wc -l

# 2. 按类型统计VMA
cat /proc/<pid>/maps | awk '{print $6}' | sort | uniq -c

# 3. 查看最大的VMA
cat /proc/<pid>/maps | awk '{
    start=strtonum("0x"$1); 
    split($1,a,"-"); 
    end=strtonum("0x"a[2]); 
    size=end-start; 
    print size/1024/1024 " MB:", $0
}' | sort -rn | head

# 4. 实时监控VMA变化
watch -d -n 0.5 'cat /proc/<pid>/maps | wc -l'

# 5. 使用pmap查看内存映射摘要
pmap -x <pid>

性能考虑

过多VMA的影响

复制代码
1. 内核维护成本
   └─> 每个VMA: ~200字节内核内存

2. 查找性能下降
   └─> VMA存储在红黑树中
   └─> 查找时间: O(log n)

3. fork()开销增加
   └─> 需要复制所有VMA结构

4. /proc/maps读取变慢
   └─> 需要遍历所有VMA

最佳实践

复制代码
1. 合并相邻VMA
   └─> 使用相同权限的连续mmap

2. 避免过度mprotect
   └─> 减少不必要的VMA分割

3. 使用大页
   └─> 减少页表和TLB压力

4. 及时munmap
   └─> 释放不需要的VMA

附录:相关系统调用

VMA相关系统调用列表

系统调用 功能 是否创建VMA
mmap() 创建内存映射 ✅ 是
munmap() 删除内存映射 ❌ 否(删除)
mremap() 调整映射大小 🔄 可能
mprotect() 修改权限 🔄 可能(分割)
madvise() 内存使用建议 🔄 可能(分割)
mlock()/mlockall() 锁定内存 ❌ 否
brk()/sbrk() 调整堆 🔄 扩展现有
shmat() 附加共享内存 ✅ 是
shmdt() 分离共享内存 ❌ 否(删除)

相关推荐
扛枪的书生2 小时前
Ansible 学习总结
linux
赵民勇2 小时前
cut命令详解
linux·shell
闻道且行之3 小时前
Linux|CUDA与cuDNN下载安装全指南:默认/指定路径双方案+多CUDA环境一键切换
linux·运维·服务器
Ahtacca3 小时前
Linux环境下前后端分离项目(Spring Boot + Vue)手动部署全流程指南
linux·运维·服务器·vue.js·spring boot·笔记
_w_z_j_3 小时前
Linux----Socket编程基础
linux·运维·服务器
默|笙3 小时前
【Linux】进程控制(3)进程程序替换
android·linux·运维
老前端的功夫4 小时前
TypeScript 全局类型声明:declare关键字的深度解析与实战
linux·前端·javascript·ubuntu·typescript·前端框架
赵民勇4 小时前
join命令使用指南与技巧
linux·shell
工业HMI实战笔记4 小时前
【拯救HMI】让老设备重获新生:HMI低成本升级与功能拓展指南
linux·运维·网络·信息可视化·人机交互·交互·ux