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() 分离共享内存 ❌ 否(删除)

相关推荐
groundhappy3 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕3 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫5 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生5 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.116 小时前
linux基础IO
linux·运维·服务器
BelongPanda8 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯8 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998349 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei379 小时前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源
笑锝没心没肺9 小时前
yum update 报错:GPG key retrieval failed: [Errno 14] HTTPS Error 404 - Not Found
linux·运维·服务器