资讯动态

虚存技术全解:从页表、TLB到缺页处理与页面置换

发布时间:2026/10/13 7:51:43 来源:尧图企业网站定制
翻开操作系统教材第五章往往是整门课的“分水岭”。在此之前你学的进程、调度、同步好歹还能在黑板上画个队列图、手算几个时间片到了虚存技术这里脑子里要同时装着硬件寄存器、内存管理单元、页表结构、磁盘I/O任何一个环节没接上后面的置换算法、工作集模型就全成了空中楼阁。我这几年面试系统研发相关岗位几乎每次都逃不开虚存相关的问题。问得最多的不是“虚拟内存是什么”而是“缺页之后CPU到底做了什么”“TLB miss和cache miss有什么区别”“Linux为什么把每个进程的地址空间画成那样一张图”。能把这些讲透的候选人大概率是真的动手折腾过内存管理而不是只背了第五章的考试重点。这篇文章就把虚存技术这条线从头捋一遍结合教材内容和实际操作系统里的落地机制把那些容易混淆、容易踩坑的地方逐个说清楚。先说清楚一个底层逻辑虚存技术解决的不是“内存不够用”这一个问题而是三个问题的打包解决方案——内存不够、地址冲突、内存保护。早期计算机没有虚存程序直接写物理地址。两个程序同时在内存里跑要么靠编译器把地址提前分配好要么靠“重定位”在装载时把地址掰过来。这种方案有两个致命伤第一程序必须一次性全部装入内存装不下就换人跑第二程序之间完全裸奔谁都能读谁的地址空间谈不上隔离和保护。虚存技术的核心思路是把“程序看到的地址”和“物理内存里实际的地址”彻底解耦让每个进程拥有一套连续的、私有的、大小接近处理器地址位宽的逻辑地址空间再由操作系统和硬件配合按需把逻辑页映射到物理页框。这套机制有四个核心要素整章内容都是围绕它们展开的逻辑地址空间、页表、地址翻译硬件MMU/TLB、缺页处理与页面置换策略。理解这四者的关系比背下任何一个单独的算法都重要。1. 虚存技术要解决的根本矛盾物理内存永远不够用1.1 从一道课堂实验说起为什么程序能跑得比物理内存还大我当年做课程实验时写过一个数值计算的程序数据规模大概需要4GB的内存。那是在一台物理内存只有2GB的机器上按说这个程序直接malloc会失败可它在Linux下居然老老实实跑完了。我当时的第一反应是“内核肯定偷偷用了swap分区”但后来仔细看现象才发现问题没那么简单——程序里真正被随机访问的核心数组只有几百兆剩下的数据大部分时间只是被顺序扫过根本不会常驻内存。教科书对此的表述是进程的局部性原理让虚存成为可能。程序在一段时间内的访存往往集中在某个较小的区域时间局部性和空间局部性因此我们完全不需要把整个程序都放在物理内存里只需要把当前正在活跃的部分放进去其余部分留在磁盘上。当程序访问到不在物理内存中的页面时硬件触发缺页异常操作系统去磁盘把该页换进来再重新执行那条触发异常的指令。这就回答了“程序为什么能比物理内存大”这个问题物理内存只是逻辑地址空间的一个缓存。缓存嘛装满不奇怪关键是命中率得高。虚存设计的成败本质上就取决于“按需换入”和“淘汰谁”这两件事做得好不好。1.2 逻辑地址空间不是“磁盘空间换的”它本身就是产品很多人有个误解觉得“虚拟内存物理内存swap分区”。这个说法不能说错但会引偏方向。严格讲即使机器完全没有swap虚拟内存机制照样存在。因为虚存先解决的是“地址空间抽象”的问题其次才是“容量扩展”的问题。每个用户态进程在32位系统里拥有4GB的逻辑地址空间在64位系统里理论上拥有巨大到用不完的地址范围。这套地址空间和物理内存是两套坐标系统程序里的指针、变量地址全都是逻辑地址硬件再通过页表把逻辑地址翻译成物理地址。翻译不命中时才轮到磁盘上场。提示虚存机制的核心收益不是“撑爆物理内存”这种极端表演而是让每个进程拥有独立、连续、受保护的地址空间。即便物理内存完全够用这套机制也必须存在——因为进程隔离和简化程序编写都依赖它。这就像快递柜给你分配了一个逻辑柜号你不需要关心自己的包裹被放到了哪个物理格子弹只要报柜号就能取件。1.3 地址空间隔离是怎么实现的两个进程可以拥有完全相同的逻辑地址比如都从0x400000开始放代码段但它们对应的页表项指向的物理页框完全不同。MMU在每次访存时都拿“当前进程的页表基址”来做翻译进程切换时只要换掉页表基址寄存器整个地址空间就跟着切走了。硬件是分时复用的而地址空间是相互隔离的。这也顺带解释了为什么用户程序永远碰不到内核地址内核所在的那段逻辑地址空间在用户态的页表项里被标记为“特权级不允许访问”。一旦用户程序去访问MMU在翻译阶段就会发现权限位不匹配直接产生保护异常连“能不能读”这一步都到不了。2. 分页机制虚存技术的地基工程2.1 页、页框和逻辑地址三段式切分分页机制为虚存提供颗粒度管理单位逻辑地址按固定大小切成“页”page物理内存按同样大小切成“页框”page frame。一个经典的配置是4KB一页。为什么是4KB而不是更大的尺寸这是权衡的结果页太小页表膨胀得厉害页太大内部碎片和传输开销又上去了。即使放到现在4KB依然是大部分系统默认值足以说明这个尺寸在绝大多数场景下的均衡性。逻辑地址被切分成三部分来看在经典单级页表模型下页目录号、页表号、页内偏移。为什么不是直接“页号偏移量”就完了因为页表本身也可能很大需要用多级结构来组织最前面几级是索引最后一层才指向真实页框偏移量直接穿透到物理地址的低12位。这是x86架构上最常见的设计。2.2 地址翻译全过程MMU在背后做了什么一条取指指令“mov (%ebx), %eax”当CPU去访问ebx寄存器里的地址时真正的流程是这样的CPU把逻辑地址交给MMU。MMU根据页表基址寄存器定位当前进程的页目录。逻辑地址的“页目录字段”作为索引从页目录中取出页表地址。“页表字段”作为索引从页表中取出页框号。页框号与“页内偏移”拼接得到物理地址。如果每个层级中对应的表项都不在TLB里则逐级查内存中的页表这是慢路径。如果表项显示该页不在物理内存PTE的存在位为0则触发缺页异常交给内核处理。这个过程在教科书上画成一张一步接一步的流程图看起来平平无奇但真正的性能问题都藏在第6步里。我没见过哪个新手第一次就能理解“一次普通的内存访问居然会触发多次额外内存访问”。每访问一个字节可能先花几十个纳秒去查页表——这才是TLB存在的意义把最近用过的页表项缓存起来让翻译过程变成一次并行查找。2.3 TLB快表是虚存性能的胜负手TLB是位于CPU内部的一个硬件缓存存放“逻辑页号→物理页框号”的映射。由于它容量极小通常只有几十到几千项设计上必须靠局部性原理吃饭程序不断访问的活跃页就那么几个TLB命中率高了地址翻译的开销才能被掩盖掉。我做过一个不算严谨但很直观的对比实验同一个数据遍历程序在开启大页和普通4KB页两种配置下分别跑一次观察运行时间的差距。大页配置下TLB可以覆盖更大的地址范围整体耗时有明显下降。原因很简单大页减少了页表遍历次数更重要的是同样大小的TLB能覆盖的内存范围变大了。这在内存密集型的科学计算负载上体现得非常直接。TLB miss和cache miss是两个完全不同的概念。我见过不少人在面试里把这俩混在一起。TLB miss是“地址翻译失败”说明页表项没缓存cache miss是“翻译完了但数据不在缓存里”。翻译在前取数在后本来就不是一个层面的东西。搞混一次后面关于内存层级的问题基本就全乱了。2.4 多级页表不只为省钱还为“不需要存在”教科书喜欢用“64位系统下一页表多大”这个例子来引入多级页表。一个进程如果使用4KB页和4GB逻辑地址空间单级页表需要约1百万个页表项按每项4字节算就是4MB。这只是一个进程的页表100个进程就是400MB。64位系统下如果还把所有逻辑地址空间都映射出来那个数字大到整个磁盘都塞不下。多级页表的核心思路是“按需创建中间层页表”如果一个顶层页目录项对应的整块地址空间都没被使用那这个目录项就是空的不必为它分配下级页表。这就像一本词典按字母拆成26本分册如果“Z”开头的词一个都没收录你就不需要硬造一整本“Z分册”。在x86-64的四级页表PML4→PDPTE→PDE→PTE里绝大多数上层页目录项都是空的。进程的地址空间中实际上只有少数字段是常驻的内核映射、用户代码数据、栈、堆、mmap区域。其余都是“空洞”的地址范围在多级索引下根本不会分配到真正的页表页。3. 缺页异常与页面置换虚拟内存从“纸上谈兵”走向“血拼物理内存”3.1 缺页处理的完整路径从硬件陷阱到内核恢复当MMU发现PTE里的存在位为0时会发起一个缺页异常。异常和中断的差别在于异常是同步的由当前指令直接引发处理完了要回到下一条“肇事指令”重新执行。所以内核在缺页处理结束后的职责很明确——确保引发缺页的指令有了合法的物理页框然后让该指令重跑一遍。教科书版本的流程检查引发缺页的地址是否合法是否在某个VMA区间内权限是否匹配。合法则进入“按需分配”或“从swap换入”的路径不合法则发送SIGSEGV给进程。分配或找到一个空闲页框把页面内容装进来可能是读文件、可能是从交换区换入、也可能是全零页。更新PTE设置存在位和权限位。刷新相关TLB条目。恢复进程继续执行。这里有个经常被忽略的细节“页面内容从哪来”并不总是swap。可执行文件的代码页第一次被访问时缺页处理程序往往只是搭建好映射关系把PTE指向文件在磁盘上的位置并没有真正的内存页框内容可读——这叫按需调页。而当你用mmap映射了一个文件再访问它同样也是先缺页、再读文件。并不只有swap才产生磁盘I/O。3.2 页面置换算法该淘汰谁这是个策略问题当物理内存不足、系统需要腾出页框给新页面时页面置换算法登场。这部分的经典算法清单不长OPT最优置换、FIFO先进先出、LRU最近最久未使用、Clock时钟置换也叫第二次机会、LFU最不经常使用。但每个算法背后的思想值得展开。OPT是理论天花板它需要知道未来的访问序列现实中不可实现但作为benchmark用来衡量其他算法的性能。FIFO实现最简单维护一个队首队尾就行却可能发生Belady异常——增加页框数反而导致缺页率上升。原因在于FIFO完全无视访问频率一个经常用到的页面早晚会被队列里的新页面挤出。LRU是很理想化的策略利用局部性淘汰最长时间没被访问的页面。表面上看我们只需要按“最后一次访问时间”排序即可但真正的难点在于每次访问都要更新这个顺序。硬件级别几乎不可能为每一页都维护一个精确时间戳且实时排序所以系统只能退而求其次用近似LRU。Clock算法是实际系统里最常见的基础方案。每个页表项维护一个访问位页面被访问时由硬件置位操作系统需要一个维护指针像表盘在环形链表上滑动。当需要淘汰页面时指针依次检查访问位是1就清0并跳过是0就淘汰。这就是“第二次机会”的由来——近期被访问过的页面会获得一次免死机会。我打过一个比方Clock算法就像一群人在一间房里排队房屋管理员每隔一轮清空所有人帽子上的标记谁在走过去这轮里被点到名页面被访问帽子上的标记会重新亮起管理员只挑帽子上没标记的人请出去。它用极低的硬件成本实现了对LRU一定程度上的逼近。3.3 工作集模型与系统抖动给置换算法安上“防抖”保险光有好的置换算法并不够更关键的问题是系统应该给每个进程分配多少页框分配少了缺页率飙升系统大部分时间在换页CPU利用率反而掉下去这就是抖动thrashing。工作集模型给出的答案是每个进程活跃使用的页面集合大小是动态的。只要系统能保证每个活跃进程的驻留集不小于它的工作集CPU就不会因为缺页而频繁空转。教科书上那张“CPU利用率—多道程序度”曲线值得反复看多道程序度一高内存分给每个进程的页框就不够所有进程都在换页CPU利用率骤降。实际系统的解决思路是控制多道程序度当检测到缺页率过高时暂时挂起一些进程调低并发把它们的页框让给正在运转的程序。这和后端服务里的“限流”逻辑一模一样——与其让所有请求都在排队超时不如只处理能处理的那部分。4. 虚存在真实系统中的落地进程地址空间到底是什么样4.1 一张进程地址空间图背后的所有映射关系从Linux的角度看虚拟内存的实现最直观的切入点是每个进程对应的内存描述结构。现代内核为每个进程维护一个逻辑地址空间视图从最低地址到最高地址典型布局是代码段text只读可执行从文件映射进来。数据段data已初始化全局变量可读写。BSS段未初始化的全局变量不占磁盘空间首次访问时由零页补充。堆heap通过break系统调用向上增长malloc大块内存时经常配合mmap。内存映射区mmap区共享库、mmap映射的文件、匿名映射。栈stack向下增长带伸缩规则和访问保护。内核地址空间固定映射在相对高地址区域。教科书把这部分表述为“进程地址空间图解”但真正有经验的人看到这张图的第一反应应该是每一个段都是一个VMA虚拟内存区域。vm_area_struct描述了一段连续的逻辑地址空间里“同一类后备存储映射”的范围。判断一个逻辑地址是否合法本质上就是判断它是否落在某个VMA范围内。4.2 malloc、mmap与缺页用户态分配的底层真相我在给新人讲malloc时总会先澄清一个反直觉的事实malloc成功分配内存绝不等于物理页已经分配好了。在Linux的默认配置下malloc分配大块内存时通常走mmap匿名映射返回的逻辑地址只是往页表里登记一个映射关系缺页时才真正分配物理页框。这个机制解释了为什么malloc超大内存看起来“几乎不会失败”只要逻辑地址空间还有富余内核就不会真的去检查物理内存够不够。后果是当你真正开始往这片区域写数据时操作系统开始忙碌地分配物理页面随时可能触发OOM内存耗尽的清理程序。面试时经常问到的“内存明明没超为什么进程被杀了”往往就发生在这个场景。想观察这些行为很简单在Linux下跑一个程序用malloc分配几百MB后再sleep期间查看进程状态和缺页统计你会看到分配前后物理内存的占用几乎纹丝不动一旦开始初始化数组RSS立刻飙升紧接着响应时间也会出现一个明显毛刺。4.3 swap、页面回收与OOM物理内存耗尽时的三重防线教科书讲swap通常一笔带过但真实系统比那复杂得多。现代内核内存回收的顺序大致是先回收干净的text页不需要写回直接丢弃即可重新读文件、再写回脏页、最后才轮到匿名页写回swap。匿名页没有对应的文件后备存储所以必须有一个专门的交换空间来承载它。swap分区或swap文件就是干这个的。但swap不等于“很重要”现代运维实践里很多服务器根本不配置swap因为磁盘I/O的延迟比内存缺页要高几个数量级宁可让OOM killer尽早杀掉进程也不愿意整机卡死。所以内核才会在swap使用率还没爆表的情况下就启动激进的内存回收。另外OOM killer杀掉某个进程的选择也不是随机点名。它给每个进程算一个累计分谁占用越多、谁被清理的收益越大谁就越有可能是那个“牺牲品”。这就是为什么明明系统每个进程都只占了一点内存最后被杀的往往是那个正在疯狂申请的进程——因为它的内存增长最快、加速因子最高。这类行为其实很符合直觉头痛砍头而不是砍脚。5. 现代系统的虚存进阶大页、内存压缩、KSM与NUMA感知5.1 大页与透明大页Taming 4KB的碎片化代价上文提到过TLB容量小、局部性好的假设但现代内存密集型负载会让局部性严重破功。遍历一个1TB的稀疏数组按4KB一页来算TLB能覆盖的连续范围实在太少缺页和TLB miss交替轰炸性能直线掉。大页把页尺寸从4KB提升到2MB甚至1GB。一次TLB lookup能覆盖的地图范围大了512倍。代价是内部碎片可能增加且大页在分配时对物理内存连续性的要求更高。container/虚拟化场景里guests内存一致地使用大页配合二代迁移往往能明显改善性能抖动。透明大页THP是Linux内核提供的一种自动行为允许应用无需显式配置就能在某些合适的使用场景下拿到大页。听上去很完美但它带来的运维问题也不少分配大页的页面迁移和合并动作偶尔会在延迟敏感的延迟关键路径上造成突发毛刺。我的经验是低延迟服务下手动关闭THP普通数据分析场景用它往往是划算的。5.2 KSM内核同页合并内存去重与虚拟化的缘分KSM做的事情是扫描物理页框找出内容完全相同的页面并将其合并为同一物理页多个进程共享它同时把PTE改成指向同一个页框并标记为写时复制。某进程一旦试图写入先复制一份再改写。它对虚拟化场景有天然吸引力一台宿主机上跑多个相似的操作系统镜像大量内核代码页完全相同KSM能把内存占用压下去不少。但KSM的扫描也要消耗CPU频繁变更的数据页做合并就是纯浪费。所以KSM的调优核心是平衡扫描频率和合并收益。5.3 NUMA内存本身就是分布式的虚存设计得跟着调整现代多路服务器上每个CPU访问本地的内存速度远快于访问远端内存。虚存机制不能假装所有物理内存都一样近。内核的内存分配器会优先从发起分配的CPU本地节点分配页面这叫NUMA感知。真正棘手的是线程迁移问题。一个线程在CPU0上分配了大量内存后来被调度到CPU1上CPU1访问这些页面全变成远端访问性能损失明显。系统会尝试通过自动迁移把页框迁移到该线程现在运行所在的节点但这本身又是昂贵的数据搬移操作。在这个问题上操作系统和应用的调优目标经常会打架内核想均衡负载应用想固定亲和性。5.4 内存压缩与zram物理内存不够时的软件自救除了swap换磁盘外现代社会系统还有一条路把内存页压缩后再放。内核的内存压缩机制会优先清理可移动页面并压缩片段以便形成更紧凑的物理页块来满足大页需求。zram则是一套把压缩后的数据块存储在内存里的swap设备。换出的页面经过压缩后还能留在RAM里等再次访问时直接从内存解压而不必跑一次磁盘I/O。这套设备跟固态硬盘这类swap载体搭配效果通常不错因为它砍掉了最贵的“走外部总线”这一环节。其背后的代价是CPU压缩开销所以不是所有负载都适合无脑开zram。6. 学习虚存技术常踩的坑与我的建议6.1 三个最典型的概念混淆第一把虚拟地址当成物理地址算大小。曾经有个同学写了一个全局二维数组然后没头没尾地引用某个越界元素问为什么没报段错误。原因很简单进程逻辑地址空间那么空错几格通常不会越完整个进程的VMA边界真正触发段错误需要跨过一大段“真空区”。不信你试试在栈里定义一个大数组再故意越界几百字节大概率依然“正常跑”。这就是VMA粒度比单片页更细但比“任意错一步都崩”更宽松的原因。第二混淆TLB和cache。一个小测验访问一个全0大数组时哪个局部性占主导从延迟曲线上看大数组访问主要被TLB miss影响cache反而不是最紧张的。如果接口的名言是“翻译错了怪TLB取数不存在怪cache”很多定位问题的思路一下子就能理顺。第三把缺页一律理解为磁盘I/O。缺页从来不只是“从swap换入”的代名词你第一次访问一个mmap文件页、第一次访问匿名页、第一次触发写时复制都会缺页。这些场景里更多是“分配页框填页表”的过程未必有真正的磁盘出入。6.2 我的课堂内外建议用Linux亲手验证这套理论如果只看书不写实验虚存这章永远学不透。我建议安排这样一组实验写一个程序分配和访问大数组在跑的过程中用工具观察内存相关指标进程状态、缺页次数、页表大小、上下文切换。用mmap映射一个几GB的文件逐步访问观察同一个指标的走势能看到明显的“首次访问缺页”与“已映射区域不再缺页”的差异。用工具调低可用内存或者直接开多进程挤压内存观察系统日志里OOM相关信息的出现顺序顺便理解内核换页和杀进程的优先级。对比开启和关闭透明大页时同一负载的性能差异注意识别是否有毛刺。这些实验不需要多高深的工具大部分发行版自带但亲手做完比盯着教科书上的曲线图要有用得多。很多面试里聊起虚拟内存我能明显感觉到对方是“文本级理解”还是“实验级理解”——后者几乎总是能接着聊下去。6.3 别把置换算法背得太死要理解衡量标准最后一层感悟是页面置换算法的价值不在公式而在权衡。为什么系统用Clock而不是LRU因为LRU在硬件上没法精确实现为什么大页又回来了因为TLB太小4KB的虚拟内存足迹吃掉了太多TLB容量。你会发现OS的设计本质上是一系列工程取舍不是在真空中做完美方案。理解了这一点再去看内核源码或做新硬件适配你才能真正读懂那些看似奇怪的参数和开关背后的人与代价。

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

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

免费获取报价 →
↑