登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux cgroup v2 memory.stat 怎样区分匿名页和文件页

来源:17golang原创

时间:2026-10-25 14:14:42 405浏览 收藏

排查 Linux cgroup v2 的内存占用时,memory.stat 最适合回答“这部分内存大致属于哪一类”,而不是直接回答“哪个进程泄漏了”。它把 cgroup 的内存足迹拆成键值对:anon 代表匿名映射,file 代表文件系统缓存(也包括 tmpfs 和共享内存),kernel 则是内核内存总量。阅读时应按键名取值,不能按输出行号取值。

官方文档:https://docs.kernel.org/admin-guide/cgroup-v2.html

先用 memory.current 确认总量,再用 memory.stat 区分匿名映射、文件缓存和内核开销;如果文件类占比明显,再看 file_mapped、file_dirty 等细项,最后把结果与 memory.events 的事件计数分开解释。

memory.stat 到底统计了什么

memory.stat 是只读的扁平键值文件,数值单位是字节。内核文档特别提醒:字段顺序可能在中间新增,程序应通过键名寻找字段。这个规则看似简单,却是很多临时排障脚本最容易忽略的地方。

memory.stat 将 cgroup 内存足迹拆分为匿名映射、文件缓存和内核开销的说明图
图1:memory.stat 的内存类型拆分说明图,展示匿名映射、文件缓存和内核开销的静态关系;这是说明图,不是运行截图。
字段可以怎样理解不要怎样理解
anon匿名映射使用的内存,例如 brk、sbrk 和匿名 mmap不等于某个进程的完整 RSS
file文件系统数据缓存,包括 tmpfs 与共享内存不等于“已经写入磁盘的文件大小”
kernel内核内存总量,包含内核栈、页表、percpu、vmalloc、slab 等不等于应用运行时的 native heap

因此,看到 file 变大时,不要马上把结论写成“磁盘缓存泄漏”;看到 anon 变大时,也不要直接断言某个对象一定没有释放。它们是 cgroup 层面的分类线索,下一步还要结合进程、映射和业务行为。

按键名读取三个主分类

下面的示例只读取接口文件,不修改 cgroup 配置。把 /sys/fs/cgroup/workload.slice 换成目标 cgroup 的实际路径。用 awk 按第一个字段匹配,避免依赖文档输出的视觉顺序。

# 把路径替换成待排查的 cgroup;这里只做只读统计。
CGROUP="/sys/fs/cgroup/workload.slice"

# 按字段名读取,不能用 NR==1、NR==2 之类的行号假设。
awk '
  $1 == "anon"   { anon=$2 }
  $1 == "file"   { file=$2 }
  $1 == "kernel" { kernel=$2 }
  END {
    printf "anon=%.2f MiB\n", anon / 1024 / 1024
    printf "file=%.2f MiB\n", file / 1024 / 1024
    printf "kernel=%.2f MiB\n", kernel / 1024 / 1024
  }
' "$CGROUP/memory.stat"

脚本中的换算只是为了阅读方便,内核文件本身仍然以字节表示。字段缺失时,变量可能保持为空;在生产脚本中应为缺失字段设置默认值,并检查文件是否可读,而不是把空值当成零再下结论。

file 变大时继续看哪些细项

file 是文件类内存的总分类,不能单独说明来源。file_mapped 可以帮助观察被 mmap 映射的文件缓存,file_dirty 表示尚未写回磁盘的脏文件缓存,file_writeback 则表示正在写回的部分。它们服务于不同判断,不应简单相加后当成独立总量。

from pathlib import Path

# 只按 key=value 读取 memory.stat,忽略未知字段以兼容内核新增项目。
def read_memory_stat(path: str) -> dict[str, int]:
    values: dict[str, int] = {}
    for line in Path(path).read_text(encoding="utf-8").splitlines():
        # 每行是字段名和字节数;异常行不参与分类。
        parts = line.split()
        if len(parts) != 2:
            continue
        key, raw = parts
        try:
            values[key] = int(raw)
        except ValueError:
            # 记录格式异常时跳过,不把异常文本伪装成数值。
            continue
    return values

stat = read_memory_stat("/sys/fs/cgroup/workload.slice/memory.stat")
# 用 get 兼容不同内核版本或权限环境下缺少的细项。
for key in ("anon", "file", "file_mapped", "file_dirty", "kernel"):
    print(f"{key}: {stat.get(key, 0) / 1024 / 1024:.2f} MiB")

这段 Python 代码强调两个边界:新字段出现时不应导致解析器崩溃,字段不存在时也不能把默认值误写成“内核明确报告为零”。如果默认值会影响告警,应额外返回缺失字段集合。

把几个字段放回同一条排查链

单看 memory.stat 只能知道分类,排查还需要一个稳定顺序。先读 memory.current,确认 cgroup 当前使用量;再读 memory.stat,找出主要分类;如果 file 占比较高,再看 file_mapped、file_dirty 和写入业务;如果 anon 持续增长,再回到堆、匿名 mmap、线程栈和子进程生命周期;最后读 memory.events,判断是否出现 high、max、oom 或 oom_kill 等事件。

从 memory.current、memory.stat 到文件缓存、匿名内存和事件计数分支的排查路径说明图
图2:从 cgroup 内存总量到字段组合判断的排查路径说明图;这是静态结构图,不是运行截图或故障证据。
观察组合优先追问
anon 高且持续上涨工作集、匿名 mmap、堆外分配或子进程是否持续增加
file 高,file_mapped 也高是否存在大量映射文件、共享库或 mmap 数据集
file_dirty 高写入是否积压,回写速度是否跟不上生产速度
kernel 高页表、内核栈、slab 或 socket 等开销是否随并发增长
events 中 oom_kill 增加是否已经发生组内 OOM,需结合日志确认受影响任务

不要把分类字段当成最终结论

第一,memory.current 和 memory.stat 都是 cgroup 视角;它们可能包含后代 cgroup 的贡献,具体要结合层级和内核接口语义解释。第二,file 包含 tmpfs 和共享内存,不能只按“磁盘文件缓存”理解。第三,memory.events 是事件计数,不是当前内存占用;事件增长与 anon、file 的大小之间没有一一对应关系。

如果服务由 systemd 或容器运行时管理,还应先确认正在看的 cgroup 路径属于哪一层。父级预算可能限制子组,进程迁移也不会把过去已经计入的内存自动改记到新组。读取统计时保留采样时间和 cgroup 路径,才能把多次结果放在同一上下文中比较。

速查:下一步应该看哪里

现象下一步
只想知道总量读取 memory.current,并记录 cgroup 路径
想区分匿名和文件类读取 memory.stat 的 anon、file、kernel
文件类占比高继续看 file_mapped、file_dirty、file_writeback
怀疑触及限制单独读取 memory.events,区分 high、max、oom、oom_kill
结果随时间变化按固定间隔采样,比较增量,不用一次读数定性

memory.stat 的字段顺序固定吗?

不固定。应按字段名寻找项目,不能按行号解析;内核可能在中间增加新字段。

file 越大就一定是缓存没释放吗?

不一定。file 包含文件系统缓存、tmpfs 和共享内存,还要结合 file_mapped、file_dirty 以及业务行为判断。

anon 是否就是应用堆?

不是。anon 覆盖匿名映射,应用堆只是其中可能的一部分,还可能包含匿名 mmap、线程栈等。

应该先读 stat 还是 events?

建议先用 current 确认总量,再用 stat 做类型拆分;events 负责说明限制或 OOM 事件,应该单独解释。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>