

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生
✨专注 C/C++ Linux 数据结构 算法竞赛 AI
🏞️志同道合的人会看见同一片风景!
👇点击进入作者专栏:
🌟《算法画解》算法相关题目点击即可进入实操🌟
感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!
文章目录
- 📖前言
- [🔧 一、地址空间引入与验证](#🔧 一、地址空间引入与验证)
- [🚗 二、进程地址空间](#🚗 二、进程地址空间)
- [💾 三、虚拟内存管理](#💾 三、虚拟内存管理)
-
- [1. 核心数据结构:mm_struct](#1. 核心数据结构:mm_struct)
- 2.虚拟内存区域:vm_area_struct
- [3. 地址空间的精细划分](#3. 地址空间的精细划分)
- 4.虚拟地址的组织方式
- [❓ 四、为什么需要虚拟地址空间?](#❓ 四、为什么需要虚拟地址空间?)
-
- [1. 直接使用物理内存的"黑暗时代"](#1. 直接使用物理内存的“黑暗时代”)
- [2. 三大致命问题](#2. 三大致命问题)
- [3. 虚拟地址空间:操作系统的"魔法"](#3. 虚拟地址空间:操作系统的“魔法”)
- [4. 虚拟内存的三大哲学](#4. 虚拟内存的三大哲学)
- [5. 虚拟地址空间的现实价值](#5. 虚拟地址空间的现实价值)
- 🎯总结:虚拟地址空间的革命性意义
📖前言
当我们用C语言写下 &a 获取变量地址,或通过 malloc 申请内存时,你是否曾好奇这些地址背后隐藏着怎样的秘密?在操作系统精密的舞台背后,每个进程都活在自己精心构建的"幻境"之中------拥有完整而独立的4GB地址空间,却对物理内存的实际排布一无所知。这并非欺骗,而是现代计算得以安全、高效运行的核心智慧。虚拟地址空间如同为每个进程定制的平行宇宙,在提供绝对隔离与安全的同时,巧妙地实现了内存共享、延迟分配与写时拷贝。本文将揭开这层幻象的面纱,从地址空间的引入与验证出发,深入剖析分页机制、写时拷贝、内核数据结构,最终揭示虚拟内存设计的深层哲学与革命意义------这是一场从物理束缚到虚拟自由的操作系统进化史。
🔧 一、地址空间引入与验证
1.引入
在学习C语⾔,C++的时候,大家可能遇到或者画过这样的空间布局图,来帮助理解。

用代码来观察:
c
#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;
}

思考:程序地址空间是内存吗?
2.验证
我们来做如下验证:
bash
#include <stdio.h>
#include <stdlib.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, getpid(), getppid());
sleep(1);
gval++;
}
}
else
{
while(1)
{
printf("父:gval: %d, &gval: %p, pid : %d, ppid : %d\n", gval, &gval, getpid, getpid(), getppid());
sleep(1);
gval++;
}
}
return 0;
}

我们发现父子进程,内容不一样,但输出地址一致,这与内存的存储矛盾。
变量内容不⼀样,所以父子进程输出的变量绝对不是同⼀个变量 但地址值是⼀样的,说明该地址绝对不是物理地址! 在Linux地址下,这种地址叫做虚拟地址 我们在用C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,用户⼀概看不到,由OS统⼀管理。
OS必须负责将虚拟地址 转化成物理地址 。
结论:程序地址空间不是内存,它真正应该叫做进程(虚拟)地址空间,是系统的概念,而不是语言层的概念。
虚拟地址是操作系统为了进程而虚拟化出来的一套地址,所以我们学习的语言,写的程序是一个个进程,用到的都是虚拟地址,由操作系统管理转化好(映射关系),不影响我们上层的使用,可以帮助我们更好理解。
🚗 二、进程地址空间
1.分页与虚拟地址空间

地址空间与进程结构:
每个进程有一个 task_struct(进程控制块)。 task_struct 包含 mm_struct,用于描述进程的地址空间布局。
每个进程看到的虚拟地址空间是独立且结构相同的,包含: 栈区 共享区 堆区 未初始化数据区 已初始化数据区 代码区
物理内存与映射:
物理内存中存放了进程实际的代码和数据。
每个进程有独立的页表,由 MMU(内存管理单元)负责虚拟地址到物理地址的转换。
父子进程中的同一个全局变量 g_val:
虚拟地址相同
物理地址不同(页表映射到不同物理页)
多个进程可以通过相同的虚拟地址访问不同的物理内存,其机制是通过各自独立的页表映射实现的。
一个进程,一个虚拟地址空间,一套页表,页表是用来做虚拟地址和物理地址映射的。用户拿到地址就能直接访问。
2.写时拷贝
一种内存管理优化技术:创建新进程时不立即复制内存,而是共享同一份数据,直到需要修改时才真正复制。
工作原理(三步走)
1️⃣ 共享开始
当父进程创建子进程时:
子进程获得父进程内存的"视图"
物理内存并未复制
两个进程的页表指向相同的物理页
这些共享页被标记为只读
父进程:[虚拟地址A] → [物理页X(数据=0)]
子进程:[虚拟地址A] → [物理页X(数据=0)] ← 同一物理页!
2️⃣ 写时触发
当子进程尝试修改数据时:
c
g_val = 100; // 试图修改
CPU检测到"只读页上的写操作",触发页面异常,操作系统介入处理。
3️⃣ 延迟复制
操作系统此时才执行真正的复制:
分配新的物理页Y
复制X的内容到Y
更新子进程页表:A → Y
将新页设为可写
恢复子进程执行
为何高效?
| 场景 | 传统复制 | 写时拷贝 |
|---|---|---|
| 进程创建 | 立即复制所有内存 | 几乎零延迟 |
| 内存占用 | 双倍内存 | 共享未修改部分 |
| 实际开销 | 总是全量复制 | 只为修改的数据付费 |
现实应用:
Linux的fork() - 快速进程创建
虚拟机快照 - 瞬间保存状态
Docker镜像 - 分层文件系统
数据库 - 实现MVCC多版本控制
......
一句话总结:
"先共享,后买单"------写时拷贝让进程复制变得轻盈,只为实际发生的修改支付内存和时间成本。
💾 三、虚拟内存管理
1. 核心数据结构:mm_struct

在 Linux 内核中,每个进程都有一个"地址空间管理员"------mm_struct(内存描述符)。它记录着进程虚拟地址空间的所有信息。
c
struct task_struct {
struct mm_struct *mm; // 指向进程的虚拟地址空间
struct mm_struct *active_mm; // 内核线程使用的备用地址空间
// ...
};
关键点:
每个进程都有独立的 mm_struct → 独立的虚拟地址空间。
普通进程的 mm 指向自己的地址空间。
内核线程没有用户空间,mm 为 NULL,但可以借用其他进程的内核映射。
2.虚拟内存区域:vm_area_struct
地址空间不是一整块,而是由多个"功能区"组成,每个区用 vm_area_struct(VMA)表示:
c
struct vm_area_struct {
unsigned long vm_start; // 区域起始地址
unsigned long vm_end; // 区域结束地址
struct vm_area_struct *vm_next, *vm_prev; // 链表连接
struct rb_node vm_rb; // 红黑树节点
struct mm_struct *vm_mm; // 所属的地址空间
unsigned long vm_flags; // 权限标志(可读、可写、可执行)
// ...
};
3. 地址空间的精细划分
在 mm_struct 中,操作系统为每个标准段都记录了明确的边界:
c
struct mm_struct {
// 核心功能区边界
unsigned long start_code, end_code; // 代码段
unsigned long start_data, end_data; // 数据段
unsigned long start_brk, brk; // 堆的起始和当前顶部
unsigned long start_stack; // 栈起始地址
// 参数和环境变量
unsigned long arg_start, arg_end; // 命令行参数
unsigned long env_start, env_end; // 环境变量
// 管理结构
struct vm_area_struct *mmap; // 指向VMA链表
struct rb_root mm_rb; // 红黑树根节点
// ...
};

4.虚拟地址的组织方式
Linux 使用两种数据结构高效管理 VMA:
方式一:链表(小规模)
当进程的虚拟区域较少时,使用单向链表连接所有 VMA。
mmap → VMA1 → VMA2 → VMA3 → NULL
方式二:红黑树(大规模)
当虚拟区域很多时,使用红黑树进行快速查找。
为什么要两种结构?
链表:适合遍历所有区域(如缺页异常处理)
红黑树:适合快速查找特定地址所在的区域(时间复杂度 O(log n))

❓ 四、为什么需要虚拟地址空间?
1. 直接使用物理内存的"黑暗时代"
在早期计算机中,程序直接操作物理内存地址。想象一下:128MB 内存,运行两个程序 A(10MB) 和 B(110MB)。操作系统只能这样分配:
物理内存布局:
0-10MB\] 程序A \[10-120MB\] 程序B \[120-128MB\] 空闲 看起来合理?问题大了! ### 2. 三大致命问题 ❌ 安全问题:人人都是"超级管理员" 每个程序都能访问任意物理内存,包括: > 操作系统内核代码 > > 其他进程的敏感数据 > > 硬件设备的映射区域 现实比喻:就像每个租客都有整栋大楼的万能钥匙,可以随意进出其他房间,甚至物业办公室。 ❌ 地址不确定问题:每次搬家都是"开盲盒" 程序编译时假设自己在地址 0 开始运行,但实际内存可能被占用: 第一次运行:加载到物理地址 0x00000000 ✅ 第二次运行:前10MB已被占用,只能加载到 0x00A00000 ❓ 第三次运行:前50MB被占用,加载到 0x03200000 ❓ 程序员崩溃了:"我的变量地址怎么天天变?!" ❌ 效率问题:搬家要"带全部家当" > 内存不足时,需要将不常用的程序暂存到磁盘(交换分区)。 > > 物理内存时代:整个程序必须一起搬走 > > 10MB 程序 × 频繁交换 = 磁盘 I/O 灾难 > > 现实比喻:每次出差都要搬走整个家,而不是只带行李箱。 ### 3. 虚拟地址空间:操作系统的"魔法" 虚拟地址空间通过页表机制,完美解决了所有问题: ✅ 安全隔离:每户独立的"平行宇宙" > 进程A视角:0x00000000-0xFFFFFFFF(完整4GB) 进程B视角:0x00000000-0xFFFFFFFF(完整4GB) > > 物理内存:A、B、内核数据分散在不同位置 每个进程都以为独占整个地址空间 页表确保进程只能访问自己的"领地",内核区域被保护,用户进程无法直接访问。 现实效果:租客只能进自己的房间,大楼管理员(操作系统)掌握所有钥匙。 ✅ 地址确定性:稳定的"门牌号系统" 程序编译时使用虚拟地址,运行时:程序代码总认为 main() 在 0x08048000,实际物理地址可能是 0x12345000,但页表自动完成转换,程序毫无感知,程序员福利:调试时地址永远不变,指针值有意义。 ✅ 高效内存管理:灵活的"拼图游戏" 延迟分配 ```c char *p = malloc(1GB); // 1. 只分配虚拟地址(瞬间完成) p[0] = 'A'; // 2. 第一次访问才分配物理页(按需分配) ``` 按页交换 只将不常用的页面换出到磁盘 4KB 页面 vs 整个程序(可能几百MB) 交换粒度细,效率大幅提升 内存共享 相同代码段(如 libc)只需一份物理拷贝 多个进程通过页表映射到同一物理页 节省大量内存 ### 4. 虚拟内存的三大哲学 哲学一:**解耦的艺术** 传统方式:进程管理 ←紧密耦合→ 内存管理 虚拟内存:进程管理 ←虚拟地址→ 内存管理 效果:进程只关心虚拟地址布局,内核负责物理内存分配。两者独立演化,互不影响。 哲学二:**欺骗的善意** 操作系统"欺骗"每个进程:"你是世界上唯一的程序,拥有全部内存。" 实际上:成百上千的进程在共享物理资源。 简化编程模型 增强系统稳定性 提升安全性 哲学三:**懒惰的智慧** "不要为明天的事今天买单" 虚拟内存将工作推迟到最后可能的时刻: 分配内存?等到真的访问时再说 加载代码?等到执行到那行时再读 建立映射?等到需要跨越边界时再建 ### 5. 虚拟地址空间的现实价值 | 场景 | 物理内存时代 | 虚拟地址空间时代 | |------|-----------|----------------| | 程序开发 | 手动管理内存覆盖 | 每个进程独立4GB空间 | | 安全防护 | 病毒可破坏整个系统 | 进程隔离,最多影响自身 | | 多任务 | 需精确计算内存总量 | 可运行总和超过物理内存的程序 | | 动态库 | 需重定位,效率低 | 固定地址加载,多进程共享 | | 内存碎片 | 外部碎片严重 | 页面机制减少碎片 | ## 🎯总结:虚拟地址空间的革命性意义 虚拟地址空间不是简单的技术改进,而是操作系统的范式革命: > 1.从"真实"到"虚拟":程序不再受物理限制 > > 2.从"耦合"到"解耦":进程与内存管理分离 > > 3.从"粗放"到"精细":以页面为单位的精细管理 > > 4.从"确定"到"延迟":将工作推迟到最后一刻 最终效果:程序员看到一个简单、一致、安全的编程模型;操作系统获得灵活、高效、可靠的管理能力。 这就是为什么现代操作系统都必须有虚拟地址空间------它不是可选项,而是构建可靠计算基石的必需品。  加油!志同道合的人会看到同一片风景。 看到这里请**点个赞** ,**关注** ,如果觉得有用就**收藏**一下吧。后续还会持续更新的。 创作不易,还请多多支持!