在这里插入图片描述

虚拟地址空间、页表、写时拷贝、mm_struct——一篇讲透进程如何“独占”内存


前言

你有没有想过这些问题?

  • 为什么每个进程都“以为”自己拥有 4GB 内存?
  • 两个进程的同一个虚拟地址(比如 0x1234)怎么会指向不同的物理地址?
  • 写时拷贝(Copy-on-Write)是什么?为什么要用它?
  • 为什么字符串常量区写入会崩溃?背后谁在拦截?

如果你对这些问题的答案模模糊糊,那这篇文章就是为你准备的。我们把虚拟地址空间页表映射写时拷贝等核心概念,一次性讲清楚。

🎯 一句话:操作系统给每个进程画了一张“超级大饼”,让进程以为自己独占物理内存,实际上背后有页表在偷偷映射。


一、一个进程,一个虚拟地址空间

在 Linux 中,每个进程都拥有自己独立的虚拟地址空间(通常 32 位系统是 4GB,64 位系统更大)。这个地址空间是“虚拟”的,不是真实的物理内存。

概念 说明
虚拟地址空间 进程“认为”自己拥有的内存地址范围
物理内存 计算机上真实的内存条

🥞 比喻:每个进程就像一个孩子,操作系统像一位爸爸。爸爸给每个孩子画了一张巨大的“大饼”(虚拟地址空间),告诉他们:“这块饼全是你的!”但实际上,爸爸只有一张真实的饼(物理内存),只是切成了小块分给孩子们。每个孩子都以为自己拥有整张饼。


二、页表:虚拟地址到物理地址的“翻译官”

2.1 页表是什么?

页表是操作系统为每个进程维护的一张映射表,用来将虚拟地址转换为物理地址


虚拟地址 ──(页表)──> 物理地址

2.2 映射关系

初始情况下,父进程和子进程的页表映射关系是一样的。也就是说,同一个虚拟地址在父子进程中映射到同一个物理页面。

🧬 比喻:页表就像一张地图。进程拿着虚拟地址(比如“王府井大街1号”),通过地图找到真实的物理位置(比如“北京市东城区……”)。

通过下面这张图,希望你能更加理解
在这里插入图片描述


三、写时拷贝(Copy-on-Write):独立性的实现

3.1 什么是写时拷贝?

当父子进程共享同一块物理内存时,如果子进程(或父进程)要对这块内存进行修改,操作系统会:

  1. 在物理内存中开辟一块新空间
  2. 将原来的内容拷贝到新空间
  3. 修改子进程的页表映射,让子进程的虚拟地址指向新空间
  4. 父进程的映射保持不变

结果:父子进程的虚拟地址相同,但物理地址不同,实现了进程的独立性

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_structmm_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 这么快(写时拷贝)
· 为什么字符串常量不能写(页表权限)
· 为什么两个进程的同一地址值不同(独立映射)
· 什么是“进程挂起”(换出换入)

动手试试:写一个程序,打印父子进程中同一个全局变量的地址和值,观察写时拷贝的效果。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐