news 2026/9/9 2:48:07

Linux内存Zone深度解析:从/proc/zoneinfo到故障排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内存Zone深度解析:从/proc/zoneinfo到故障排查

排查 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 的本质,是把物理内存按照“哪些硬件能访问、内核怎么映射”这两条标准做分级。每一级就是一个 Zone,Zone 内部有独立的 free list、LRU 链表和水印水位,页面分配器会根据请求的 flags 决定从哪个 Zone 找内存。这就好比你家里分了好几个工具柜:抽屉 A 放常备螺丝,抽屉 B 放专用工具,抽屉 C 放不常用的旧零件,找东西的时候先按类别定位,再在抽屉里翻。

1.2 Zone 的本质:按“服务能力”给内存分级

Linux 内核在include/linux/mmzone.h里用枚举定义了所有 Zone 类型,通用版本依次是ZONE_DMAZONE_DMA32ZONE_NORMALZONE_HIGHMEMZONE_MOVABLEZONE_DEVICE。这里要强调一个关键点:不是每个架构、每台机器都会创建全部 Zone,内核在启动阶段会根据物理内存大小、架构特性动态决定哪些 Zone 可被使用。

还要区分的两个概念是“Zone 的边界”和“pageblock 的对齐”。内核划分 Zone 时会做低对齐和高对齐处理,保证每个 Zone 的起始页帧号满足伙伴系统的分配需求。所以你在/proc/zoneinfo里看到的spanned(总跨度页数)、present(实际存在页数)、managed(纳入伙伴系统管理的页数)往往不一样,后面我会专门讲这三个字段为什么有差异。

理解 Zone 之后,你再看mallocfree、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 经常只有几十上百 MB,DMA32 Zone 可能有 1GB 到 3GB,剩下的全归 Normal。如果厂商没有老设备,这两个 Zone 大部分页都是空闲的。内核参数vm.zone_reclaim_mode和我们常用的分配申请动作,绝大多数都不会碰 DMA/DMA32 里的页,只有驱动主动带GFP_DMAGFP_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_HIGHMEM:32 位时代的特殊产物

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)场景下自动形成。CMA(Contiguous 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 混淆:

  • Node:NUMA 架构下的 NUMA 节点,一个 Node 包含多组 Zone;
  • Memory 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 freeminlowhigh,以及一堆nr_*计数器。这里你只需要先看懂三层关系:

  • spanned:Zone 在物理地址空间里的总跨度,包含空洞(比如内存条插槽之间的地址空缺);
  • present:实际可用的物理页,等于 spanned 减去不能被内核使用的空洞页;
  • managed:真正纳入伙伴系统、可以被分配的页,等于 present 减去内核保留页(如 memmap、保留页表等)。

所以正常情况下spanned >= present >= managed。如果这三个数值差距异常大,比如 present 远小于 spanned,往往是 BIOS 或虚拟机配置导致内存空洞,值得排查。

3.2 水印机制:min、low、high 是怎么算出来的

Zone 里最容易被面试官追问的是三个水位:minlowhigh。它们不是拍脑袋定的,而是由内核根据min_free_kbyteswatermark_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 --interleave=all做内存交错,或者通过cpuset调整绑定关系。

4. 实操:把 Zone 变成能落地的排查工具

4.1 判断系统是否存在内存碎片问题

内存碎片问题在 zoneinfo 里能看到很明显的特征:nr_free_pages总量不小,但free的高 order 块很少,或者说/proc/buddyinfo里 Order 3、Order 4 以上的空闲页几乎没有。

我常用的排查组合是:

cat /proc/buddyinfo cat /proc/pagetypeinfo | head -80

buddyinfo输出每一行里的连续数字代表 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 的大页、某些网卡驱动)都有这种连续内存需求。排查和解决的思路通常按顺序来:

  1. 先确认是不是真的碎片化。执行cat /proc/buddyinfo,如果 Order 0/1 块很多但高 order 块很少,那基本确定是碎片。
  2. 临时触发内存 compaction。echo 1 > /proc/sys/vm/compact_memory可以让内核尝试压缩内存,把可移动页挪走、合并出大块。
  3. 查清楚是谁在申请高 order 内存。用cat /proc/vmstat | grep -E "compact|thp"观察 compaction 和透明大页(THP)的统计,必要时在应用层关闭 THP 或改用普通页对齐分配。
  4. 如果是长期问题,考虑调整 ZONE_MOVABLE 的比例,把更多内存划成可移动区,或者给应用分配 hugpages(大页),减少高 order 申请频率。

这里要特别提醒:compact_memory只能临时救急,重启后恢复原状。根治思路是让应用少申请大块连续内存,或者用 CMA 提前预留连续内存区域。

4.3 内核参数调优的边界在哪里

与 Zone 最直接相关的内核参数有三个:vm.min_free_kbytesvm.watermark_scale_factorvm.zone_reclaim_mode

zone_reclaim_mode控制着本地 Zone 内存不足时是否回收本地内存而不是去远端 Node 借内存。默认通常是 0(允许跨 Node 分配),在某些低延迟应用里可以改成 1 强制先回收本地,但要小心这会增大延迟和 CPU 消耗,不一定划算。

给新手一个相对稳妥的调优起点:

sysctl -w vm.min_free_kbytes=131072 # 比如 128MB,具体按总内存 0.5% 左右估 sysctl -w vm.watermark_scale_factor=20

改完之后用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_nodeshrink_zone这些基于 Zone/LRU 的回收路径。所以你会看到 cgroup 内存压力大时,对应 Node 某个 Zone 的pgscanpgsteal计数也会上涨。排查时如果发现 cgroup 统计正常、但整个 Node 的内存水位很低,就要回到 Zone 视角看是不是 NUMA 分配不均导致的问题。

6. 我在真实运维里总结的几点心得

最后分享几个不常写进文档里的实操体会。

第一,/proc/zoneinfo是你判断内存压力最直接的入口。很多人只会看free -h,但free里的 available 是个估算值,而 zoneinfo 里的pages freemin/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 是否健康 → 水位是否触发 → 回收是否有效”这个顺序往下走,基本不会迷路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 2:46:19

2026桌面AI助手横评:能聊天的遍地都是,能干活的才值得推荐

1. 2026年的桌面AI助手,比的不再是“谁话多”如果你心里还装着2024年那套“桌面AI助手一个能聊天的悬浮窗”的印象,这篇横评可能会推翻你大半的判断。2026年再聊桌面AI助手,我最大的感受是:能聊天的满地都是,能在你电脑…

作者头像 李华
网站建设 2026/9/9 2:45:45

SpringBoot+Vue3+MyBatis+MySQL前后端分离的小型企业CRM系统实战

做小型企业CRM系统,最怕的就是一上来就撸代码,做到一半才发现表结构不合理、接口设计混乱,前后端联调的时候改来改去。这个项目我前前后后搭过三轮,这一版用SpringBootVue3MyBatisMySQL前后端分离的方案,算是在小型团队…

作者头像 李华
网站建设 2026/9/9 2:45:25

STM32官方例程食用指南:从GPIO点灯到USB与Bootloader实战

简介:STM32官方例程是ST公司为基于ARM Cortex-M内核的微控制器打造的参考代码合集,面向嵌入式初学者、进阶开发者以及需要快速验证外设功能的工程师,能够帮助理解中断、时钟、GPIO、定时器、串口、ADC与DMA等核心模块。压缩包内共504个文件&a…

作者头像 李华
网站建设 2026/9/9 2:43:09

Qt实时曲线图实战:QCustomPlot+kissfft串口数据可视化与性能调优

简介:面向QT与C开发者的曲线图制作实战资源,适合需要掌握QT绘图机制、模型/视图架构及自定义图形项的中级开发者。资源围绕曲线图完整实现展开,覆盖QPainter绘图、QGraphicsView/Scene场景搭建、QGraphicsPathItem与QPainterPath路径构建&…

作者头像 李华
网站建设 2026/9/9 2:41:12

Qt桌面项目架构实战:从MVC到MVVM的演进与模块划分

接手过一个别人留下的 Qt 桌面项目。MainWindow.cpp 六千多行,按钮的槽函数里直接写数据库查询,UI 线程上跑网络请求,一个 QTableWidget 塞进去几万行数据,拖动滚动条都能感觉到明显的迟滞。代码不是不能跑,而是没人敢…

作者头像 李华
网站建设 2026/9/9 2:40:46

51单片机贪食蛇游戏机设计与实现:从硬件到代码全解析

简介:这是一份基于89C52单片机的贪食蛇游戏机完整设计,面向51单片机初学者、嵌入式课程设计以及电子制作爱好者,可帮助从零搭建一个多功能交互娱乐项目。项目使用清翔MCS51开发板,代码结构清晰,并提供上位机软件&#…

作者头像 李华