Linux内存水位线计算与GDB单步调试实战

发布时间:2026/10/27 11:41:35
Linux内存水位线计算与GDB单步调试实战 水位线watermark这三个数字几乎所有讲 Linux 内存管理的资料都会提一嘴但真要追问一句我把min_free_kbytes改成 16384 之后ZONE_NORMAL 的 low 为什么是 2536 而不是 2500能当场算明白的人其实不多。我在排查一个内存回收跟不上分配速度的问题时就是卡在这个地方/proc/zoneinfo里 free 明明远高于 min分配却开始走直接回收pgscan_direct一路飙。最后是靠 GDB 挂到 QEMU 上单步跟着__setup_per_zone_wmarks和__zone_watermark_ok走了一遍才把水位线怎么算出来的和分配时怎么用它的这两件事对上账。这篇东西写给两类人一类是想真正搞懂页分配器水箱模型的内核初学者另一类是线上遇到内存明明够用却开始回收这类玄学问题、需要拿调试器实锤的运维和驱动开发。全文围绕单步调试这条主线把水位线的计算公式、断点怎么下、GDB 里怎么读struct zone、以及为什么free 够但分配还是失败讲透。所有代码片段以 5.x 到 6.x 的主线源码为参照不同版本细节会有出入落到你自己手上的内核树时请以实际源码为准。1. 水位线不是拍脑袋定的三个数1.1 min_free_kbytes 到 pages_min 之间那次右移很多资料会告诉你水位线由min_free_kbytes推导而来然后就没了。真正值得抠的是__setup_per_zone_wmarks()里那一行unsigned long pages_min min_free_kbytes (PAGE_SHIFT - 10);PAGE_SHIFT在 4K 页的 x86_64 上是 12所以右移量是 2。为什么要右移 2 位因为min_free_kbytes的单位是 KB而内核要的是页数。一个页 4KB1KB 乘 4 等于 1 页反过来说 KB 数除以 4 就是页数除以 4 在二进制里就是右移 2 位。这是纯粹的进制换算不是某种经验系数。但这里有个极容易被忽略的分支如果你跑的是 64K 大页的 ARM64 或者 PPC64PAGE_SHIFT是 16右移量变成 6也就是除以 64。同样一个min_free_kbytes 8192在 4K 页机器上换成 2048 页在 64K 页机器上只换成 128 页。而 128 页 × 64KB 8MB物理意义是一致的。我见过有人在 64K 页平台上照抄 4K 页的调优参数结果水位线数值差了 16 倍回收压力直接压到分配路径上。这一步换算之所以要放在调试的第一站是因为它是整条计算链的源头。你后面看到的 min、low、high 三个数全都是从这个pages_min出发的。源头算错后面单步走得再细都是白费。我习惯在 GDB 里断在__setup_per_zone_wmarks入口第一件事就是p min_free_kbytes和p/x PAGE_SHIFT把这个换算手算一遍。再补一个背景min_free_kbytes的默认值不是固定的它由init_per_zone_wmark_min()在启动时算出来lowmem_kbytes nr_free_buffer_pages() * (PAGE_SIZE 10); new_min_free_kbytes int_sqrt(lowmem_kbytes * 16);nr_free_buffer_pages()大致是可管理的低端内存页数会扣掉一些保留区乘PAGE_SIZE 10得到 KB 数。然后开根号乘 16。把它化简一下设物理内存为 M MBlowmem_kbytes ≈ M * 1024那么int_sqrt(16 * M * 1024) 4 * 32 * sqrt(M) 128 * sqrt(M)。所以有个非常好记的口诀默认 min_free_kbytes ≈ 128 × √(内存MB数) KB。物理内存默认 min_free_kbytes理论值折算成页4K1 GB4096 KB10244 GB8192 KB204816 GB16384 KB409664 GB32768 KB8192256 GB 及以上65536 KB被上限截断16384最后一行是重点init_per_zone_wmark_min()里有if (min_free_kbytes 65536) min_free_kbytes 65536;。也就是说再大的机器默认水位线也只到 64MB 就封顶了。这条封顶规则在大内存机器上经常被人忽略——机器有 1TB 内存默认最低水位线还是 64MB占总量不到万分之一。如果你的业务有突发性大块分配这个默认值基本等于没设保护。1.2 LOW 和 HIGH 的增量用的是 tmp不是 zone 自己的 min这是我在单步时真正被卡住的地方也是大多数二手资料写错的地方。看代码tmp (u64)pages_min * zone_managed_pages(zone); do_div(tmp, lowmem_pages); ... zone-_watermark[WMARK_MIN] tmp; zone-_watermark[WMARK_LOW] min_wmark_pages(zone) (tmp 2); zone-_watermark[WMARK_HIGH] min_wmark_pages(zone) (tmp 1);tmp是把全局的pages_min按当前 zone 的managed_pages占全部低端内存的比例缩放出来的每 zone 份额。对ZONE_HIGHMEM分支还有额外处理页数除以 1024然后 clamp 到SWAP_CLUSTER_MAX到 128 之间但那个分支在 64 位平台上基本走不到。关键点在于 LOW 和 HIGH 的增量tmp 2和tmp 1。这两个值基于tmp也就是缩放后的 min而不是min_wmark_pages(zone)本身。单节点单 zone 的情况下两者相等你根本看不出区别一旦跑到 NUMA 多节点或者 zone 之间有大小差异差别立刻暴露。举个例子假设两个 NUMA 节点节点 0 有 60% 的内存、节点 1 有 40%。全局pages_min是 4000 页。那么节点 0 的tmp 2400节点 1 的tmp 1600。节点 0 的 low 是2400 600 3000high 是2400 1200 3600节点 1 的 low 是1600 400 2000high 是1600 800 2400。如果你按low min × 5/4去算得到的是节点 0 low 3000碰巧对因为单 zone 时 tmp min、节点 1 low 2000也碰巧对。真正会出岔子的是ZONE_HIGHMEM或某些被 clamp 过的路径此时min_wmark_pages(zone)已经被改写过而tmp还是原值两个增量就不一样了。单步调试时想验证这一点最直接的做法是在 GDB 里断到赋值那几行连续next三次每次都p tmp、p zone-_watermark[0]、p zone-_watermark[1]、p zone-_watermark[2]。三次打印之间tmp不变但_watermark[0]会变化你就能直观看到增量是常数、基准是变量这个关系。还有个语义层面的坑LOW 和 HIGH 之间那一档差值tmp 2是异步回收的缓冲区。free 掉到 LOW 以下kswapd 被唤醒但分配还能继续掉到 MIN 以下普通分配直接被挡回走直接回收。所以tmp 2这个量决定了从发现压力到真正卡住之间有多少缓冲。默认公式给的是 min 的 25%机器越大绝对量越大、但比例不变。对延迟敏感的业务调的往往不是 min 本身而是这个隐含比例——只不过没法直接改只能靠改min_free_kbytes间接放大绝对值。1.3 一个 4G 单节点的完整算例把账算平光看公式没感觉手工算一遍最实在。假设一台单节点 x86_64 机器4GB 物理内存没有 CMA/proc/zoneinfo显示 ZONE_NORMAL 的managed是 1000000 页约 3.8GB差额被内核镜像和保留区吃掉了。第一步启动时算默认min_free_kbytes。nr_free_buffer_pages()大概是 990000 页左右lowmem_kbytes 990000 × 4 3960000KB。int_sqrt(3960000 × 16) int_sqrt(63360000) ≈ 7960。所以默认值是 7960 KB 上下。第二步pages_min 7960 2 1990页。第三步lowmem_pages就等于 ZONE_NORMAL 的 managed非 highmem 全算进去即 1000000。第四步tmp 1990 × 1000000 / 1000000 1990。第五步三个水位线min 1990low 1990 (1990 2) 1990 497 2487high 1990 (1990 1) 1990 995 2985。拿这个结果去对/proc/zoneinfo如果你看到的是 1989/2486/2984 或者类似差一两个的数那是整除截断正常。如果差得离谱比如 min 是几百那八成是managed的实际值和你预估的不一样或者机器上有 CMA、ZONE_MOVABLE参与分摊。这一步对账看着琐碎但它能帮你验证我理解的公式是不是内核实际跑的公式后面所有单步都以这个为基准。顺便记一下单位陷阱/proc/zoneinfo里的 min/low/high 报的是页数不是 KB。很多人把它当 KB 看然后觉得我设了 8MB怎么显示 2000其实 2000 × 4KB 8MB完全对得上。2. 把断点打进页分配热路径的正确姿势2.1 QEMU 的 gdbstub 参数和 nokaslr 为什么不能省单步调试内核和单步调试用户态程序最大的区别在于你面对的是一个已经跑起来、可能正在疯狂分配页的操作系统而且地址随机化默认开着。启动参数少一个nokaslr你在 GDB 里通过vmlinux符号算出来的地址和运行时地址就会差一个随机偏移断点要么打不上要么打到一个完全无关的位置上然后你对着崩溃现场怀疑人生。我常用的最小启动命令长这样qemu-system-x86_64 \ -kernel arch/x86/boot/bzImage \ -append consolettyS0 root/dev/sda rw nokaslr maxcpus1 panic-1 \ -drive filerootfs.img,formatraw \ -nographic \ -m 4G \ -s几个参数值得单独说。nokaslr关掉内核地址随机化保证符号地址和运行时一致maxcpus1强制单核启动理由在 2.3 讲-s等价于-gdb tcp::1234把 gdbstub 开在 1234 端口-nographic让串口输出直接进终端方便你在 GDB 停住的时候看内核日志有没有刷出 panic。内核配置侧至少要开这几项CONFIG_DEBUG_INFO本质要求没有调试信息你连局部变量都看不到、CONFIG_DEBUG_INFO_DWARF4或 DWARF5跟你的 GDB 版本匹配、CONFIG_GDB_SCRIPTS可选能让你用lx-symbols自动加载模块符号、CONFIG_FRAME_POINTER栈回溯会舒服很多。如果机器上开着CONFIG_DEBUG_INFO_BTF也没关系不影响 GDB。GDB 这一侧的初始化序列gdb vmlinux (gdb) set auto-load safe-path / (gdb) target remote :1234 (gdb) set print pretty on (gdb) hbreak __setup_per_zone_wmarks (gdb) continueset auto-load safe-path /是为了让 GDB 自动加载内核源码树里的vmlinux-gdb.py没有这一句你会看到一堆 auto-loading has been declined 的提示。加载成功后就能用lx-symbols、lx-ps、lx-dmesg这些内核专用命令。至于用hbreak还是break热路径上的断点我强烈建议用硬件断点。软件断点的工作方式是往目标地址写一条int3指令而在内核里改写代码段是有副作用的——某些内核开着CONFIG_STRICT_KERNEL_RWX代码段只读GDB 写不进去就只能干瞪眼。硬件断点靠调试寄存器实现不碰内存但数量有限x86 上一般就 4 个。所以你要精打细算别一上来就把get_page_from_freelist、__alloc_pages、rmqueue、free_pcppages_bulk全下上。2.2 inline 函数下不了断点这件事三种绕法都试过这是我在实操中撞得最狠的一堵墙。zone_watermark_fast()和__zone_watermark_ok()在头文件里都标着static inline编译器在-O2下会毫不犹豫地把它们内联进get_page_from_freelist()。你在 GDB 里敲break __zone_watermark_ok它可能直接告诉你 Function not defined也可能成功了但断点落在一个你完全没预期的地址上。第一种绕法也是最省事的用文件加行号下断。(gdb) break mm/page_alloc.c:3721即使函数被内联DWARF 里仍然保留了内联实例的行号映射GDB 8 以后支持内联栈帧断在行号上能命中bt也能显示出一层zone_watermark_ok的虚帧。进去之后用frame N切到那个内联帧info locals就能看到mark、free_pages这些变量。这个技巧是我目前用得最多的因为它不需要重新编译内核。第二种绕法给函数加noinline重新编译。改一下源码把static inline改成static noinline单独make mm/page_alloc.o然后重新链接vmlinux。注意不要make clean否则整棵树重编半小时起步。这种方式的好处是断点行为完全可预期坏处是你调试的不是原版内核了某些依赖内联做的优化路径会变测出来的时序数据不能当性能结论用。第三种绕法换个不被内联的锚点。__setup_per_zone_wmarks()本身是普通函数get_page_from_freelist()也是wakeup_kswapd()也是。你可以断在这些位置然后用next/step往里走走到被内联的那段代码时 GDB 会显示出内联帧。虽然后退不了但只要断点的位置选得巧比如断在调用zone_watermark_fast的前一行一样能看到决策前的所有输入。三种方式我一般混着用先用行号断点快速看一眼现场觉得需要反复观察某个量再去改代码加noinline重编。前者快后者准别指望一种方法包打天下。2.3 maxcpus1 这个开关省了我一晚上的时间多核机器上单步调试内核不加限制的话体验极其难受。QEMU 的 gdbstub 在你continue之后会让所有 vCPU 一起跑你刚step完一条指令另一个 CPU 可能已经把那块内存改掉了于是你看到的变量值在两次p之间莫名其妙地变化。理论上可以用set scheduler-locking on来限制但那是 GDB 对本地多线程进程的能力对 gdbstub 上的多 vCPU 并不好使。真正有效的办法就是启动时maxcpus1让内核从根上就只看到一个 CPU。代价是有些依赖多核的路径测不到比如 per-CPU 变量的并发更新、跨 CPU 的 pcp 列表迁移但对于水位线这种纯计算逻辑单核完全够用。如果你确实需要在 SMP 环境下调试可以退一步保留多核但把断点设成只在特定 CPU 上命中用条件断点if smp_processor_id() 0。这个条件本身要在每次命中时求值开销不小在热路径上会显著拖慢系统慎用。还有个细节提醒maxcpus1和nr_cpus1不是一回事。maxcpus限制的是启动时上线的 CPU 数量后面还可以热插拔上来nr_cpus直接改内核的NR_CPUS上限。我们只要前者。另外单步调试期间系统是停住的如果断点打在每分配一个页就要命中的位置整个虚拟机基本等于卡死你的网络、串口、watchdog 全部超时。所以我习惯在断点命中后立刻disable掉或者用ignore跳过前 N 次(gdb) break __setup_per_zone_wmarks (gdb) ignore 1 100这个ignore在启动阶段特别好使因为__setup_per_zone_wmarks在内存热插拔、cgroup 变化、min_free_kbytes写值的时候都会调用启动早期可能会被触发好几次。3. 跟着 __setup_per_zone_wmarks 单步走一遍3.1 在哪一行停下来看 tmp 和 managed_pages断点命中后第一件事是确认自己在哪个上下文。bt一下正常情况下应该能看到setup_per_zone_wmarks→__setup_per_zone_wmarks这样的调用链再往上可能是init_per_zone_wmark_min启动路径或者min_free_kbytes_sysctl_handler写 sysctl 路径。然后确认进入 for 循环前的两个全局量(gdb) p pages_min (gdb) p lowmem_pages (gdb) p min_free_kbytes这三者的关系是pages_min min_free_kbytes (PAGE_SHIFT - 10)lowmem_pages是所有非 highmem zone 的managed_pages之和。如果lowmem_pages和你按/proc/zoneinfo加出来的数对不上那大概率有 zone 被跳过了或者managed_pages里包含了ZONE_MOVABLE。进循环后每一步都值得停一下(gdb) next (gdb) p tmp (gdb) p zone-name (gdb) p zone-managed_pages (gdb) next (gdb) p zone-_watermarkdo_div(tmp, lowmem_pages)这一步要格外小心。do_div是个宏在 32 位平台上展开成一堆指令单步进去会看到一堆看不懂的汇编。我的建议是不要step进do_div直接next过去看结果。在 64 位平台上它通常能正常展开但保险起见还是next。还有个小坑tmp是u64类型GDB 打印时默认十进制如果你习惯性按页数去理解会发现数字大得离谱。加个格式化p/x tmp或者p (unsigned long)tmp。在单 zone 场景下tmp应该等于pages_min如果不是先回头查managed_pages和lowmem_pages。顺带说一句zone_managed_pages()这个访问器。它内部走的是atomic_long_read(zone-managed_pages)在 6.x 上有时候会被内联GDB 单步进去会看到atomic_long_read的展开。不想看这些噪音的话直接读结构体字段zone-managed_pages.counter更快。3.2 NUMA 节点结构体在 GDB 里怎么串起来单节点机器上zone是全局数组contig_page_data.node_zones[]里的元素GDB 里直接p contig_page_data.node_zones[2]._watermark就行。但 NUMA 机器上是另一种结构extern struct pglist_data *node_data[];这是个指针数组。要看节点 1 的 ZONE_NORMAL 水位线(gdb) p node_data[1]-node_zones[2]._watermark那个下标2是enum zone_type的值在 x86_64 上一般是ZONE_DMA0、ZONE_DMA321、ZONE_NORMAL2、ZONE_MOVABLE3、ZONE_DEVICE4。但这个顺序会随CONFIG_ZONE_DMA、CONFIG_ZONE_DMA32、CONFIG_ZONE_DEVICE变化别硬编码。稳妥的做法是先打印zone-name确认(gdb) p node_data[1]-node_zones[2].name $5 Normal\nGDB 会把name数组打全带引号和转义字符看的时候忽略掉就行。遍历所有节点可以手写循环(gdb) set $i 0 (gdb) while $i 2 p node_data[$i]-node_zones[2]._watermark set $i $i 1 end如果CONFIG_GDB_SCRIPTS开着并且lx-symbols加载成功还有更省事的办法——直接用list_for_each_entry的宏展开去看node_states不过说实话节点数一般就个位数手敲几次也不费事。这里提一个我踩过的陷阱GDB 打印_watermark数组的时候默认显示成{1990, 2487, 2985}看起来是三个页数其实它是unsigned long[]。在 32 位内核上unsigned long是 4 字节64 位上是 8 字节这本身不影响读数但如果你用x/命令去看内存字长对不上会读出乱码。用p打印就没事。3.3 写一次 sysctl在断点里看完整的重算过程静态看一遍数据只能验证公式是对的想看变化的完整过程最好的办法是手动触发一次重算。找个 shell执行echo 16384 /proc/sys/vm/min_free_kbytes这个写入路径是min_free_kbytes_sysctl_handler→setup_per_zone_wmarks__setup_per_zone_wmarks会被调用一次。因为此时系统已经跑起来了你的 GDB 断点会立刻命中能看到min_free_kbytes从旧值变成 16384 的全过程。按前面的公式手算一下预期值pages_min 16384 2 4096单 zone 场景下tmp 4096于是 min 4096、low 4096 1024 5120、high 4096 2048 6144。在 GDB 里p zone-_watermark看到这三个数就说明你对公式的理解完全正确。写值的时机也有讲究。如果你在系统启动完成后、内存压力很大的时候去写可能还没等你下完断点写操作就返回了。稳妥的操作顺序是先在 GDB 里把断点挂好、continue然后切到另一个终端写 sysctl让断点在写操作触发的路径上命中。反过来操作的话很可能就错过了。还有一点值得注意/proc/sys/vm/min_free_kbytes写入后不会被你的值永久锁定。如果系统之后触发内存热插拔比如online_pagessetup_per_zone_wmarks可能会被重新调用但user_min_free_kbytes会被记住技术上你的值仍然优先。这个细节在容器环境里尤其重要——很多人以为在容器里写了 sysctl 就生效了其实容器里的/proc/sys/vm/多数是只读的写了会报错或者被静默忽略必须改宿主机上的值。4. __zone_watermark_ok 里藏着的四个减法4.1 free 够不够得先扣掉 unusable free 和 lowmem_reserve单步走完水位线计算接下来该看分配路径怎么用这些数了。入口是zone_watermark_fast()它有个 order-0 的快路径if (order 0 free_pages mark) return true; return __zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags, free_pages);注意free_pages mark用的是严格大于不是大于等于。所以当 free 正好等于 min 的时候快路径不成立会落进__zone_watermark_ok。这个边界条件我在计算水位线的时候栽过——差一个页走的是完全不同的分支日志和计数器表现都不一样。慢路径里最关键的两行free_pages - __zone_watermark_unusable_free(z, order, alloc_flags); if (free_pages min z-lowmem_reserve[highest_zoneidx]) return false;__zone_watermark_unusable_free扣掉的是名义上空闲但拿不到的页不同版本实现略有差异常见的是扣除NR_FREE_CMA_PAGESorder 0 时 CMA 页碎片化严重拿不到。lowmem_reserve扣的是给上层 zone 预留的空间防止低端 zone 分配把高端 zone 饿死。lowmem_reserve的初值由setup_per_zone_lowmem_reserve()算公式是逐级累加do { if (i 0) break; lower_zone pgdat-node_zones idx; lower_zone-lowmem_reserve[j] managed_pages / sysctl_lowmem_reserve_ratio[idx]; managed_pages zone_managed_pages(lower_zone); } while (idx--);sysctl_lowmem_reserve_ratio默认是{1, 256, 32}对应 DMA、DMA32、Normal 的顺序。这个数组的具体值和顺序跟平台强相关x86_64 上只用得到后两档。/proc/zoneinfo里protection那一行显示的(N, M, ...)就是这个数组看的时候别跳过。为什么要把这个扣减放到单步里看因为它是free 明明高于 watermark 却分配失败最常见的原因。你会看到free_pages打印出一个漂亮的大数然后min lowmem_reserve打印出一个更大的数判断直接返回 false。不单步走进去光看/proc/zoneinfo里那个 free你永远想不到还有一层预留扣减。4.2 __GFP_HIGH 和 ALLOC_HARDER 对 min 的放水紧接着还有两行调整if (alloc_flags ALLOC_HIGH) min - min / 2; if (unlikely(alloc_flags ALLOC_HARDER)) min - min / 4;这就是内核里紧急分配可以适当吸水的机制。ALLOC_HIGH对应__GFP_HIGH通常由GFP_ATOMIC这类不睡眠的紧急请求带出来ALLOC_HARDER一般是实时任务或者__GFP_ATOMIC组合出来的。两个标志叠加时min会被逐步削到只剩四分之一标志组合min 的实际门槛都无minALLOC_HIGHmin - min/2约 0.5 minALLOC_HARDE先 /2 再 /4约 0.375 min两者叠加约 0.375 min在 GDB 里想看这个数值可以在__zone_watermark_ok的内联帧里(gdb) frame N (gdb) p min (gdb) p alloc_flagsalloc_flags是个位图你不会直接读它但可以对照gfp.h里的定义判断。调试经验如果你的系统在压力下频繁成功分配但free一直低于 high那多半是原子分配走了放水路径。真正卡住的分配失败来自那些没有__GFP_HIGH的普通请求它们只能老老实实守 min 那条线。还有一个容易被忽视的ALLOC_HARDER只对gfp_mask里带__GFP_ATOMIC或者当前是 RT 任务的场景生效。如果你的业务是普通用户态进程走不到这条路径别指望靠它挤出内存。4.3 order 大于 0 时还得看 free_area 的碎片分布前面那些判断都通过之后__zone_watermark_ok还要做一次碎片检查for (o order; o MAX_ORDER; o) { struct free_area *area z-free_area[o]; int mt; if (!area-nr_free) continue; for (mt 0; mt MIGRATE_PCPTYPES; mt) { if (!free_area_empty(area, mt)) return true; } } return false;这段的意思很直白从请求的 order 开始往上扫只要有任何一个 order 的空闲链表在 UNMOVABLE、MOVABLE、RECLAIMABLE 这三类里非空就认为能分配返回 true。MIGRATE_PCPTYPES就是 3只覆盖这三个迁移类型。坑在哪如果空闲页全在MIGRATE_CMA或者MIGRATE_ISOLATE里这里会返回 false。MIGRATE_ISOLATE一般是内存热插拔隔离出来的页不算可用MIGRATE_CMA只有alloc_flags里带ALLOC_CMA才额外检查普通分配撞不进去。我遇到过一次特别典型的机器上配了大块 CMA 做显示缓冲/proc/zoneinfo里 free 看着挺多/proc/buddyinfo里高 order 的块也不少但普通分配一到 order-9 就失败。单步进去才看清楚free_area[9]里非空的只有 CMA 那个链表MIGRATE_UNMOVABLE早断供了。这个现象光靠看汇总数字是发现不了的必须把/proc/buddyinfo的各列拆开看。顺带把这个排查套路记下来/proc/buddyinfo的每一行是一个 zone每一列从 order-0 到 order-10对应的数字是那个 order 上所有迁移类型的空闲块总数它不区分迁移类型。想知道具体是哪一类只能进 GDB 或者用drgn读zone-free_area[o].free_list[mt]。5. 用 /proc 和 pgalloc 计数器回头看你的单步结论5.1 percpu 计数器的漂移会让账对不平单步看到了精确值回头再去读/proc/zoneinfo经常会发现对不上差个几百页甚至上千页。这时候先别怀疑人生八成是 per-CPU 计数器在作怪。NR_FREE_PAGES是ZONE_STAT里的一个走的是 per-CPU 批量更新每个 CPU 先在本地缓存里加减攒够阈值percpu_pagelist_high_fraction相关的阀值才刷到全局。所以你从/proc/zoneinfo看到的pages free是全局值加上各 CPU 缓存里未刷出的部分本身就有延迟和偏差。想看更准的值有两个办法。一是多次读/proc/zoneinfo取几次结果里比较稳定的那个二是用内核里的 snapshot 路径__zone_page_state_snapshot它会临时汇总各 CPU 的值。GDB 里可以直接调(gdb) p __zone_page_state_snapshot(zone, NR_FREE_PAGES)不过要注意这个函数不是随便什么时候都能调——需要目标栈上下文有效且不能在被中断打断的临界区里调。稳妥的做法还是看/proc/zoneinfo然后心里留个±几百页的容差。我在 GDB 里通常这么对齐先在断点停下时p zone_page_state(zone, NR_FREE_PAGES)拿到精确值记下来然后continue到用户态立刻cat /proc/zoneinfo对比那个 zone 的 free。两者的差值可以帮你知道当前 CPU 缓存积压了多少这个经验值在后续排查中很有用。5.2 kswapd 什么时候被唤醒pgscan 计数器说了算水位线的直接作用是触发回收。free 掉到 LOW 以下wakeup_kswapd会被调用掉到 MIN 以下且回收也顶不住分配就会走direct reclaim也就是在分配路径上同步回收延迟会突然蹿上来。这两个事件都有计数器pgscan_kswapd和pgscan_direct在/proc/vmstat里能读到。单步调试时想验证free 掉到多少会触发 kswapd最直接的做法是人为压一下cat /proc/vmstat | grep -E pgscan_kswapd|pgscan_direct # 记下基线 stress-ng --vm 1 --vm-bytes 80% --timeout 30s cat /proc/vmstat | grep -E pgscan_kswapd|pgscan_direct看pgscan_kswapd有没有涨。如果涨了说明水位线判断生效kswapd 被唤醒。如果pgscan_direct也在涨说明 kswapd 没跟上回收压力已经传导到分配路径上了。再进一步你可以在 GDB 里断wakeup_kswapd看它被调用时的zone-_watermark[WMARK_LOW]和当时的free_pages是不是恰好卡在那条线上。这个验证比看计数器更直接但也更慢因为wakeup_kswapd在压力期调用频率很高。5.3 三个我实际踩过的账不平第一个坑改了min_free_kbytes但/proc/zoneinfo没变。原因是写入被忽略了。容器里/proc/sys/vm/往往只读写入会返回EACCES但脚本

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询