
前言:初学 Linux 操作系统时,虚拟地址空间 、页表 、进程内存分段 、fork 写时拷贝 总是晦涩难懂,很多人停留在概念背诵,看不懂底层数据结构关联。本文依托自己手绘的全流程思维导图,抛开枯燥课本定义,从一个全局变量int g_val=100入手,由浅入深串联进程地址隔离 、页表地址翻译 、父子进程 COW 、内核mm_struct/vm_area_struct结构体 、缺页中断 、mmap 映射整套逻辑,打通应用代码到操作系统内核的内存链路。
我们平时写 C 语言定义全局变量 、malloc 堆内存 、创建子进程 fork ,所有内存操作看似在连续内存上运行,实则全部依托操作系统虚拟内存机制做中转。弄懂虚拟地址空间,是理解进程隔离、内存管理、段错误、缺页异常的核心钥匙。

一.Linux 32位进程虚拟地址空间分布

1. 分区总览(32 位系统总虚拟地址 4GB)
高地址 1GB :内核空间(操作系统内核独占,用户进程无法直接访问)
低地址 3GB:用户空间(进程自身使用,分为从低→高地址的 5 大区域 + 命令行环境区)
2. 用户空间各段详解(从低地址→高地址排序)
正文代码段 (.text): 存放编译后的程序二进制指令,只读;
初始化数据段 (.data) :全局 / 静态已初始化变量;
未初始化数据段 (.bss) :全局 / 静态未初始化变量,程序运行前系统清零;
堆 (heap) :malloc/free/new/delete动态申请内存,地址向上增长(向高地址);
共享区 :动态库 (so)、mmap 映射文件的加载区域;
栈 (stack) :局部变量、函数参数、函数返回地址,地址向下增长(向低地址);
命令行参数 & 环境变量:程序启动时传入的 argv、环境变量,位于用户空间最高处。
3.验证
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
二、为什么需要虚拟地址空间?(本质是对物理地址的保护)
我们先从 3 个基础概念切入:
1.一个进程,一个虚拟地址空间:Linux 中每个进程被操作系统分配独立的虚拟地址空间,这是进程隔离的基石;
2.一套页表,绑定一个进程:页表是虚拟地址 ↔ 物理地址的翻译手册,每个进程自带专属页表;
3.页表 = 虚拟地址与物理地址的翻译中介 :CPU 访问变量时,通过页表完成虚拟地址到真实物理内存的转换;
以全局变量int g_val=100举例:代码里操作的是虚拟地址,CPU 借助页表查询,找到该虚拟地址对应的真实物理内存单元,完成读写。
关键特性:进程独立性
子进程通过 fork 创建后,初期并不会立刻复制物理内存,而是采用写时拷贝(COW):
1.子进程和父进程共享同一份物理页;
2.只有任意一方修改变量时,OS 才分配新物理内存、更新页表映射,这也是 fork 高效的核 心原因。
1.页表
1.什么是页表
页表 = 虚拟地址 ↔ 物理内存的翻译字典
操作系统维护,MMU 查表用
进程用的是【4G 虚拟地址】,CPU 通过MMU(硬件地址翻译器) + 页表 ,把虚拟地址翻译成真实内存条的物理地址。
3 个最核心要点
1.一张表,存着:虚拟页号 → 物理页号
2.每个进程一张独立页表(存在 mm_struct->pgd)
3.MMU 硬件靠它完成地址翻译

2. 为什么需要页表?
1.地址隔离安全:每个进程独立页表,进程只能访问自己的虚拟地址,无法篡改其他进程物理内存;
2.内存离散利用:物理内存碎片化时,通过页表把零散物理页拼成连续的虚拟地址空间;
3.缺页 + swap 实现内存扩容:借助页表标记页面在内存 / 磁盘,实现 "小物理内存跑大程序"

cpp
#include <stdio.h>
#include <unistd.h>
int gval = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("子: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());
sleep(1);
gval++;
}
}
else
{
while(1)
{
printf("父: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());
sleep(1);
}
}
return 0;
}

2.缺页中断
1.什么是缺页中断
你访问了一个虚拟地址,但这个地址目前还没有映射到物理内存 ,MMU 硬件发现后,立刻暂停程序,通知操作系统:"快去把数据加载到内存!"
这个 "通知 + 加载" 的过程,就叫 缺页中断。
2.为什么会有缺页中断?
因为 虚拟地址 ≠ 物理内存!
操作系统为了省内存、提高效率,不会把程序的所有代码、数据一次性全部加载进内存。
问题:大量进程如何高效管理虚拟地盘?
答案:内核用一套结构体数据结构统一管控,也就是 Linux 源码里的mm_struct,它是进程内存管家,串联起整个地址空间管理。
三、虚拟地址空间的区域划分:分段管理
进程的虚拟地址 不是混沌一片,被划分为多个固定区域:代码段 (Text)、数据段 (Data/BSS)、堆 (Heap)、栈 (Stack)、共享库区。
想要新增 / 修改内存区域,只需要修改对应区域的start/end边界值,内核就能识别新的地址区间。
1.内核管控结构体
1.mm_struct
mm_struct :整个进程虚拟内存的总描述符 ,**一个进程只有一个mm_struct,**记录代码段、数据段、堆、栈等全段边界、页表指针;
cpp
//1. .text代码段(正文)
unsigned long start_code;
unsigned long end_code;
//2. .data初始化全局数据段
unsigned long start_data;
unsigned long end_data;
//3. .bss未初始化全局区 → BSS在 end_data ~ start_brk
//4. 堆heap边界:start_brk堆起始,brk堆当前末尾(malloc/brk扩展改brk)
unsigned long start_brk;
unsigned long brk;
//5. mmap共享库/文件映射区起始基准
unsigned long mmap_base;
//6. 用户栈起始(栈从高地址往下增长)
unsigned long start_stack;
//7. 栈底:命令行参数argv区域
unsigned long arg_start;
unsigned long arg_end;
//8. 栈底:环境变量env区域
unsigned long env_start;
unsigned long env_end;
task_struct(进程 PCB,进程描述符)中内嵌struct mm_struct *mm指针,task_struct → mm_struct → vm_area_struct 构成三级内存管理链:
2.vm_area_struct
vm_area_struct :单个内存区域(VMA)的描述结构体 ,一个进程有多个 VMA(代码区一个、堆一个、栈一个、每个动态库一个),串联成链表。
cpp
struct vm_area_struct {
struct mm_struct *vm_mm; //归属哪个进程的地址空间(mm_struct)
unsigned long vm_start; //这片VMA区域的起始虚拟地址
unsigned long vm_end; //这片VMA区域的结束后第一个地址(左闭右开:[vm_start, vm_end))
//...剩下链表、权限、文件映射等成员
};
一张图看懂层级:
task_struct(进程) → mm_struct(内存总管) → 多个vm_area_struct(各内存分区)

内核靠三层结构体管理进程虚拟地址,MMU + 页表完成虚实地址转换,缺页中断实现按需分配物理内存,构成 Linux 虚拟地址空间整套机制。
