虚拟地址空间:从“地址一样但物理内存不同”说起

虚拟地址空间:从"地址一样但物理内存不同"说起

我最近在学操作系统里的内存管理,本来以为"虚拟地址空间"就是一句"每个进程都有自己独立的地址空间"就完事了。结果越看越发现,这里面坑挺多:父子进程打印出来的地址一样,但物理内存其实不是同一块;malloc 了也不一定马上给你物理页;task_structmm_struct 又各自管什么......我把自己踩过的坑和终于搞懂的点记一下。

先插一句:普通命令和内建命令

看 shell 的时候发现,命令和命令之间也有差异:

  • 二进制文件级别的命令:普通命令,比如 lsgrep,需要去 PATH 里找对应的可执行文件。
  • 内建命令:shell 内部自己实现的,比如 cdecho,属于 bash 自己内部的函数调用,不依赖第三方路径。

这跟后面的进程加载、地址空间也有点关系,不过重点还是虚拟地址空间。

CPU 眼里的地址,其实是虚拟地址

CPU 中看到的地址分布大概像这样:

一个进程的虚拟地址空间里,代码段、数据段、堆、栈、共享库等都有自己的位置。栈整体是向低地址方向增长的,堆是向高地址方向增长的,所以大家常说"堆栈相对而生"。

text 复制代码
高地址
+------------------+
|      栈 stack     |  向下增长
+------------------+
|        ...        |
+------------------+
|      堆 heap      |  向上增长
+------------------+
|  BSS / data / text|
+------------------+
低地址

当然,实际布局还要看 32 位还是 64 位,以及内核如何安排。

踩了个坑:地址一样,物理内存就一样吗?

我写了个 fork 的小实验,父子进程打印同一个变量的地址,结果地址居然一样。当时我第一反应是:"那它们不就访问同一块内存了吗?"

后来看到类似这样的现象:

这个现象其实说明了两件事:

  • 进程具有独立性。
  • 这里打印出来的是虚拟地址,物理地址不可能这么巧完全相同。背后是写时拷贝:OS 给写入的一方分配了新的物理页,再把同一个虚拟地址重新映射到新的物理页上。

所以"地址相同"只是虚拟地址相同,不代表物理内存相同。终于搞懂了这一点之后,再看页表就顺眼多了。

task_struct 和 mm_struct:进程的"内存说明书"

程序在运行时会被转化为进程,一个进程会有一个 task_struct

而一个 task_struct 中会包含一个 mm_struct,用来定义虚拟地址空间。task_struct 含有指针 mm,指向独立的 mm_struct 结构体,用来描述进程虚拟地址空间。

c 复制代码
struct mm_struct {
    struct vm_area_struct *mmap;        // VMA链表(同时维护红黑树,用于快速查找虚拟内存区域:堆、栈、代码段、动态库)
    struct vm_area_struct *mmap_cache;  // 最近访问的VMA缓存,加速查找
    pgd_t *pgd;                         // 页全局目录,页表基址,对应CPU CR3寄存器

    unsigned long start_code, end_code; // 代码段 .text 的起止虚拟地址
    unsigned long start_data, end_data; // 已初始化数据段 .data
    unsigned long start_brk, brk;       // 堆:start_brk堆起始地址,brk堆当前边界(brk系统调用修改brk)
    unsigned long start_stack;          // 用户栈起始虚拟地址
    // ... 引用计数、内存统计等
};

这里每个字段其实都在描述"进程的虚拟内存长什么样"。比如 mmap 管 VMA,pgd 管页表基址,start_brkbrk 管堆的边界,start_stack 管用户栈起点。

全局变量、static 和虚拟地址空间的意义

全局变量和 static 变量的生命周期是全局的,本质上是因为它们的生命周期跟着进程走。

虚拟地址空间不只是"给每个进程画个饼",它还有几个很实际的作用:

  • 可以拦截进程的非法行为,保护物理内存和其他进程。
  • 有了虚拟地址空间和页表,物理内存不需要连续、有序,也能映射成进程看到的连续虚拟地址。
  • 让进程管理和内存管理解耦。

以前我总觉得页表只是"地址转换表",现在发现它更像一层权限和隔离机制。

程序启动加载:不是一股脑全塞进内存

程序启动加载这块也很容易误解。创建进程时,OS 只是构建虚拟地址空间、页表,并不会一次性把磁盘上的代码和数据全部载入物理内存。

首次访问对应虚拟地址时,会触发缺页中断;OS 再从磁盘读取页面,分配物理内存,建立虚拟地址到物理地址的映射。

new / malloc 也是类似的思路:本质上是在虚拟地址空间的堆区申请,先在页表层面创建虚拟地址,后续真正要使用时再填充物理地址。这就是空间的懒加载机制。

所以 malloc 成功,不代表物理内存已经立刻到手;真正写入的时候,才可能触发缺页,让 OS 把物理页补上。

小结

这轮学下来,我最大的收获是:虚拟地址空间不是"假的地址"这么简单,它把进程隔离、写时拷贝、缺页中断、懒加载、页表权限这些东西串在了一起。以前看到父子进程地址一样会懵,现在至少知道要去看页表和写时拷贝;以前觉得 malloc 就是直接拿内存,现在知道它更多是在虚拟地址空间里先圈地,物理页可以后面再补。

继续啃内存管理,感觉还有不少坑要踩。

相关推荐
孙启超1 小时前
【AI开发之Rust】第 1 课:认识 Rust 与开发环境 —— 装好工具链,跑通第一个程序
开发语言·人工智能·后端·ai·rust·安全架构
Bs_MoneyMagnet2 小时前
基于springboot+vue的宠物殡葬管理平台的设计与实现 源码+文档
java·javascript·vue.js·spring boot·后端·宠物
萧瑟余晖3 小时前
JPA 规范与 Hibernate 入门详解
后端·架构·hibernate
程序员cxuan3 小时前
跟 WebUI 说再见了,最强 DeepSeek 桌面端来了!
人工智能·后端·程序员
geovindu3 小时前
CSharp: Strategy Pattern
开发语言·后端·c#·.net·.netcore·策略模式·行为模式
stark张宇3 小时前
从 Paxos 到 Raft:一文讲透分布式一致性与复制状态机
分布式·后端
RsThe3 小时前
Spring AI 2.0:Java AI 开发的分水岭
后端
n8n4 小时前
Spring AI 工具调用实战:模型自主决策、@Tool 方法封装与 ReAct 模式
后端
只爱喝胡辣汤4 小时前
05-JVM 监控与故障排查工具
后端