2、内存基础原理:物理内存与虚拟内存、内存寻址与映射、内存页与页表机制

各位同学,咱们今天聊点硬核的。内存监控告警,你光会看指标不行,得先搞懂内存到底是怎么工作的。我见过太多运维兄弟,看到内存报警就慌,加内存、重启、清缓存三板斧,其实很多问题根子出在原理没吃透。

这一章,咱们就把内存的底裤扒干净。物理内存、虚拟内存、寻址、映射、页表……这些概念你搞明白了,以后看监控数据,那叫一个通透。

2.1 物理内存 vs 虚拟内存:一个唱红脸,一个唱白脸

先说物理内存。就是你服务器上插的那根内存条,DDR4、DDR5,多大容量就是多大。程序要运行,指令和数据必须加载到物理内存里,CPU才能直接访问。这个很好理解。

但问题来了。早期操作系统,程序直接操作物理地址。你写个程序,编译出来地址就是物理地址。这有什么毛病?

  • 隔离性差:一个程序写崩了,能把整个系统搞挂。我早年维护过一台老旧的Solaris机器,一个Java进程内存越界,直接把内核数据覆盖了,系统直接panic。那叫一个酸爽。
  • 碎片化严重:程序加载、卸载,物理内存很快变成一片碎渣。大程序申请连续内存,系统只能干瞪眼。
  • 容量受限:物理内存就那么大,程序想用超过物理内存的容量?没门。

所以,虚拟内存登场了。说白了,就是给每个进程一个“假”的内存空间。32位系统下,每个进程看到的是4GB的连续地址空间(0x00000000 - 0xFFFFFFFF)。64位系统就更大了,理论上16EB,实际受硬件限制,但也足够你折腾。

核心观点:虚拟内存是操作系统给每个进程画的“大饼”。进程以为自己独占整个内存空间,实际上它用的地址是虚拟地址,需要经过转换才能落到物理内存上。

你想想看,这有什么好处?

  • 进程隔离:每个进程的虚拟地址空间是独立的。A进程访问0x1234,B进程也访问0x1234,它们映射到不同的物理地址,互不干扰。一个进程挂了,不会拖累整个系统。
  • 简化编程:程序员不用管物理内存布局,不用操心碎片问题。你只管用你的虚拟地址,剩下的操作系统帮你搞定。
  • 支持超量使用:物理内存只有8GB,但你可以跑一堆总需求超过8GB的进程。为什么?因为不是所有数据都在物理内存里,不常用的数据可以换到磁盘上(Swap)。

个人经验:我在监控一个高并发Web服务时,发现物理内存使用率只有60%,但Swap使用率却在缓慢增长。很多人觉得物理内存没用完,Swap不该用。其实这是虚拟内存的正常行为——内核可能把一些长时间不活跃的进程页换出,给文件缓存腾空间。但如果你发现Swap持续增长,同时物理内存还有大量空闲,那就要检查是不是内存泄漏或者配置问题了。

2.2 内存寻址与映射:CPU怎么找到数据?

好,现在每个进程都有了自己的虚拟地址空间。但CPU最终还是要从物理内存里拿数据。这中间怎么转换?

这个过程叫地址翻译。CPU执行指令时,用的是虚拟地址。这个地址会送到内存管理单元(MMU),MMU负责把虚拟地址翻译成物理地址,然后去物理内存里取数据。

翻译过程大致是这样的:

  1. CPU发出一个虚拟地址,比如0x00401000。
  2. MMU把这个地址拆成三部分:页目录索引、页表索引、页内偏移。
  3. MMU去查页目录和页表,找到对应的物理页框号。
  4. 物理页框号 + 页内偏移 = 最终的物理地址。
  5. MMU拿着这个物理地址去访问物理内存。

这个过程听起来复杂,但硬件实现得非常快。不过,如果每次地址翻译都要去内存里查页表,那性能就崩了。所以CPU里有个叫TLB(Translation Lookaside Buffer)的缓存,专门缓存最近用过的虚拟地址到物理地址的映射。TLB命中,一次翻译就几个CPU周期;TLB未命中,那就得走慢速路径,去内存里查页表,几十上百个周期就出去了。

避坑指南:我曾经优化过一个数据库实例,发现CPU的L1/L2缓存命中率都很高,但内存延迟就是降不下来。查了一圈,发现是TLB miss率偏高。原因是数据库用了大量的内存映射文件(mmap),虚拟地址空间跨度很大,TLB覆盖不住。解决方案是启用大页(Huge Pages),让一个TLB条目覆盖更大的内存范围。效果立竿见影,延迟降了30%。

2.3 内存页与页表机制:分而治之的艺术

虚拟内存和物理内存之间,不是按字节一一映射的。那样的话,页表会大到无法想象。操作系统把内存分成固定大小的块,叫(Page)。物理内存这边,对应的块叫页框(Page Frame)。

标准页大小通常是4KB。为什么是4KB?历史原因,早期硬件就这么设计的。4KB的页,管理起来粒度够细,但页表条目也很多。一个4GB的虚拟地址空间,需要4GB / 4KB = 1,048,576个页表条目。每个条目4字节,光页表就要4MB。每个进程一份,100个进程就是400MB,这开销不小。

所以现代CPU都支持多级页表。以x86-64为例,通常使用4级页表:

级别 名称 作用
L4 PML4(Page Map Level 4) 顶级页表,指向下一级页目录
L3 Page Directory Pointer Table 页目录指针表,指向页目录
L2 Page Directory 页目录,指向页表
L1 Page Table 页表,指向物理页框

多级页表的好处是:如果某个虚拟地址范围没有被使用,对应的中间级页表条目可以标记为“无效”,不需要分配下一级页表。这样,大部分进程的页表实际占用的内存远小于4MB。

页表条目里不光有物理页框号,还包含一些标志位:

  • Present位:这个页是否在物理内存中。如果为0,访问这个页会触发缺页异常(Page Fault)。
  • RW位:读写权限。0表示只读,1表示可读写。
  • User/Supervisor位:用户态还是内核态才能访问。
  • Dirty位:这个页是否被写过。写过的页,换出到Swap时需要写回磁盘。
  • Accessed位:这个页最近是否被访问过。操作系统用这个位来实现页面置换算法。

关键点:缺页异常(Page Fault)是虚拟内存的核心机制。它分两种:

  • 软缺页:页不在物理内存,但数据在Swap分区或文件映射中。需要从磁盘读入。这是正常的,但频繁发生会导致性能下降。
  • 硬缺页:访问了非法地址,比如空指针、越界访问。这通常意味着程序有bug,会收到SIGSEGV信号,直接崩溃。

监控时,重点关注Major Page Fault(主缺页中断),它意味着磁盘I/O。如果这个指标飙升,说明物理内存不足,系统在频繁换页,性能会急剧恶化。

2.4 大页(Huge Pages):性能优化的利器

前面说了,标准4KB页,TLB覆盖范围有限。对于内存密集型应用(数据库、Java虚拟机、大数据计算),TLB miss会成为瓶颈。

大页就是解决这个问题的。x86-64支持2MB和1GB的大页。一个2MB的大页,相当于512个4KB小页。TLB条目不变,但覆盖的内存范围扩大了512倍。TLB命中率自然就上去了。

使用大页有两种方式:

  • 透明大页(Transparent Huge Pages, THP):内核自动将连续的4KB小页合并成大页。对应用透明,但有时会导致内存碎片和性能抖动。我建议在数据库服务器上关闭THP,手动管理大页。
  • 显式大页(Explicit Huge Pages):通过hugetlbfs文件系统或mmap的MAP_HUGETLB标志,应用主动申请大页。需要提前预留大页内存。

我的习惯:在部署Elasticsearch或MySQL时,我通常会做两件事:一是关闭透明大页(echo never > /sys/kernel/mm/transparent_hugepage/enabled),二是根据内存大小预留显式大页。比如一台128GB内存的机器,预留64GB给大页,剩下的给操作系统和其他进程。这样既保证了数据库的性能,又不会让系统因为大页耗尽而崩溃。

2.5 总结:从原理到监控

好了,这一章的内容不少。咱们捋一捋:

  • 物理内存是硬件,虚拟内存是操作系统给进程的抽象。两者通过MMU和页表建立映射。
  • 内存寻址是CPU、MMU、TLB、页表协同工作的过程。TLB miss是性能杀手。
  • 页表机制是虚拟内存的基石。多级页表节省内存,页表条目里的标志位控制着访问权限和换入换出。
  • 大页是优化TLB命中率的有效手段,但需要合理配置。

搞懂了这些,你再去看监控指标:

  • MemTotalMemFreeMemAvailable —— 物理内存的总体情况。
  • SwapTotalSwapFree —— 虚拟内存的交换空间。
  • pgfaultpgmajfault —— 缺页中断,特别是主缺页中断,是内存压力的直接信号。
  • nr_tlb_remote_flush —— TLB刷新次数,多核场景下可能成为瓶颈。

下一章,咱们就拿着这些原理,去搭建真正能用的内存监控告警体系。到时候你会发现,原理懂了,配置告警阈值、分析告警原因,那都不是事儿。