读懂 free、buff/cache 与 MemAvailable:别把缓存误判成泄漏。
从 free、buff/cache 和 MemAvailable 判断内存余量
一台 Linux 机器运行了一段时间。你打开 free,发现 free 列已经很小,used 很大,buff/cache 也占了一块。
值班群里很容易出现一句话:
Linux 把内存吃满了,是不是泄漏?
先不要清缓存,也不要急着找“吃内存”的进程。真正需要确认的是两件事:
- 已用内存主要去了匿名页、文件页缓存,还是内核对象?
本文从一个示意场景出发。
先给结论
MemFree 很小,不等于系统马上缺内存;buff/cache 很大,也不等于发生了泄漏。- 判断主机还能否承接一般用户态负载,通常先看
MemAvailable 及其趋势。但它是估算,不是后续分配一定成功的承诺。 - 如果内存持续增长,先判断主要来自匿名页、文件页还是内核对象,再进入对应的归因路径。
这里说的是主机级、一般用户态负载的第一层容量观察,不代表特定 zone、NUMA 节点、连续物理页或其他受约束申请一定成功。
一张 free 截图为什么不够
~ # free -h total used free shared buff/cache availableMem: 906.9M 33.0M 805.1M 66.4M 68.8M 773.3MSwap: 0 0 0~ #
free 看起来像一个内核接口,其实它是用户态工具。它读取 /proc/meminfo,再按照 procps-ng 自己的规则进行计算和排版。
这条边界很重要:
/proc/meminfo 中的 MemFree、MemAvailable、Cached、Slab 等字段,由内核汇总并输出。free 中的 used、cache、buff/cache 等列,可能是用户态工具对多个字段做加减后得到的派生结果。
因此,不能笼统地说“free 每一列都是内核直接给出的真值”。更准确的说法是:它的数据源来自内核,列的展示和部分计算属于 procps-ng。
不同 procps-ng 版本、发行版补丁、宽输出和普通输出,可能带来列名或计算口径差异。比如 free -w 会把 buffers 和 cache 分开显示,普通输出通常把它们合并为 buff/cache。要解释某台机器上的具体数字,至少应同时保存:
uname -afree --versionfree -w -kcat /proc/meminfo
单位也不能忽略。/proc/meminfo 使用 kB 标签,接口中的数值按 KiB 量级理解;free 是否使用 1024 进制或 SI 单位,则受参数影响。
/proc/meminfo 的 Buffers 也不是“所有内核缓冲区”的总和。Linux 6.6.145 的 proc 文档将它概括为与原始磁盘块相关的相对临时存储;不要把网络缓冲、全部 slab 或全部 page cache 都算进它。
~ # cat /proc/meminfo MemTotal: 928664 kBMemFree: 824172 kBMemAvailable: 791576 kBBuffers: 0 kBCached: 68040 kBSwapCached: 0 kBActive: 11188 kBInactive: 57156 kBActive(anon): 11188 kBInactive(anon): 57156 kBActive(file): 0 kBInactive(file): 0 kBUnevictable: 0 kBMlocked: 0 kBSwapTotal: 0 kBSwapFree: 0 kBDirty: 0 kBWriteback: 0 kBAnonPages: 304 kBMapped: 1180 kBShmem: 68040 kB
所以,一张 free 截图最多告诉你“某一时刻,某个版本的工具这样展示内存”。它没有告诉你这些内存为何增长、归谁所有、能否回收,更没有提供时间趋势。
MemAvailable 到底估算了什么
MemFree 回答的是:现在有多少 RAM 没有被使用。
MemAvailable 试图回答一个更接近工程需求的问题:在不引发 swapping 的前提下,启动新应用大约还有多少内存可用。
注意“大约”。
在 Linux 6.6.145 中,MemAvailable 由内核的 si_mem_available() 估算。它不是把 MemFree 和 buff/cache 直接相加,而是大致经过以下考虑:
- 从空闲页数量出发,先扣除
totalreserve_pages。 - 汇总各 zone 的 low watermark;它不是再次从总量中简单扣除,而是作为后续估算文件 LRU 和可回收内核内存时的保守留存上限。
- 将 active/inactive file LRU 的一部分计入,同时保留“其一半”和“low watermark”两者中的较小值。
- 对可回收 slab 和其他可回收内核内存采用同类保守估算。
可以把它理解为:
扣除系统保留量后的当前空闲页+ 扣除保守留存量后的部分文件 LRU+ 扣除保守留存量后的部分可回收内核内存= MemAvailable 的估算结果
这只是帮助理解的因果表达,不是一个可以拿 free 各列直接代入的固定公式。
为什么要保守扣留?因为缓存里仍有工作集,内核对象里仍有在用对象,系统也需要留出运行余量。把所有“看起来可以回收”的数字都算进去,会高估真实承载能力。
为什么它又不是承诺?因为内存状态随并发活动不断变化,而且一个全局总量不能覆盖所有具体分配约束。MemAvailable 适合做系统容量余量的第一观察项,但不能解释每一次具体申请是否成功。
所以,工程上的正确用法不是“MemAvailable 还有 2 GiB,因此接下来一定能申请 2 GiB”,而是观察它在相同口径下的水平和趋势,并结合工作负载与压力证据判断。
RAM 主要去了哪里
初次排查不需要把 /proc/meminfo 的所有字段都背下来。先建立三个方向就够了:匿名页、文件页缓存、内核对象。
1. 匿名页:进程数据常见的落点
匿名页通常没有普通文件作为直接后备。进程运行时的堆、栈以及一部分匿名映射,最终使用的物理内存会反映到匿名页相关统计中。
在系统级视角下,可以先观察:
AnonPagesActive(anon)Inactive(anon)
这里的目标只是判断“增长是否主要朝匿名页方向走”,不是立即把所有匿名页归到某个进程。进程级的 RSS、PSS、共享页和 smaps 归因,需要另一套证据,后续文章再展开。
AnonPages 在这里主要反映映射进用户页表的非文件后备页;它不是所有匿名虚拟地址、malloc() 承诺量或进程 RSS 的简单总和。
2. 文件页缓存:读过的文件内容可以留在 RAM 中
程序读取文件后,Linux 往往会把文件内容保留在 page cache 中。后续再次访问相同内容时,系统可能直接从内存取得数据,避免重复读取存储设备。
这部分 RAM 确实已经被使用,但用途是缓存文件数据。只要系统没有明显压力,保留这些内容通常比让 RAM 长期空着更有价值。
/proc/meminfo 中的 Cached 可以帮助观察这个方向,但不能把它机械地翻译成“普通磁盘文件缓存”。Linux 6.6.145 的 proc 文档明确说明,Cached 还包括 tmpfs/shmem,并且不包含 SwapCached。因此,Cached 与 Shmem 也不能随手相加后宣称得到一个严格的“总文件缓存”。
与文件页方向相关的常用字段包括:
CachedActive(file)Inactive(file)Shmem
这些字段提供的是观察角度,不是一组互斥、可以严格守恒相加的会计科目。
Cached 与 Active(file) + Inactive(file) 也不是严格等价项:si_mem_available() 使用 file LRU 统计,并不直接读取或代入 /proc/meminfo 的 Cached。tmpfs/shmem 虽然使用 page cache 机制并进入 Cached 的统计口径,却没有普通磁盘文件作为后备,因此要把它作为交叉项单独观察。
3. slab:内核对象也要占内存
~ # cat /proc/slabinfo slabinfo - version: 2.1# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>p9_req_t 0 0 160 25 1 : tunables 0 0 0 : slabdata 0 0 0isp1760_qtd 0 0 72 56 1 : tunables 0 0 0 : slabdata 0 0 0isp1760_urb_listitem 0 0 24 170 1 : tunables 0 0 0 : slabdata 0 0 0sas_task 0 0 320 12 1 : tunables 0 0 0 : slabdata 0 0 0bio-120 64 64 128 32 1 : tunables 0 0 0 : slabdata 2 2 0io_kiocb 0 0 256 16 1 : tunables 0 0 0 : slabdata 0 0 0bfq_io_cq 0 0 1360 12 4 : tunables 0 0 0 : slabdata 0 0 0mqueue_inode_cache 17 17 960 17 4 : tunables 0 0 0 : slabdata 1 1 0v9fs_inode_cache 0 0 696 23 4 : tunables 0 0 0 : slabdata 0 0 0nfs4_xattr_cache_cache 0 0 2128 15 8 : tunables 0 0 0 : slabdata 0 0 0nfs_direct_cache 0 0 224 18 1 : tunables 0 0 0 : slabdata 0 0 0nfs_commit_data 21 21 768 21 4 : tunables 0 0 0 : slabdata 1 1 0nfs_read_data 34 34 960 17 4 : tunables 0 0 0 : slabdata 2 2 0nfs_inode_cache 0 0 1064 15 4 : tunables 0 0 0 : slabdata 0 0 0nfs_page 0 0 128 32 1 : tunables 0 0 0 : slabdata 0 0 0fat_inode_cache 0 0 776 21 4 : tunables 0 0 0 : slabdata 0 0 0fat_cache 0 0 40 102 1 : tunables 0 0 0 : slabdata 0 0 0squashfs_inode_cache 0 0 704 23 4 : tunables 0 0 0 : slabdata 0 0 0jbd2_journal_head 0 0 120 34 1 : tunables 0 0 0 : slabdata 0 0 0ext2_inode_cache 0 0 808 20 4 : tunables 0 0 0 : slabdata 0 0 0ext4_inode_cache 0 0 1168 14 4 : tunables 0 0 0 : slabdata 0 0 0ext4_allocation_context 0 0 152 26 1 : tunables 0 0 0 : slabdata 0 0 0ext4_prealloc_space 0 0 112 36 1 : tunables 0 0 0 : slabdata 0 0 0bio_post_read_ctx 170 170 48 85 1 : tunables 0 0 0 : slabdata 2 2 0extent_status 0 0 40 102 1 : tunables 0 0 0 : slabdata 0 0 0mbcache 0 0 56 73 1 : tunables 0 0 0 : slabdata 0 0 0kioctx 0 0 576 14 2 : tunables 0 0 0 : slabdata 0 0 0fanotify_perm_event 0 0 96 42 1 : tunables 0 0 0 : slabdata 0 0 0dio 0 0 640 12 2 : tunables 0 0 0 : slabdata 0 0 0pid_namespace 0 0 144 28 1 : tunables 0 0 0 : slabdata 0 0 0rpc_inode_cache 0 0 704 23 4 : tunables 0 0 0 : slabdata 0 0 0ip4-frags 0 0 200 20 1 : tunables 0 0 0 : slabdata 0 0 0ip_fib_trie 0 0 48 85 1 : tunables 0 0 0 : slabdata 0 0 0ip_fib_alias 0 0 56 73 1 : tunables 0 0 0 : slabdata 0 0 0PING 0 0 960 17 4 : tunables 0 0 0 : slabdata 0 0 0tw_sock_TCP 0 0 264 15 1 : tunables 0 0 0 : slabdata 0 0 0request_sock_TCP 0 0 312 13 1 : tunables 0 0 0 : slabdata 0 0 0TCP 0 0 2240 14 8 : tunables 0 0 0 : slabdata 0 0 0hugetlbfs_inode_cache 12 12 648 12 2 : tunables 0 0 0 : slabdata 1 1 0bio-248 48 48 256 16 1 : tunables 0 0 0 : slabdata 3 3 0request_queue 34 34 920 17 4 : tunables 0 0 0 : slabdata 2 2 0bio-184 21 21 192 21 1 : tunables 0 0 0 : slabdata 1 1 0biovec-128 16 16 2048 16 8 : tunables 0 0 0 : slabdata 1 1 0biovec-64 16 16 1024 16 4 : tunables 0 0 0 : slabdata 1 1 0khugepaged_mm_slot 0 0 40 102 1 : tunables 0 0 0 : slabdata 0 0 0user_namespace 0 0 616 13 2 : tunables 0 0 0 : slabdata 0 0 0dmaengine-unmap-256 15 15 2112 15 8 : tunables 0 0 0 : slabdata 1 1 0dmaengine-unmap-128 15 15 1088 15 4 : tunables 0 0 0 : slabdata 1 1 0audit_buffer 170 170 24 170 1 : tunables 0 0 0 : slabdata 1 1 0sock_inode_cache 38 38 832 19 4 : tunables 0 0 0 : slabdata 2 2 0skbuff_ext_cache 64 64 64 64 1 : tunables 0 0 0 : slabdata 1 1 0skbuff_small_head 69 69 704 23 4 : tunables 0 0 0 : slabdata 3 3 0skbuff_head_cache 48 48 256 16 1 : tunables 0 0 0 : slabdata 3 3 0configfs_dir_cache 46 46 88 46 1 : tunables 0 0 0 : slabdata 1 1 0file_lock_cache 0 0 216 18 1 : tunables 0 0 0 : slabdata 0 0 0file_lock_ctx 0 0 56 73 1 : tunables 0 0 0 : slabdata 0 0 0buffer_head 0 0 104 39 1 : tunables 0 0 0 : slabdata 0 0 0taskstats 18 18 432 18 2 : tunables 0 0 0 : slabdata 1 1 0proc_dir_entry 294 294 192 21 1 : tunables 0 0 0 : slabdata 14 14 0proc_inode_cache 92 92 696 23 4 : tunables 0 0 0 : slabdata 4 4 0seq_file 68 68 120 34 1 : tunables 0 0 0 : slabdata 2 2 0sigqueue 102 102 80 51 1 : tunables 0 0 0 : slabdata 2 2 0bdev_cache 38 38 1664 19 8 : tunables 0 0 0 : slabdata 2 2 0shmem_inode_cache 1240 1240 784 20 4 : tunables 0 0 0 : slabdata 62 62 0kernfs_iattrs_cache 0 0 80 51 1 : tunables 0 0 0 : slabdata 0 0 0kernfs_node_cache 15624 15624 144 28 1 : tunables 0 0 0 : slabdata 558 558 0mnt_cache 36 36 320 12 1 : tunables 0 0 0 : slabdata 3 3 0inode_cache 1534 1534 624 13 2 : tunables 0 0 0 : slabdata 118 118 0dentry 2877 2877 192 21 1 : tunables 0 0 0 : slabdata 137 137 0names_cache 48 48 4096 8 8 : tunables 0 0 0 : slabdata 6 6 0net_namespace 0 0 2944 11 8 : tunables 0 0 0 : slabdata 0 0 0iint_cache 0 0 128 32 1 : tunables 0 0 0 : slabdata 0 0 0uts_namespace 0 0 432 18 2 : tunables 0 0 0 : slabdata 0 0 0nsproxy 56 56 72 56 1 : tunables 0 0 0 : slabdata 1 1 0vma_lock 319 612 40 102 1 : tunables 0 0 0 : slabdata 6 6 0vm_area_struct 253 299 176 23 1 : tunables 0 0 0 : slabdata 13 13 0files_cache 46 46 704 23 4 : tunables 0 0 0 : slabdata 2 2 0signal_cache 84 84 1152 14 4 : tunables 0 0 0 : slabdata 6 6 0sighand_cache 75 75 2112 15 8 : tunables 0 0 0 : slabdata 5 5 0task_struct 72 72 3840 8 8 : tunables 0 0 0 : slabdata 9 9 0anon_vma 312 312 104 39 1 : tunables 0 0 0 : slabdata 8 8 0shared_policy_node 0 0 48 85 1 : tunables 0 0 0 : slabdata 0 0 0numa_policy 128 128 32 128 1 : tunables 0 0 0 : slabdata 1 1 0perf_event 0 0 1072 15 4 : tunables 0 0 0 : slabdata 0 0 0pool_workqueue 112 112 512 16 2 : tunables 0 0 0 : slabdata 7 7 0maple_node 224 224 256 16 1 : tunables 0 0 0 : slabdata 14 14 0radix_tree_node 1260 1260 584 14 2 : tunables 0 0 0 : slabdata 90 90 0task_group 0 0 384 21 2 : tunables 0 0 0 : slabdata 0 0 0mm_struct 26 26 1216 13 4 : tunables 0 0 0 : slabdata 2 2 0vmap_area 280 280 72 56 1 : tunables 0 0 0 : slabdata 5 5 0kmalloc-cg-8k 0 0 8192 4 8 : tunables 0 0 0 : slabdata 0 0 0kmalloc-cg-4k 16 16 4096 8 8 : tunables 0 0 0 : slabdata 2 2 0kmalloc-cg-2k 16 16 2048 16 8 : tunables 0 0 0 : slabdata 1 1 0kmalloc-cg-1k 32 32 1024 16 4 : tunables 0 0 0 : slabdata 2 2 0kmalloc-cg-512 32 32 512 16 2 : tunables 0 0 0 : slabdata 2 2 0kmalloc-cg-256 224 224 256 16 1 : tunables 0 0 0 : slabdata 14 14 0kmalloc-cg-192 126 126 192 21 1 : tunables 0 0 0 : slabdata 6 6 0kmalloc-cg-128 96 96 128 32 1 : tunables 0 0 0 : slabdata 3 3 0kmalloc-cg-96 42 42 96 42 1 : tunables 0 0 0 : slabdata 1 1 0kmalloc-cg-64 512 512 64 64 1 : tunables 0 0 0 : slabdata 8 8 0kmalloc-cg-32 256 256 32 128 1 : tunables 0 0 0 : slabdata 2 2 0kmalloc-cg-16 512 512 16 256 1 : tunables 0 0 0 : slabdata 2 2 0kmalloc-cg-8 1024 1024 8 512 1 : tunables 0 0 0 : slabdata 2 2 0dma-kmalloc-8k 0 0 8192 4 8 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-4k 0 0 4096 8 8 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-2k 0 0 2048 16 8 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-1k 0 0 1024 16 4 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-512 0 0 512 16 2 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-256 0 0 256 16 1 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-192 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-128 0 0 128 32 1 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-96 0 0 96 42 1 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-64 0 0 64 64 1 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-32 0 0 32 128 1 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-16 0 0 16 256 1 : tunables 0 0 0 : slabdata 0 0 0dma-kmalloc-8 0 0 8 512 1 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-8k 0 0 8192 4 8 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-4k 0 0 4096 8 8 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-2k 0 0 2048 16 8 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-1k 0 0 1024 16 4 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-512 0 0 512 16 2 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-256 0 0 256 16 1 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-192 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-128 0 0 128 32 1 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-96 84 84 96 42 1 : tunables 0 0 0 : slabdata 2 2 0kmalloc-rcl-64 64 64 64 64 1 : tunables 0 0 0 : slabdata 1 1 0kmalloc-rcl-32 0 0 32 128 1 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-16 0 0 16 256 1 : tunables 0 0 0 : slabdata 0 0 0kmalloc-rcl-8 0 0 8 512 1 : tunables 0 0 0 : slabdata 0 0 0kmalloc-8k 12 12 8192 4 8 : tunables 0 0 0 : slabdata 3 3 0kmalloc-4k 24 24 4096 8 8 : tunables 0 0 0 : slabdata 3 3 0kmalloc-2k 80 80 2048 16 8 : tunables 0 0 0 : slabdata 5 5 0kmalloc-1k 336 336 1024 16 4 : tunables 0 0 0 : slabdata 21 21 0kmalloc-512 320 320 512 16 2 : tunables 0 0 0 : slabdata 20 20 0kmalloc-256 368 368 256 16 1 : tunables 0 0 0 : slabdata 23 23 0kmalloc-192 1365 1365 192 21 1 : tunables 0 0 0 : slabdata 65 65 0kmalloc-128 288 288 128 32 1 : tunables 0 0 0 : slabdata 9 9 0kmalloc-96 671 798 96 42 1 : tunables 0 0 0 : slabdata 19 19 0kmalloc-64 896 896 64 64 1 : tunables 0 0 0 : slabdata 14 14 0kmalloc-32 742 768 32 128 1 : tunables 0 0 0 : slabdata 6 6 0kmalloc-16 1536 1536 16 256 1 : tunables 0 0 0 : slabdata 6 6 0kmalloc-8 1536 1536 8 512 1 : tunables 0 0 0 : slabdata 3 3 0kmem_cache_node 192 192 64 64 1 : tunables 0 0 0 : slabdata 3 3 0kmem_cache 160 160 256 16 1 : tunables 0 0 0 : slabdata 10 10 0~ #
内核管理文件、目录项、inode、网络状态和其他内部对象时,也需要内存。大量小对象通常由 slab 分配器组织,/proc/meminfo 会展示:
SlabSReclaimableSUnreclaimKReclaimable
Slab 由 SReclaimable 和 SUnreclaim 两部分构成。在 Linux 6.6.145 的 /proc/meminfo 实现中,KReclaimable = SReclaimable + NR_KERNEL_MISC_RECLAIMABLE,后者代表其他可回收的非 slab 内核页。
这意味着,Slab、SReclaimable 和 KReclaimable 不能全部相加。它们存在包含关系,盲目相加会重复计数。
字段名也不能直接替你下结论:
SReclaimable 是 Slab 中被统计为可能回收的部分,不代表其中每个对象此刻都能释放,也不代表一次回收一定能回收相同数量。SUnreclaim 表示这部分不能通过 slab reclaim 回收,不代表它天然就是泄漏。
内核长期持有必要对象是正常行为。只有看到异常的持续增长,并进一步定位到具体对象和调用路径,才有资格讨论泄漏。
“可回收”为什么不等于“空闲”
这是理解 Linux 内存指标时最容易混淆的一层。
空闲页当前没有承载有效内容,可以直接作为新的内存来源。可回收页仍在承载数据或对象,只是内核在压力下有机会释放其中一部分。
以 page cache 为例:
- 有些文件页近期频繁访问,仍属于有效工作集,保留它们能减少后续 I/O。
- 有些页包含尚未完成处理的修改,回收前可能需要额外工作。
- 有些页虽然能够淘汰,但释放后如果很快再次访问,就需要重新读取,产生 refault(重新访问已淘汰页)成本。
slab 也类似。某个缓存类别可以参与回收,不代表其中所有对象都已经无人使用。Linux 6.6.145 的相关代码在估算可用内存时,会对可回收 slab 和其他可回收内核内存做保守扣留,因为其中仍有在用对象。
因此,下面两个等式都不成立:
可回收内存 = 当前空闲内存可回收内存 = 可以无代价全部释放的内存
缓存不是一块免费的“第二 RAM”。回收能够腾出空间,但可能付出扫描、对象收缩以及后续重新读取等代价。反过来,暂时没有被回收也不说明发生了泄漏。内核没有压力时,本来就没有必要为了让 free 的数字好看而主动丢掉有用缓存。
这也是为什么不应把 drop_caches 当成常规诊断手段。它会人为改变缓存状态和性能基线,却不能证明或排除泄漏。
LRU 和 reclaim:只需要先掌握这个最小模型
理解前面的 MemAvailable 估算,需要知道一点 reclaim,但不用在第一篇就钻进完整实现。
Linux 会把可淘汰页按匿名页与文件页、活跃与不活跃等维度组织。Linux 6.6.145 中可以看到这些基本类别:
Inactive(anon)Active(anon)Inactive(file)Active(file)Unevictable
LRU 可以先理解为一种回收候选的组织方式,而不是一块精确记录“多少字节马上能释放”的仪表。
Inactive(file) 通常比 Active(file) 更接近优先考虑的回收候选,但 inactive 不代表一定能够回收,active 也不代表永远不能回收。实际过程中,内核会从候选列表中扫描和隔离页,尝试处理;未能回收的页还可能被放回 LRU。
用一句最小因果链概括:
以下因果链是传统 inactive-list 页回收路径的教学简化,用来说明“候选不等于结果”;它不覆盖 Multi-Gen LRU、slab/shrinker 和完整 reclaim 决策。
候选页进入组织列表 → 内存压力出现 → 扫描并隔离候选 → 尝试回收 → 未回收者回到列表
这个模型足以说明两件事:
第一,文件页和部分内核对象确实可能为新负载让出空间。第二,候选不等于结果,列表大小也不是可立即释放量。
配置边界:Linux 6.6.145 还可以配置 Multi-Gen LRU。即使启用,/proc/meminfo 仍会提供上述兼容性汇总字段。本文只借这些字段建立观测模型,不把传统 active/inactive 描述成全部内部实现。
从“used 很高”到三分流排查
把前面的判断合在一起,可以得到一条从 used 很高开始的排查路径。重点不是用一条命令定案,而是把“内存满了”改写成可验证的问题。
图1:从“used 很高”到三分流排查这张图表达的是排查顺序,不是严格的内存守恒关系。
图中的 MemAvailable 是主机级余量的第一观察项,不是唯一健康判据。“稳定”只能阻止我们仅凭 MemFree 小就判定内存不足;“持续下降”也只是触发进一步归因,不能直接证明泄漏。
第一步:确认口径
保存内核版本、free 版本、输出单位、是否使用 -w,并保留同一时刻的 /proc/meminfo 原始字段。
如果没有这些信息,就无法判断 used 和 buff/cache 的具体计算方式,也无法可靠比较不同机器或不同时间的截图。
第二步:看 MemAvailable 的趋势
不要只看某个瞬间有多高或多低。连续观察它是稳定波动、下降后回升,还是持续下降。
MemFree 小而 MemAvailable 稳定,说明按当前估算口径,可用余量暂时没有持续下降;不能仅凭这一点认定内存不足。至于已用 RAM 是否主要用于缓存,仍需结合 Cached、file LRU、Shmem、匿名页和 slab 等字段判断。MemAvailable 持续下降值得继续追踪,但它本身仍不能指出增长来源。
第三步:按用途分流
先看哪个方向贡献了主要变化:
- 匿名页方向:
AnonPages、anon LRU 是否持续增长? - 页缓存方向:
Cached、file LRU 是否增长?同时检查 Shmem,避免把 tmpfs/shmem 增长直接认作普通文件 page cache 增长。 - 内核对象方向:
Slab、SReclaimable、SUnreclaim、KReclaimable 如何变化?
三分流不是严格分类账。shmem/tmpfs 等统计存在交叉,slab 字段也有包含关系。它的价值是指导下一步调查,而不是让你把几个字段相加到刚好等于 MemTotal。
第四步:找压力旁证
/proc/vmstat 中的扫描、回收和 workingset 相关字段可以作为旁证,但它们大多是启动以来的累计计数。单看一个很大的累计值,不能证明当前正在发生压力。
应比较同一采样窗口内的增量,并明确字段是否受当前内核配置影响。即使计数发生变化,也只能说明相应活动出现过,不能单独证明性能故障或泄漏。
第五步:进入专项归因
分流以后再选择工具:
- 匿名页增长,进入进程 RSS、PSS、
smaps 归因。 - 页缓存方向增长,先结合
Shmem 区分普通文件与 tmpfs/shmem;普通文件方向再检查具体文件工作集、脏页和回收行为。 - 内核对象增长,先比较
KReclaimable、SReclaimable 和 SUnreclaim 的变化;确认是 slab 方向后,再用 slabinfo 做具体对象归因。
到这里,“Linux 吃满内存”已经变成了几个能够采集证据的问题。
四个最容易带偏排查的误解
误解一:buff/cache 很大就是内存泄漏
buff/cache 是 procps-ng 对缓存相关内存的汇总展示。文件访问和内核对象缓存都可能让它增长,这是正常内存用途。
缓存也可能异常增长,但需要时间序列、对象或文件归因,以及压力下的回收行为才能判断。字段名本身不是泄漏证据。
误解二:page cache 可以立即、全部、零成本释放
文件页只是回收候选。活跃工作集值得保留,部分页面需要额外处理,淘汰后还可能因再次访问而产生 I/O 和 refault 成本。
“可能回收”描述的是能力和候选关系,不是释放承诺。
误解三:MemAvailable 等于 MemFree 加 buff/cache
Linux 6.6.145 的 si_mem_available() 会考虑保留量、low watermark、文件 LRU 和可回收内核内存,并对后两者做保守扣留。
它是内核估算,不应从某次 free 输出反推为跨版本不变的用户态公式。
误解四:执行 drop_caches 就能验证是否泄漏
drop_caches 会主动改变缓存和性能基线。数值下降不能证明之前没有问题,数值不降也不能直接证明泄漏。生产环境不应把它当作常规排查动作。
附录 A:可以安全做的只读验证
以下内容用于复现和核验;只想先掌握判断方法的读者,可以直接跳到文末结论。
以下实验目前只给出命令、预期现象和风险说明,不把预期写成实际观察。
实验一:建立工具与字段口径
风险:只读,低风险。
uname -afree --versionfree -kcat /proc/meminfo
预期现象:/proc/meminfo 由内核提供 MemFree、MemAvailable、Cached、Slab 等字段;free 读取这些字段,进行单位换算和排版,并按该 procps-ng 版本的规则派生部分展示列。
实际观察:目标 Linux 6.6.145 ARM64 QEMU 环境执行。
~ # uname -aLinux (none) 6.6.145 #1 SMP PREEMPT Fri Jul 31 17:32:31 CST 2026 aarch64 Linux~ # free --versionBusyBox v1.37.0 (2026-01-10 15:38:28 UTC) multi-call binary.Usage: free [-bkmgh]Display free and used memory~ # free -w -kBusyBox v1.37.0 (2026-01-10 15:38:28 UTC) multi-call binary.Usage: free [-bkmgh]Display free and used memory~ # free -k total used free shared buff/cache availableMem: 928664 33808 824424 68040 70432 791828Swap: 0 0 0~ # free -h total used free shared buff/cache availableMem: 906.9M 33.0M 805.1M 66.4M 68.8M 773.3MSwap: 0 0 0~ # cat /proc/meminfoMemTotal: 928664 kBMemFree: 824424 kBMemAvailable: 791828 kBBuffers: 0 kBCached: 68040 kBSwapCached: 0 kBActive: 11188 kBInactive: 57140 kBActive(anon): 11188 kBInactive(anon): 57140 kBActive(file): 0 kBInactive(file): 0 kBUnevictable: 0 kBMlocked: 0 kBSwapTotal: 0 kBSwapFree: 0 kBDirty: 0 kBWriteback: 0 kBAnonPages: 304 kBMapped: 1180 kBShmem: 68040 kBKReclaimable: 2392 kBSlab: 8988 kBSReclaimable: 2392 kBSUnreclaim: 6596 kBKernelStack: 848 kBPageTables: 68 kBSecPageTables: 0 kBNFS_Unstable: 0 kBBounce: 0 kBWritebackTmp: 0 kBCommitLimit: 464332 kBCommitted_AS: 68740 kBVmallocTotal: 133141626880 kBVmallocUsed: 960 kBVmallocChunk: 0 kBPercpu: 176 kBHardwareCorrupted: 0 kBAnonHugePages: 0 kBShmemHugePages: 0 kBShmemPmdMapped: 0 kBFileHugePages: 0 kBFilePmdMapped: 0 kBCmaTotal: 32768 kBCmaFree: 32384 kBHugePages_Total: 0HugePages_Free: 0HugePages_Rsvd: 0HugePages_Surp: 0Hugepagesize: 2048 kBHugetlb: 0 kB~ #
验证重点:保存 procps-ng 版本和本机手册,确认目标环境中 used、cache、buff/cache 的实际公式。
实验二:连续采样,区分“高”与“持续增长”
风险:只读,低风险。小型 QEMU 中不宜长期高频采样。
for i in 1 2 3 4 5; do date awk '/^(MemFree|MemAvailable|Cached|Shmem|AnonPages|KReclaimable|SReclaimable|SUnreclaim):/' /proc/meminfo sleep 2done
预期现象:字段会随系统活动波动,一次高值不能证明持续泄漏。
实际观察:执行结果。
~ # cat test.sh for i in 1 2 3 4 5; do date awk '/^(MemFree|MemAvailable|Cached|Shmem|AnonPages|KReclaimable|SReclaimable|SUnreclaim):/' /proc/meminfo sleep 2done~ # ./test.sh Fri Aug 14 10:22:52 UTC 2026MemFree: 824424 kBMemAvailable: 791828 kBCached: 68044 kBAnonPages: 508 kBShmem: 68044 kBKReclaimable: 2392 kBSReclaimable: 2392 kBSUnreclaim: 6604 kBFri Aug 14 10:22:54 UTC 2026MemFree: 824172 kBMemAvailable: 791576 kBCached: 68044 kBAnonPages: 500 kBShmem: 68044 kBKReclaimable: 2392 kBSReclaimable: 2392 kBSUnreclaim: 6604 kBFri Aug 14 10:22:56 UTC 2026MemFree: 824172 kBMemAvailable: 791576 kBCached: 68044 kBAnonPages: 468 kBShmem: 68044 kBKReclaimable: 2392 kBSReclaimable: 2392 kBSUnreclaim: 6604 kBFri Aug 14 10:22:58 UTC 2026MemFree: 824172 kBMemAvailable: 791576 kBCached: 68044 kBAnonPages: 428 kBShmem: 68044 kBKReclaimable: 2392 kBSReclaimable: 2392 kBSUnreclaim: 6604 kBFri Aug 14 10:23:01 UTC 2026MemFree: 824172 kBMemAvailable: 791576 kBCached: 68044 kBAnonPages: 452 kBShmem: 68044 kBKReclaimable: 2392 kBSReclaimable: 2392 kBSUnreclaim: 6604 kB~ #
读取 /proc/meminfo 也不是原子快照。系统持续运行时,相邻字段可能来自略有差异的采样时刻,核对近似关系时应允许小幅偏差。
实验三:只读观察 reclaim 旁证
风险:只读,低风险。
awk '/^(pgscan_|pgsteal_|workingset_refault_file|workingset_activate_file)/' /proc/vmstatsleep 5awk '/^(pgscan_|pgsteal_|workingset_refault_file|workingset_activate_file)/' /proc/vmstat
预期现象:字段是否存在及具体命名受版本和配置影响;两次读取的差值比单个累计值更接近采样窗口内的活动。
实际观察:执行结果。
~ # cat test_vmstat.sh awk '/^(pgscan_|pgsteal_|workingset_refault_file|workingset_activate_file)/' /proc/vmstatsleep 5awk '/^(pgscan_|pgsteal_|workingset_refault_file|workingset_activate_file)/' /proc/vmstat~ # ./test_vmstat.sh workingset_refault_file 0workingset_activate_file 0pgsteal_kswapd 0pgsteal_direct 0pgsteal_khugepaged 0pgscan_kswapd 0pgscan_direct 0pgscan_khugepaged 0pgscan_direct_throttle 0pgscan_anon 0pgscan_file 0pgsteal_anon 0pgsteal_file 0workingset_refault_file 0workingset_activate_file 0pgsteal_kswapd 0pgsteal_direct 0pgsteal_khugepaged 0pgscan_kswapd 0pgscan_direct 0pgscan_khugepaged 0pgscan_direct_throttle 0pgscan_anon 0pgscan_file 0pgsteal_anon 0pgsteal_file 0~ #
附录 B:证据状态与适用边界
本文涉及的 /proc/meminfo 字段和 procps 展示主要属于通用 Linux 接口。架构与配置会影响字段存在性和实际内存布局,但不能仅凭 ARM64 或 x86_64 的架构名称改写字段定义。本文的目标基线仍是 Linux 6.6.145 ARM64;运行结果需要在对应配置和环境中单独核验。
本文的静态结论基于本地 Linux 6.6.145 源码树 /home/ubuntu/linux-src/linux-6.6.145 中已经完成的核验,主要证据包括:
fs/proc/meminfo.c:/proc/meminfo 的生成入口,以及 MemFree、MemAvailable、Cached、匿名页、LRU 和 slab 相关字段的输出。mm/show_mem.c:si_mem_available() 对空闲页、保留量、zone low watermark、文件 LRU 和可回收内核内存的估算。include/linux/mmzone.h:anonymous/file、active/inactive 与 unevictable 的基础 LRU 分类。mm/vmscan.c:从 inactive LRU 隔离候选、尝试回收、将未回收者放回列表的最小路径。Documentation/filesystems/proc.rst:MemAvailable 的估算语义、Cached 对 tmpfs/shmem 的包含关系,以及 reclaimable 字段的边界。
源码能够证明代码定义和静态路径,不能替代目标配置下的运行观察。后续取得 Linux 6.6.145 ARM64 QEMU 基线后,应补充实际字段、工具版本和时间序列;在此之前,所有运行现象都保持“待验证”状态。
别再只问“RAM 有没有被用”
RAM 被使用,通常不是问题本身。真正需要确认的是:可用余量是否持续下降,增长来自哪类内存,以及系统是否已经出现回收压力。
因此,一张 free 截图只能作为入口,不能单独证明内存不足,更不能证明内存泄漏。
这篇文章位于内存管理知识地图的压力链入口,并与资源链交叉:page cache 和 slab 解释了 RAM 的两类实际去向。地址链不在本文展开。
下一篇继续追问:page cache 增长到什么程度才值得怀疑?它与真正的内存泄漏应该怎样用证据区分?
你遇到过 MemFree 很小、但业务仍然正常的情况吗?当时你最先看了哪个指标?
我是熊树先生,持续记录 Linux 内核、嵌入式系统与底层技术学习,分享内核源码分析、驱动开发和系统软件实践。
如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见。