【程序地址空间:进程眼中的“超级大饼”】
虚拟地址空间是操作系统给每个进程画的“大饼”。页表做映射,写时拷贝保独立,一篇讲透地址转换与内存管理。

虚拟地址空间、页表、写时拷贝、mm_struct——一篇讲透进程如何“独占”内存
文章目录
前言
你有没有想过这些问题?
- 为什么每个进程都“以为”自己拥有 4GB 内存?
- 两个进程的同一个虚拟地址(比如 0x1234)怎么会指向不同的物理地址?
- 写时拷贝(Copy-on-Write)是什么?为什么要用它?
- 为什么字符串常量区写入会崩溃?背后谁在拦截?
如果你对这些问题的答案模模糊糊,那这篇文章就是为你准备的。我们把虚拟地址空间、页表映射、写时拷贝等核心概念,一次性讲清楚。
🎯 一句话:操作系统给每个进程画了一张“超级大饼”,让进程以为自己独占物理内存,实际上背后有页表在偷偷映射。
一、一个进程,一个虚拟地址空间
在 Linux 中,每个进程都拥有自己独立的虚拟地址空间(通常 32 位系统是 4GB,64 位系统更大)。这个地址空间是“虚拟”的,不是真实的物理内存。
| 概念 | 说明 |
|---|---|
| 虚拟地址空间 | 进程“认为”自己拥有的内存地址范围 |
| 物理内存 | 计算机上真实的内存条 |
🥞 比喻:每个进程就像一个孩子,操作系统像一位爸爸。爸爸给每个孩子画了一张巨大的“大饼”(虚拟地址空间),告诉他们:“这块饼全是你的!”但实际上,爸爸只有一张真实的饼(物理内存),只是切成了小块分给孩子们。每个孩子都以为自己拥有整张饼。
二、页表:虚拟地址到物理地址的“翻译官”
2.1 页表是什么?
页表是操作系统为每个进程维护的一张映射表,用来将虚拟地址转换为物理地址。
虚拟地址 ──(页表)──> 物理地址
2.2 映射关系
初始情况下,父进程和子进程的页表映射关系是一样的。也就是说,同一个虚拟地址在父子进程中映射到同一个物理页面。
🧬 比喻:页表就像一张地图。进程拿着虚拟地址(比如“王府井大街1号”),通过地图找到真实的物理位置(比如“北京市东城区……”)。
通过下面这张图,希望你能更加理解
三、写时拷贝(Copy-on-Write):独立性的实现
3.1 什么是写时拷贝?
当父子进程共享同一块物理内存时,如果子进程(或父进程)要对这块内存进行修改,操作系统会:
- 在物理内存中开辟一块新空间
- 将原来的内容拷贝到新空间
- 修改子进程的页表映射,让子进程的虚拟地址指向新空间
- 父进程的映射保持不变
结果:父子进程的虚拟地址相同,但物理地址不同,实现了进程的独立性。
3.2 为什么要写时拷贝?
| 原因 | 说明 |
|---|---|
| 减少创建时间 | fork 时不需要拷贝全部内存,只需复制页表 |
| 减少内存浪费 | 只有修改时才真正拷贝,否则一直共享 |
💡 如果没有写时拷贝,fork 一个进程就要把父进程的所有内存拷贝一遍,既慢又浪费内存。
3.3 写时拷贝流程示意
┌─────────────────────────────────────────────────────────┐
│ fork 之后(初始状态) │
│ 父进程虚拟地址 V ──页表──> 物理页面 P │
│ 子进程虚拟地址 V ──页表──> 物理页面 P(相同) │
└─────────────────────────────────────────────────────────┘
│
│ 子进程尝试写入
▼
┌─────────────────────────────────────────────────────────┐
│ 写时拷贝后 │
│ 父进程虚拟地址 V ──页表──> 物理页面 P(不变) │
│ 子进程虚拟地址 V ──页表──> 物理页面 P'(新拷贝) │
└─────────────────────────────────────────────────────────┘
🎯 核心:虚拟地址不变,但页表映射变了,物理地址变了。
四、虚拟地址空间的本质:一个数据结构
4.1 要不要管理“大饼”?
要管理! 操作系统用 struct mm_struct 这个数据结构来描述虚拟地址空间。
| 概念 | 说明 |
|---|---|
| 虚拟地址空间 | 操作系统给进程画的“饼” |
mm_struct |
描述虚拟地址空间的内核数据结构 |
🧠 经典思想:先描述,再组织。操作系统用
mm_struct描述每个进程的地址空间,再用链表或红黑树组织起来。

4.2 虚拟地址空间的区域划分
虚拟地址空间不是铁板一块,而是划分成多个区域。每个区域有起始地址和结束地址。
| 区域 | 典型位置 | 内容 |
|---|---|---|
| 代码段 | 低地址 | 程序指令 |
| 数据段 | 代码段上方 | 已初始化的全局变量 |
| BSS 段 | 数据段上方 | 未初始化的全局变量 |
| 堆 | 向上增长 | 动态分配的内存(malloc) |
| 内存映射区 | 堆和栈之间 | 共享库、mmap |
| 栈 | 高地址向下增长 | 局部变量、函数调用 |
// 内核中 mm_struct 的简化表示
struct mm_struct {
unsigned long code_start, code_end; // 代码段范围
unsigned long data_start, data_end; // 数据段范围
unsigned long heap_start, heap_end; // 堆范围
unsigned long stack_start, stack_end; // 栈范围
// ... 还有页表指针等
};
📐 比喻:虚拟地址空间就像一栋大楼的楼层分布图,标明了哪一层是办公室(代码段)、哪一层是仓库(数据段)、哪一层是食堂(堆)……
五、虚拟地址空间的意义
5.1 意义一:将地址从“无序”变“有序”
物理内存的分配是杂乱的,但虚拟地址空间让每个进程看到连续、整齐的地址范围,简化了程序开发。
5.2 意义二:权限保护
页表中不仅包含物理地址,还包含读写执行权限(rwx)。当 CPU 访问一个虚拟地址时,MMU(内存管理单元)会检查:
· 如果操作违反权限(比如对只读区域进行写入),则触发段错误(Segmentation Fault)。
🔒 这就是为什么字符串常量区写入会崩溃:
char *str = "hello world";
*str = 'H'; // 段错误!
| 原因 | 说明 |
|---|---|
| 字符串常量存储在只读数据段 | 页表中该区域标记为只读 |
| 写入操作被 MMU 拦截 | 操作系统收到异常,发送 SIGSEGV 信号 |
| 进程崩溃 | 就是我们看到的段错误 |
5.3 意义三:解耦合
虚拟地址空间让进程管理和内存管理解耦合:
· 进程只关心自己的虚拟地址空间
· 操作系统负责把虚拟地址映射到物理内存
· 进程不需要知道物理内存的实际情况
🧩 好处:进程可以随便用地址,操作系统可以在后台搬家(比如页面换入换出),进程毫不知情。
六、深入理解进程的“独立性”
进程具有独立性,体现在两个方面:
| 独立性 | 说明 |
|---|---|
| 内核数据结构独立 | 每个进程有自己的 task_struct、mm_struct、页表 |
| 加载进入内存的代码和数据独立 | 通过写时拷贝,修改时物理内存分离 |
💡 两个进程可以拥有完全相同的虚拟地址空间布局,但映射到不同的物理内存。
七、进程挂起的深刻认识
进程挂起是指进程的某些部分(或全部)被换出到磁盘(swap 分区),以释放物理内存。
有了虚拟地址空间,挂起变得容易:
· 进程的 mm_struct 依然存在
· 页表中某些条目可以标记为“未映射”或“已换出”
· 当进程访问这些地址时,操作系统触发缺页异常,从磁盘换入
🧳 比喻:就像你出差时把行李寄存在火车站。你手上只有一张寄存凭证(虚拟地址),需要时凭凭证取回行李(物理内存)。凭证还在,行李可能不在手上。
八、再扩展一点:vm_area_struct
mm_struct 描述整个地址空间,而每个具体区域(比如堆、栈、代码段)由 vm_area_struct 来描述。
struct vm_area_struct {
unsigned long vm_start; // 区域起始地址
unsigned long vm_end; // 区域结束地址
unsigned long vm_flags; // 权限标志(rwx)
struct vm_area_struct *next; // 链表指针
};
所有的 vm_area_struct 通过链表或红黑树连接,组成了完整的地址空间布局。
📚 比喻:mm_struct 是一本书的目录,vm_area_struct 是每个章节的详细信息。
下图展示了他们与虚拟地址空间的关系

九、澄清几个问题
Q1:可以不加载代码和数据吗?
可以。 一个进程创建后,可以先只有:
· task_struct(进程控制块)
· mm_struct(地址空间描述)
· 页表
代码和数据可以按需加载(比如执行到某个函数时才把对应的代码页从磁盘读入)。
Q2:如何深刻理解写时拷贝?
写时拷贝的核心目的:
| 目的 | 说明 |
|---|---|
| 减少创建时间 | fork 时只复制页表,不复制物理内存 |
| 减少内存浪费 | 父子进程共享物理内存,只有修改时才拷贝 |
🚀 写时拷贝是操作系统的一种惰性优化:不到万不得已,绝不拷贝。
十、总结速查表
| 知识点 | 核心内容 |
|---|---|
| 虚拟地址空间 | 每个进程认为自己独占一整块内存 |
| 页表 | 虚拟地址 → 物理地址的映射表 |
| 写时拷贝 | 修改共享内存时,才真正拷贝物理页 |
mm_struct |
描述虚拟地址空间的数据结构 |
vm_area_struct |
描述地址空间中一个区域的数据结构 |
| 虚拟地址空间意义 | 有序化、权限保护、解耦合 |
| 段错误本质 | 页表权限拦截非法访问 |
| 进程独立性 | 内核数据结构独立 + 代码数据独立 |
| 进程挂起 | 部分内存换出到磁盘,通过缺页异常换入 |
| 写时拷贝优点 | 减少 fork 时间,减少内存浪费 |
最后
虚拟地址空间是现代操作系统的基石。理解它,你就明白了:
· 为什么 fork 这么快(写时拷贝)
· 为什么字符串常量不能写(页表权限)
· 为什么两个进程的同一地址值不同(独立映射)
· 什么是“进程挂起”(换出换入)
动手试试:写一个程序,打印父子进程中同一个全局变量的地址和值,观察写时拷贝的效果。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)