
排查 Linux 内存问题的时候我习惯先看一眼/proc/zoneinfo。这个文件乍看全是数字但只要你搞懂了 Zone内存区域的划分逻辑它几乎就是一台机器内存健康状况的完整体检单。所谓 Linux 内存区域Zone本质上是内核把物理内存按“服务能力”分成若干段每一段有独立的分配策略、回收阈值和碎片控制手段。这篇文章我会把 Zone 为什么存在、六大 Zone 分别管什么、怎么用命令验证、以及生产环境里最常见的 Zone 相关故障都讲透。适合刚接触内存管理的运维、正在准备 Linux 面试的开发以及被“page allocation failure”折磨过的同学。1. 为什么 Linux 要把物理内存切成 Zone先看清内存管理的盘面1.1 一切从硬件限制说起不是所有内存都能干所有事很多人一开始不理解物理内存不就是一串连续的地址吗内核为什么非要人为划线划成 DMA、DMA32、Normal 这些区域答案其实不在软件而在硬件。你想想内存条插在主板上对 CPU 来说是一视同仁的地址空间。但对外设网卡、磁盘控制器、USB 控制器来说情况完全不一样。老式的 ISA 设备只能访问地址低于 16MB 的内存做 DMA 传输后来 32 位 PCI 设备也只能寻址 4GB 以内的空间而 64 位设备才可以访问全部内存。如果内核不把这些地址范围单独标记出来那么驱动程序想要给设备分配一块“设备能访问”的 DMA buffer 时就只能在整个内存空间里碰运气效率极低甚至可能分配失败。所以 Zone 的本质是把物理内存按照“哪些硬件能访问、内核怎么映射”这两条标准做分级。每一级就是一个 ZoneZone 内部有独立的 free list、LRU 链表和水印水位页面分配器会根据请求的 flags 决定从哪个 Zone 找内存。这就好比你家里分了好几个工具柜抽屉 A 放常备螺丝抽屉 B 放专用工具抽屉 C 放不常用的旧零件找东西的时候先按类别定位再在抽屉里翻。1.2 Zone 的本质按“服务能力”给内存分级Linux 内核在include/linux/mmzone.h里用枚举定义了所有 Zone 类型通用版本依次是ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM、ZONE_MOVABLE、ZONE_DEVICE。这里要强调一个关键点不是每个架构、每台机器都会创建全部 Zone内核在启动阶段会根据物理内存大小、架构特性动态决定哪些 Zone 可被使用。还要区分的两个概念是“Zone 的边界”和“pageblock 的对齐”。内核划分 Zone 时会做低对齐和高对齐处理保证每个 Zone 的起始页帧号满足伙伴系统的分配需求。所以你在/proc/zoneinfo里看到的spanned总跨度页数、present实际存在页数、managed纳入伙伴系统管理的页数往往不一样后面我会专门讲这三个字段为什么有差异。理解 Zone 之后你再看malloc、free、OOM、内存回收这些高层概念就会通顺很多用户态见到的是虚拟内存的抽象内核真正在做的就是在一堆 Zone 之间调度物理页。一个申请可能从 Normal 拿页也可能从 DMA32 拿页具体走哪个 Zone由分配标志和 Fallback 顺序共同决定。2. 六大 Zone 逐个拆解谁在什么情况下用哪个区域2.1 ZONE_DMA 与 ZONE_DMA32给老设备留的口子先说这两个最容易混淆的区域。x86 架构下ZONE_DMA是物理内存最开头那 16MB。之所以保留它纯粹是为了兼容 ISA 总线的老设备——它们只有 24 位地址线物理地址超过 16MB 就访问不到。今天你买的服务器上基本没有 ISA 设备但内核仍然保留了 DMA Zone因为很多驱动尤其是声卡驱动和古老的网卡驱动还硬编码会从 DMA Zone 申请内存。x86_64 加入的ZONE_DMA32则是给“只能访问 4GB 以内物理地址”的 32 位 DMA 设备准备的。它覆盖 DMA Zone 结束位置到 4GB 之间的内存。比如某些便宜的 RAID 卡、部分 USB 3.0 控制器它们的 DMA 引擎就只实现了 32 位地址线。内核启动时识别到这类设备就会从 DMA32 Zone 给它们分配可用的 DMA buffer避免数据写到设备访问不了的高地址内存里去。这里分享一下我在实际服务器上观察到的现象健壮的 64 位系统上DMA Zone 经常只有几十上百 MBDMA32 Zone 可能有 1GB 到 3GB剩下的全归 Normal。如果厂商没有老设备这两个 Zone 大部分页都是空闲的。内核参数vm.zone_reclaim_mode和我们常用的分配申请动作绝大多数都不会碰 DMA/DMA32 里的页只有驱动主动带GFP_DMA或GFP_DMA32标志时才会用到。2.2 ZONE_NORMAL常规操作的绝对主力ZONE_NORMAL是 64 位系统里真正的“主角”。它覆盖 DMA32 结束之后一直到物理内存顶端的几乎全部内存。在一个典型的 x86_64 机器上cat /proc/zoneinfo里 Normal Zone 的managed可能占总量 95% 以上。Normal Zone 有一个核心特性这段物理内存被内核线性映射到了虚拟地址空间的高位区域也就是说内核可以直接通过page_address()算出物理页对应的虚拟地址访问效率非常高。内核栈、slab 对象、页表、文件缓存等绝大部分内核结构体都从这里分配。值得一提的是虽然叫 Normal但内核从 Normal Zone 分配页面时也可能发生回收。一旦某个 Zone 的空闲页低于low水位kswapd 内核线程就会被唤醒扫描这个 Zone 的 LRU 链表把不活跃的页写回或丢弃如果申请的优先级很高甚至会发生直接回收direct reclaim这是后面排查内存压力时必须盯住的信号。2.3 ZONE_HIGHMEM32 位时代的特殊产物ZONE_HIGHMEM现在大多数人只在面试题里见过了但它背后的历史能帮你理解整个内存映射设计。32 位系统只有 4GB 虚拟地址空间内核默认分掉 1GB用户态分 3GB。所以内核能直接映射的物理内存最多 896MB还要减去 vmalloc 和 fixmap 区域占用的地址剩下的物理内存必须要动态映射才能访问这一部分就叫 HighMem。换句话说HighMem 不是“高端内存”而是“内核没法一直看到的内存”。HighMem 里的页面可以被用户态映射使用但内核要访问它们时得临时建立映射访问完再撤销。这种机制导致了 32 位系统上内核内存极其宝贵——slab、内核栈、文件描述符这些内核对象总容量被卡在 1GB 以内所以老一代运维都很熟悉“32 位机器上 4GB 内存只能用出 3GB 多”这个经典限制。64 位系统因为地址空间足够大内核线性映射可以覆盖整个物理内存所以根本没有 HighMem Zone。这也是为什么新机器上跑zoneinfo你只看到 DMA、DMA32、Normal、Movable而看不到 HighMem。2.4 ZONE_MOVABLE让内存可以“搬家”ZONE_MOVABLE是这几个 Zone 里设计思想最值得学习的。它的名字直译是“可移动区域”内核在里面存放的页面都是可迁移的——用户态进程的内存、文件缓存这些页面内容可以拷贝到别处再改映射而内核分配的不可移动页如 slab、内核栈不会放到这里。为什么要专门划一块可移动内存核心目的是反碎片。伙伴系统长时间运行后不可移动的页会像钉子一样零散分布在物理内存里导致即使总空闲内存很多也无法凑出连续的大块内存高 order 的内存申请就会失败。如果把不可移动的页全部限制在非 Movable Zone把可移动页集中放到 Movable Zone那么当需要大块连续内存时内核可以通过页迁移把 Movable Zone 里的页面挪走腾出连续空间。ZONE_MOVABLE 的大小有两种来源一种是通过内核参数kernelcore和movablecore显式指定另一种是在内存热插拔memory hotplug场景下自动形成。CMAContiguous Memory Allocator机制也和它紧密相关视频编解码、GPU 驱动申请连续物理内存时常常依赖这套能力。你会在/proc/zoneinfo里看到 Movable Zone在/proc/pagetypeinfo里看到可移动页的分组统计。2.5 ZONE_DEVICE 与其它给特殊场景的补充ZONE_DEVICE是相对较新加入的主要服务于持久内存persistent memory和 GPU 显存这类“设备内存”。设备内存不具备普通 DDR 的特性它不能被内核作为常规 RAM 随意分配但又要纳入页缓存、mapping 等统一框架里管理。ZONE_DEVICE 里的页面由驱动管理生命周期不会出现在合作伙伴关系的伙伴分配器统计中。除了这些标准 Zone实际系统里还有几个和内存区域划分相关的概念容易和 Zone 混淆NodeNUMA 架构下的 NUMA 节点一个 Node 包含多组 ZoneMemory cgroup对内存使用量做限额的逻辑分组和物理 Zone 划分是两条独立维度VMPressure / Watermark基于 Zone 水印触发回收和 OOM 的信号体系。我在看/proc/zoneinfo的时候习惯先确认每个 Node 下有哪些 zone再看每个 Zone 的水印和空闲页是否健康。只要 Zone 结构清楚了这套输出的每一个字段都能对应到实际内存行为。3. 从命令到内核源码看清你的系统里 Zone 到底怎么分的3.1 /proc/zoneinfo一份 Zone 的体检报告先直接跑一下命令看真实输出长什么样。我在一台 64 位 8G 内存的云服务器上执行cat /proc/zoneinfo | grep -E Node|zone|managed|present|spanned关键输出大致如下数值我已简化Node 0, zone DMA spanned 8192 present 7629 managed 3976 Node 0, zone DMA32 spanned 1044480 present 921075 managed 893026 Node 0, zone Normal spanned 1245184 present 1163858 managed 1139863 Node 0, zone Movable spanned 0 present 0 managed 0每个 Zone 下面还有pages free、min、low、high以及一堆nr_*计数器。这里你只需要先看懂三层关系spannedZone 在物理地址空间里的总跨度包含空洞比如内存条插槽之间的地址空缺present实际可用的物理页等于 spanned 减去不能被内核使用的空洞页managed真正纳入伙伴系统、可以被分配的页等于 present 减去内核保留页如 memmap、保留页表等。所以正常情况下spanned present managed。如果这三个数值差距异常大比如 present 远小于 spanned往往是 BIOS 或虚拟机配置导致内存空洞值得排查。3.2 水印机制min、low、high 是怎么算出来的Zone 里最容易被面试官追问的是三个水位min、low、high。它们不是拍脑袋定的而是由内核根据min_free_kbytes和watermark_scale_factor计算出来的。先看计算链路。用户通过sysctl vm.min_free_kbytes设置的是整个系统保留空闲内存的最小值内核把它按各 Zone 的managed页数比例分配到每个 Zone得到一个“基础 min”然后low min * 1.25加上 watermark_scale_factor 影响的增量high max(min * 1.5, min scale_factor 增量)。在较新的内核里watermark_scale_factor默认是 10表示 low 和 min 之间、high 和 low 之间会按 0.1% 的比例再拉开差距。三个水位的实际含义是水位触发条件行为high空闲页高于 highkswapd 进入睡眠low空闲页低于 lowkswapd 被唤醒开始异步回收min空闲页低于 min普通分配进入直接回收只有PF_MEMALLOC等紧急标志才能使用 min 以下内存你可以用下面命令看当前生效的系统级最小值sysctl vm.min_free_kbytes sysctl vm.watermark_scale_factor建议在内存水位紧张、频繁触发直接回收的机器上适当调大min_free_kbytes到物理内存的 0.5%~1%给紧急分配留足缓冲。但不要无脑调大——保留太多空闲页会导致可用内存白白浪费缓存命中率下降。3.3 NUMA 环境下 Zone 的分布规律NUMA 架构下每个 Node 都有自己独立的一套 Zone。比如双路服务器通常是 Node 0 和 Node 1每个 Node 下各有 DMA、DMA32、Normal、Movable。此时“从哪个 Node 分配内存”变成了新的问题。内核为每个 Node 维护一份zonelist分配内存时按优先级遍历先找本 Node 的 Zone再找远端 Node 的 Zone。遍历顺序不是简单的 Zone 编号顺序而是由gfp_zone()和zonelist_order策略决定的。默认策略是“Node 优先”node-local first也就是说能本地分配就绝不去远端因为跨 Node 访问内存的延迟远高于本地。生产环境里常见的一个坑是Node 0 的内存快耗尽、Node 1 还有很多空闲却出现了奇怪的内存压力或性能抖动。原因可能是某些驱动或内核线程绑在了 Node 0分配时始终优先 Node 0导致 Node 0 频繁回收。排查时可以用numactl --hardware查看 Node 与内存的对应关系再用numastat或者/sys/devices/system/node/node*/meminfo确认每个 Node 的内存使用情况。如果不希望进程绑死在某个 Node可以用numactl --interleaveall做内存交错或者通过cpuset调整绑定关系。4. 实操把 Zone 变成能落地的排查工具4.1 判断系统是否存在内存碎片问题内存碎片问题在 zoneinfo 里能看到很明显的特征nr_free_pages总量不小但free的高 order 块很少或者说/proc/buddyinfo里 Order 3、Order 4 以上的空闲页几乎没有。我常用的排查组合是cat /proc/buddyinfo cat /proc/pagetypeinfo | head -80buddyinfo输出每一行里的连续数字代表 Order 0 到 Order 10 的空闲页块数量。看到形如“0 0 0 0 1 2 5 12 30 ...”这种前面大量为 0 的情况就说明系统已经碎片化大块连续内存稀缺。这时候如果有进程申请 order 4即 64KB以上的连续内存就可能触发直接回收甚至出现“page allocation failure”。pagetypeinfo会按页类型Unmovable、Reclaimable、Movable、CMA进一步拆分能帮你判断碎片到底来自哪类页面。如果 Unmovable 的页占了大量低 order 块说明内核对象分配太分散这是最麻烦的情况如果只是 Movable 页多通常可以通过回收缓解。4.2 处理 Zone 分配失败的经典场景内核在物理内存不足或者分配连续大块失败时会往 dmesg 打印类似这样的日志java: page allocation failure: order:4, mode:0xcc0(GFP_KERNEL)看到order:4不代表你缺 64KB 内存而是缺“物理上连续的 64KB”。很多应用比如 JVM 的 DirectByteBuffer、DPDK 的大页、某些网卡驱动都有这种连续内存需求。排查和解决的思路通常按顺序来先确认是不是真的碎片化。执行cat /proc/buddyinfo如果 Order 0/1 块很多但高 order 块很少那基本确定是碎片。临时触发内存 compaction。echo 1 /proc/sys/vm/compact_memory可以让内核尝试压缩内存把可移动页挪走、合并出大块。查清楚是谁在申请高 order 内存。用cat /proc/vmstat | grep -E compact|thp观察 compaction 和透明大页THP的统计必要时在应用层关闭 THP 或改用普通页对齐分配。如果是长期问题考虑调整 ZONE_MOVABLE 的比例把更多内存划成可移动区或者给应用分配 hugpages大页减少高 order 申请频率。这里要特别提醒compact_memory只能临时救急重启后恢复原状。根治思路是让应用少申请大块连续内存或者用 CMA 提前预留连续内存区域。4.3 内核参数调优的边界在哪里与 Zone 最直接相关的内核参数有三个vm.min_free_kbytes、vm.watermark_scale_factor、vm.zone_reclaim_mode。zone_reclaim_mode控制着本地 Zone 内存不足时是否回收本地内存而不是去远端 Node 借内存。默认通常是 0允许跨 Node 分配在某些低延迟应用里可以改成 1 强制先回收本地但要小心这会增大延迟和 CPU 消耗不一定划算。给新手一个相对稳妥的调优起点sysctl -w vm.min_free_kbytes131072 # 比如 128MB具体按总内存 0.5% 左右估 sysctl -w vm.watermark_scale_factor20改完之后用sysctl -p持久化。不过我不建议在生产环境一次性大幅调整更稳的做法是分步调、每步观察zoneinfo里的水位变化和应用侧的错误日志。记住一点Zone 参数的调整是给紧急分配留余量不能解决内存总量不足的问题总量不够时该加内存就加内存。5. 高频面试题与常见误区盘点5.1 为什么 zoneinfo 里的 present/managed 不一致这个问题我见很多人答错。present 和 managed 的差异来自内核启动时保留的内存包括memmap页结构体数组本身占用的内存内核代码段、数据段、initrd 等已占用的页某些架构为 DMA 保留的页被mem或crashkernel参数预留的内存。这些保留页在 Zone 中以PageReserved或直接不被纳入伙伴系统的方式存在。所以 managed 才是伙伴系统真正能分的页。面试时如果能把这个差异讲清楚会显得你对内核内存初始化有真实理解。5.2 HighMem 到底还能不能见到很多人在网上看 32 位内存管理的文章以为现在还能经常碰到 HighMem。实际在服务器和企业环境里CONFIG_HIGHMEM早已不是主流64 位内核默认不开启 HighMem。你唯一可能见到它的场景是某些嵌入式 32 位环境、旧 ARM 板子以及面试题。如果要找 HighMem 和 Normal 的分界线不同内核版本具体阈值不同常见的是 1:3 或 2:2 的 kernel/user 地址空间划分。但你不必死记硬背具体数字理解“HighMem 是因为内核映射地址不够才存在的”就够了。5.3 Zone 与 cgroup 内存限制的关系这是很多人混淆的地方cgroup 的内存限制是针对 memcg 的和物理 Zone 划分不是一回事。cgroup 限制的是“这个 cgroup 里的进程能用多少内存”Zone 限制的是“某个物理地址范围内的内存能由谁分配”。当 cgroup 达到限额时内核会回收该 cgroup 的页或者触发 OOM但这并不影响其他 cgroup 使用别的 Zone。但两者有交互memcg 回收时也会走到shrink_node、shrink_zone这些基于 Zone/LRU 的回收路径。所以你会看到 cgroup 内存压力大时对应 Node 某个 Zone 的pgscan、pgsteal计数也会上涨。排查时如果发现 cgroup 统计正常、但整个 Node 的内存水位很低就要回到 Zone 视角看是不是 NUMA 分配不均导致的问题。6. 我在真实运维里总结的几点心得最后分享几个不常写进文档里的实操体会。第一/proc/zoneinfo是你判断内存压力最直接的入口。很多人只会看free -h但free里的 available 是个估算值而 zoneinfo 里的pages free与min/low/high的组合能精确告诉你“当前离 kswapd 唤醒还有多远”。我观察过几次故障free显示还有 1GB 内存但某个 Zone 已经跌破 min进程分配时频繁 direct reclaim这很容易被忽视。第二调min_free_kbytes不要盲目照抄网上的经验值。它影响的不只是水位还会改变整个回收节奏。我曾经把一台 32G 机器的min_free_kbytes从默认值调到 1GB结果 kswapd 经常提前抢内存应用性能反而下降。后来按 0.5% 总内存设成约 160MB 才稳定下来。合理的做法是先记录当前水位和回收统计再每次调整 25% 左右观察一两天。第三碎片问题远比容量问题隐蔽。如果你在 dmesg 里看到page allocation failure先别着急加内存。先跑一遍buddyinfo看高 order 块的分布再决定是开 compaction、调 CMA 还是给应用配大树。我一个项目里就是通过给 Redis 开启透明大页相关的连续分配优化彻底解决了偶发延迟而没有动硬件。Zone 这套机制理解透了Linux 内存管理的一半你就算真正入门了。从zoneinfo到水印再到回收策略整个链路是有迹可循的排查问题时按“Zone 是否健康 → 水位是否触发 → 回收是否有效”这个顺序往下走基本不会迷路。