Linux系统篇(十五)——进程(四):一文吃透 Linux 虚拟地址空间:从页表映射到内核结构体全链路拆解

发布时间:2026/7/13 16:31:32
Linux系统篇(十五)——进程(四):一文吃透 Linux 虚拟地址空间:从页表映射到内核结构体全链路拆解 前言初学 Linux 操作系统时虚拟地址空间、页表、进程内存分段、fork 写时拷贝总是晦涩难懂很多人停留在概念背诵看不懂底层数据结构关联。本文依托自己手绘的全流程思维导图抛开枯燥课本定义从一个全局变量int g_val100入手由浅入深串联进程地址隔离、页表地址翻译、父子进程 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.验证#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_val100举例代码里操作的是虚拟地址CPU 借助页表查询找到该虚拟地址对应的真实物理内存单元完成读写。关键特性进程独立性子进程通过 fork 创建后初期并不会立刻复制物理内存而是采用写时拷贝COW1.子进程和父进程共享同一份物理页2.只有任意一方修改变量时OS 才分配新物理内存、更新页表映射这也是 fork 高效的核 心原因。1.页表1.什么是页表页表 虚拟地址 ↔ 物理内存的翻译字典操作系统维护MMU 查表用进程用的是【4G 虚拟地址】CPU 通过MMU硬件地址翻译器 页表把虚拟地址翻译成真实内存条的物理地址。3 个最核心要点1.一张表存着虚拟页号 → 物理页号2.每个进程一张独立页表存在 mm_struct-pgd3.MMU 硬件靠它完成地址翻译2. 为什么需要页表1.地址隔离安全每个进程独立页表进程只能访问自己的虚拟地址无法篡改其他进程物理内存2.内存离散利用物理内存碎片化时通过页表把零散物理页拼成连续的虚拟地址空间3.缺页 swap 实现内存扩容借助页表标记页面在内存 / 磁盘实现 “小物理内存跑大程序”​ #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_structmm_struct整个进程虚拟内存的总描述符一个进程只有一个mm_struct记录代码段、数据段、堆、栈等全段边界、页表指针//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_structvm_area_struct单个内存区域VMA的描述结构体一个进程有多个 VMA代码区一个、堆一个、栈一个、每个动态库一个串联成链表。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 虚拟地址空间整套机制。