资讯动态

Linux虚拟地址空间原理与实战:页表、mmap与写时复制

发布时间:2026/10/6 4:54:24 来源:尧图企业网站定制
我一直觉得Linux进程这块内容里虚拟地址空间是最容易被轻视、又最值得花时间啃的一块硬骨头。很多人会用top、free看内存占用会写malloc分配内存但一问“进程看到的那个地址到底是不是物理地址”就愣住了。如果你也在学Linux或者准备面试或者单纯想把进程跑飞的原因查明白那这期内容应该能帮上忙。我会从一个从业者的角度把虚拟地址空间的原理、布局、地址翻译过程、以及与fork、IPC、内存共享这些热词的关联逐一拆开讲尽量讲得实在一点不绕弯子。虚拟地址空间这个词听起来高深但它本质上回答的是一个问题进程凭什么以为自己拥有整台机器的内存每个进程都觉得自己独占内存、互不干扰底层靠的正是这一层虚拟化机制。搞懂它后面再去看进程通信、守护进程、线程模型、内存泄漏排查都会顺很多。1. 虚拟地址空间到底在解决什么问题1.1 从“地址”两个字讲起先做个简单类比。你住在一栋公寓楼里每户都有一个房间号比如3楼302。这个302就是门牌号对应一个真实的物理房间。但是如果整栋楼重新装修把302挪到了5楼房间号还得改成502不然访客找不到你。为了避免这种麻烦物业引入了一个“总机”访客在外面只需要说“302”总机再查表告诉你实际位置。进程和物理内存的关系就像访客和房间的关系。在没有虚拟地址空间的时代比如早期的实模式操作系统程序访问的就是物理地址。程序A写了某个地址程序B也可能写到同一个地址互相覆盖一会儿A崩溃一会儿B崩溃非常难受。引入虚拟地址之后每个进程看到的是一套从0到某个上限的“假地址”这套地址由内核和硬件共同翻译成真实物理地址。所以进程A的0x400000和进程B的0x400000物理上完全可能是两块不同的内存。这里有一个容易被忽略的点虚拟地址空间解决的不仅是“隔离”还包括“安全”和“效率”。先说安全用户态进程根本接触不到内核区域的物理地址即使代码写错了也不至于直接破坏别的进程或内核数据。再说效率有了虚拟地址这层壳我们就可以用“按需加载”的方式让物理内存只承载真正被访问到的页而不是把所有段都一股脑塞进内存。1.2 32位与64位的空间格局说到虚拟地址空间的大小先得看位数。32位系统上虚拟地址空间是2的32次方也就是4GB。但4GB并不是全部给用户用的典型的Linux 32位布局中内核通常会占据最高的1GB从0xC0000000往上用户空间只剩3GB。这也就是老程序员常说的“用户态3G/内核态1G”。64位系统情况就不一样了。理论地址空间是2的64次方大得离谱但现在的x86-64硬件并没有把所有位都用于寻址通常会限制在48位或者57位。Linux在x86-64上一般把用户空间定为0x0000000000000000到0x00007fffffffffff约128TB内核空间从0xffff800000000000开始。中间那一大片不可达区域就是为了将来扩展留的。对于实战排查来说最直观的影响是64位进程能映射的地址范围极大很多程序“看得到但用不到”的空间会非常夸张这也是为什么top里面VIRT列经常出现几十甚至上百GB的原因。别慌那只是虚拟大小不是物理占用。2. 进程眼中的内存布局一段不可见的城市地图2.1 从低地址到高地址真实布局长什么样如果你用cat /proc/pid/maps看一个正在运行的进程会看到一长串十六进制地址区间。这些区间按从低到高排列有一个非常经典的规律跟我一起在脑子里画一张图。最低地址区域通常是代码段Text紧接着是数据段Data和BSS段。代码段存放机器指令通常只读数据段存放已初始化的全局变量BSS段存放未初始化的全局变量它不占磁盘空间但在进程加载时会被分配并清零。再往上就是堆区Heap通过malloc、new动态分配的内存基本都来自这里。堆之上会有一块动态共享库的映射区域也就是你看到的/usr/lib/...这样的映射行再往上还有mmap区域用于文件映射、共享内存、动态库加载。然后是栈区Stack从高地址往低地址长。真正的最高用户地址附近还保留着一块用于存放环境变量和命令行参数的区域。这里有一个高频考点栈和堆的生长方向是相反的。栈从高地址往低地址压堆从低地址往高地址涨。两边的地址如果不断逼近最终会导致进程申请内存失败或栈溢出。但实际中栈有上限ulimit -s控制堆也会受到映射区间的挤压所以并不会真的发生“堆栈相遇”这种戏剧性事件。2.2 栈与堆的相向生长和常见误区我见过不少新手犯一个误区以为栈和堆是两块固定大小的内存谁先耗完谁受限。其实“栈大小”和“堆大小”是两回事。栈的上限由RLIMIT_STACK决定通常8MB可用ulimit -s查看而堆并没有硬性上限它取决于虚拟地址空间剩余大小以及物理内存和交换空间的总量。在64位系统下堆理论空间非常大。但要注意栈和堆的“相向生长”在并发编程、线程模型中变了味道。多线程程序里每个线程都有自己的独立栈这些栈是分配在线程创建时映射出来的固定区域所以线程栈其实是“各占一块”不再满足传统的“一个大栈”模型。谈到进程和线程的区别时地址空间共享是核心差异进程之间地址空间相互隔离线程之间共享整个地址空间。这个点面试特别喜欢问记住“虚拟地址空间隔离”和“线程共享同一虚拟地址空间”这两句话能挡住很多连环问。3. 页表与MMU地址翻译这件事是怎么落地的3.1 一次虚拟地址访问经过的完整链路现在我们从进程视角切到硬件视角。进程代码里写了个地址比如0x400abc当CPU执行到访问这条指令时它并不会直接把0x400abc发到内存总线上。而是先交给MMU内存管理单元MMU查页表把虚拟地址翻译成物理地址然后才发出真正的访存请求。页表是内核为每个进程维护的多层级表格。为什么是多级如果每个进程都维护一张“一对一”的大表4GB地址空间用4KB页来分就是100万个条目每个条目几十字节算下来光是页表就要几十MB64位系统动辄上GB的页表简直不可接受。多级页表的核心思想是大部分虚拟地址区域根本没有被使用就干脆不建立对应的页目录项。这就像一本字典如果你只需要查几个字没必要把所有页面全部印出来只需要用到哪一页就加哪一页。我这么说吧x86-64的四级页表叫PGD、PUD、PMD、PTE一共四层索引再加最后的页内偏移合成48位有效虚拟地址。CPU拿到虚拟地址后把地址拆成这么几段9位给PGD9位给PUD9位给PMD9位给PTE剩下12位是4KB页内的偏移。每次访问如果页表项有效就直接得到物理页帧号然后拼上偏移得到最终物理地址。3.2 缺页异常、TLB 和多级页表页表项不一定有效。当进程访问了一个“地址空间内存在但物理页还没加载”的地址时MMU会触发缺页异常Page FaultCPU跳进内核的异常处理程序。内核看这个缺页到底是合法缺页还是非法访问如果是合法缺页比如代码段还没从磁盘读入、堆刚扩展但还没分配物理页、或者mmap的文件页还没缓存那就从磁盘读页或者分配零页把页表填好再回到用户态重试指令如果是非法访问比如写只读页、访问未映射地址就会给进程发SIGSEGV信号也就是我们常说的Segmentation Fault。缺页还有细分。缺页后需要读磁盘的称作major fault物理页已存在但页表项缺失比如共享页刚被换出又换回称作minor fault耗时差异非常大。性能排查时可以用/usr/bin/time -v看到进程的Major/Minor page faults如果major fault特别多说明程序内存访问模式不太好或者滥用了mmap导致频繁回读磁盘。TLB则是MMU旁边的小快表毕竟每访存一次都去查四五级页表太慢了。TLB缓存了最近使用过的虚拟地址到物理地址的映射命中率通常能达到99%以上。我们平时说“CPU缓存友好”其实不只是L1/L2数据缓存的事TLB命中率影响更大。如果一个程序在内存里跳来跳去访问大量独立页面TLB会频繁失效很多所谓“莫名慢”的问题根子就在这。4. fork、mmap 与 IPC虚拟地址空间在进程协作中的角色4.1 写时复制fork 能很快跑起来的秘密讲进程通信和进程池之前得先说fork和虚拟地址空间的关系。fork创建子进程时如果不做优化最简单粗暴的做法就是把父进程的整个地址空间复制一份。这开销大到离谱尤其是父进程占了几GB内存时。Linux引入的机制是“写时复制”Copy on WriteCOW。fork之后父子进程的虚拟地址空间映射的是同一个物理页页表项被标记成只读。只要父子都不写大家都读同一块物理内存瞬间完成“复制”。一旦某一方试图写陷入缺页异常内核发现是COW页就把物理页复制一份重新映射并恢复写权限然后把控制权还给用户态。这样就把“复制全部”延迟成了“复制被写的那一页”代价小得多。这个机制还解释了一个现象为什么fork之后子进程继承的虚拟地址空间是“逻辑副本”而不是“物理副本”。子进程看到的地址范围、映射关系一模一样但物理页却是共享的直到有人写入。这也是理解写时复制和进程池为什么配合良好的原因进程池预先创建一批子进程每个子进程虽然有自己的地址空间但在不动数据的前提下并不需要复制父进程所有物理页省内存且启动快。4.2 mmap 与共享内存的关系进程通信IPC中有一类经典方案是共享内存而共享内存的底层往往就是mmap。mmap可以把文件、设备、匿名内存映射到进程的虚拟地址空间里。关键在于多个进程可以映射同一块物理内存虚拟地址不同物理页一致这样就能实现高效通信。我实际项目中经常用mmap做这种“无锁”数据交换生产者把数据写进一块mmap区域消费者读同一块内存。相比管道和消息队列共享内存少了数据在内核态和用户态之间的反复拷贝延迟低很多。但要注意同步问题因为没有内核帮你排队必须自己用锁、信号量或原子变量保证一致性。这也是面试中问“进程通信方式有哪些”时容易踩坑的点只列出共享内存这个名词还不够得能说清楚它和虚拟地址空间的映射关系。mmap还有另一个常见用途是加载动态库。ld.so把.so文件的代码段以只读、执行的方式映射到进程地址空间中多个进程如果加载同一个库物理页也是共享的。你看到/proc/pid/maps里那一堆libc.so映射就是mmap的实际效果。5. 实操用常见命令把进程的虚拟地址空间“看穿”5.1 /proc/pid/maps 逐列拆解纸上谈兵再多不如自己动手看一次。先随便起一个常驻进程比如sleep 3600 记住它的PID然后执行cat /proc/pid/maps你看到的每一行基本是这个格式地址区间 权限 偏移 设备 inode 路径 00400000-0040b000 r-xp 000000 08:01 123456 /usr/bin/sleep地址区间是虚拟地址的起始和结束权限位分别是读、写、执行、私有/共享p/s。权限里的p指私有映射s指共享映射。比如rw-p就是可读可写但私有的匿名内存通常对应堆和栈r-xp就是代码段只读且可执行r--p可能是只读数据。偏移字段对文件映射来说表示该段在文件中的起始偏移量设备号是文件所在磁盘的编号inode是文件节点号路径就是映射来源。看这个文件时你最直观的感受是地址区间的数量比你想象的多。一个普通进程可能几十行的映射其中大部分是动态库。排查“内存是不是泄漏时”多出来的映射区间往往是线索所在。5.2 pmap 与常用内存排查命令组合仅仅看maps还不够直观推荐组合使用pmap -x pidpmap -x会把每个映射区间的RSS驻留物理内存大小列出来并按总内存汇总。它能直接告诉我们哪些段真正占了物理页哪些只是虚拟地址很大但没有实际分配。排查泄漏的时候我以前的做法是先用top里VIRT和RES对比看哪个进程虚拟疯涨但物理占用不高基本可以怀疑是mmap只扩地址没落物理页。再用pmap -x找到最大的几个区间判断属于堆、栈还是文件映射。如果想看历史趋势可以配合pidstat -r 1或者/proc/pid/status里的VmPeak、VmSize、VmRSS字段。这套组合拳对运维场景非常有用。比如线上有个服务莫名其妙占了几十G虚拟内存你先别急着加机器先用cat /proc/pid/maps看看是不是加载了超大文件映射或者是不是“forget to unmap”导致映射区间越攒越多。很多所谓内存泄漏其实根本不是堆泄漏而是mmap区域泄漏。6. 常见问题排查与面试题高频考点6.1 虚拟内存占用高不等于真用得多有一次帮同事查一个Java进程top里VIRT显示60多G物理内存RES却只有几个G同事怀疑是不是把机器内存吃光了。我让他先ps aux看RSS再看pmap发现八成是JVM预留的堆、元空间和线程栈映射占了大坑真正压力还在控制范围内。这个现象的本质就是虚拟地址空间和物理内存的差异虚拟地址是“画饼”物理内存是“实际吃下去的饭”。进程申请1GB虚拟内存内核只是把对应区域的页表项准备好并不立刻分配物理页。真正写入的时候才触发缺页异常一页一页地分配。所以一个进程虚拟占用再大只要RES不高机器通常就没有那么大压力。但这不是说虚拟大就没问题。某些情况下比如启动时给虚拟内存设了超高ulimit -v限制或者进程的映射区域数量过多光页表本身也会吃掉不少物理内存。每个进程即使在未使用区域页表的顶层目录也是要常驻物理内存的几万进程的页表开销加起来非常可观。6.2 高频面试题整理附简答思路我收集了几个和虚拟地址空间强相关、又总被面试官反复问的问题统一整理一下方便大家复习。问题参考思路进程和线程的本质区别是什么进程拥有独立的虚拟地址空间线程共享所属进程的地址空间但各线程独享栈。32位系统的4GB空间为什么进程只能用3GB内核要留一部分虚拟地址空间用于自身运行通常高位1GB归内核态使用。malloc分配的内存会立即占用物理内存吗不会。malloc只是扩展了堆的虚拟地址范围首次写入时才触发缺页分配物理页。fork之后父子进程的内存相同吗虚拟地址空间内容逻辑相同物理内存通过写时复制共享写后才分家。动态库加载为什么用mmap多个进程可以共享同一份物理内存中的库代码节省物理内存。守护进程与会话有什么关系守护进程通常调用setsid创建新会话脱离控制终端避免被终端信号影响。进程通信方式和虚拟地址空间有关吗共享内存本质上是同一物理页映射到多个进程的虚拟地址空间。不只是面试平时写代码遇到疑难杂症把这套思维带进去也能少走很多弯路。最后再分享一个我自己的习惯排查任何进程内存问题时第一件事不是看top而是先cat /proc/pid/maps扫一眼映射区间再配合pmap -x看RSS分布。很多时候一眼就能发现是堆膨胀、栈爆了还是文件映射没释放。这个流程我用了很多年几乎每一轮排查都能帮上忙。虚拟地址空间这个东西看起来是内核概念实际离业务排查一点都不远。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑