先把一个画面放在这儿:很多年前我在排查一个“变量被莫名篡改”的 bug,开了两个进程分别打印同一个全局变量的地址,结果发现居然一模一样。当时第一反应是我把地址搞混了,又或者内存冲突了。可这两个进程跑得稳稳当当,谁也不干扰谁,压根没有想象中的“抢内存”崩溃。
后来我才意识到,那并不是“同一个内存”,而是每个进程都活在自己的一套虚拟地址里。也就是说,你打印出来的那个地址,根本不是物理内存条上的真实位置,而是操作系统给你搭出来的一层“假象”。
这个“假象”,就是虚拟内存。而与它牢牢绑定在一起的,是进程、线程、协程这一整条并发与资源管理的链条。理解它们之间谁是容器、谁在共享、谁在调谁,比单独背一堆术语定义重要得多。这篇内容我就按自己的理解,把四者的关系从头到尾拆一遍,尽量落到日常排查和开发能用到的地方。
1. 虚拟内存:为什么你在 printf 里看到的地址并不是真的
1.1 每个进程都活在同一个“假地址”里
现代操作系统里,用户态程序拿到的地址永远是虚拟地址,永远不会是物理地址。你在 64 位 Linux 里用 printf(“%p”, &i) 看到的那串 0x7fff...,只是进程自己的“虚拟地址空间”里的一个编号。CPU 在真正访问内存之前,会经过一个翻译动作,把虚拟地址查表换算成物理地址,然后才去碰真实的 RAM。
这个翻译带来的第一个好处,就是进程与进程之间的内存彻底隔离。你的程序在地址 0x7fff1234 放了一个值,另一个程序也可能在自己的 0x7fff1234 放了完全不相关的东西,两者互不知情,也不会互相覆盖。页面级的权限保护也在这一层生效:代码段只读、数据段可读写、内核区域用户态根本映射不进去。如果你把一个函数指针改成跳转到数据段,大概率会直接段错误,这正是虚拟内存页保护在起作用。
也是因为这个设计,所有进程看起来都像“独占”了整台机器。32 位程序看到 4GB 连续空间,64 位程序看到的是巨大的 128TB 虚拟空间,哪怕你的物理内存只有 8GB。操作系统只是把你实际用到的那些页映射到物理页上,用不到的部分不占物理内存。
这个“地址失真”不是偶然缺陷,而是系统设计里至关重要的一层隔离墙。没有这层墙,任何一个野指针的进程都能把别的进程甚至操作系统内核踩烂。
1.2 页表、MMU 和缺页中断:让幻觉成立的幕后
虚拟地址到物理地址的翻译由 MMU 硬件完成,翻译的“账本”叫页表。Linux 通常以 4KB 为一页,虚拟地址空间被切成无数个虚拟页,物理内存被切成物理页框,页表里记录着虚拟页到物理页框的对应关系。
关键点在于:不是所有虚拟页一开始就有对应物理页。你在 C 语言里调用 malloc(1GB),绝大多数情况下这 1GB 只是“记账”记下了,进程地址空间里多了一段映射,但物理 RAM 并不会立刻被占 1GB。当你真的一个字节一个字节去写这块内存,才会逐步触发缺页,操作系统才一页一页补上物理页。这也是为什么有时候你用 malloc 申请一大块内存,看 RSS 几乎没涨,但一 memset 整块区域,内存占用立刻飙升。
如果物理内存不够了怎么办?一部分不活跃的页会被写出到磁盘上的交换区,腾出物理页给真正需要的进程。等进程再访问那个被换出的页,再触发缺页读回来。虚拟内存的最大价值不单纯是“硬盘当内存用”,它更核心的设计目标是隔离、按需分配、以及提供“我拥有连续大空间”的能力。只是当物理内存实在紧张时,swap/pagefile 才成为兜底手段。
对开发者而言,有两个指标最容易被误读:VSZ 和 RSS。VSZ 是进程“虚拟内存大小”,RSS 是“实际驻留物理内存大小”。看到 VSZ 巨大不用恐慌,可能只是程序映射了一段很大的虚拟区域但根本没碰;真正反映物理压力的是 RSS,以及系统整体的 free、available。
1.3 pagefile、kswapd 与“GPU OOM 却叫我调虚拟内存”
Windows 上的虚拟内存设置经常让人困惑。系统里那个 pagefile.sys 本质就是交换文件,里面装的是被换出物理内存的页。很多人问“win11 如何将 pagefile.sys 转移到其他非系统盘”,操作上很简单:系统属性 → 高级 → 性能设置 → 高级 → 虚拟内存更改,取消“自动管理所有驱动器的分页文件大小”,先在目标盘设成“系统管理的大小”并点击“设置”,再把 C 盘选成“无分页文件”,重启生效。
这里顺序不能反,如果你先把 C 盘改成无分页文件,系统会提示没有可用分页文件而拒绝操作。
至于热搜里“设置
