深蓝黑背景封面。左侧原创示意图:一个终端窗口展示 free -m 的输出,表头为 total、used、free、buff/cache、available,Mem 行给出一组示意数字;终端下方三张注释卡依次解释:buffers 是写给块设备的缓冲(文件系统元数据、待落盘的块),量通常很小;cached 是页缓存(读过的文件内容),是可回收的主力;available 是不触发 swap 还能给新进程使用的估算值。底部一行口诀。右侧大标题「buffers 和 cached 到底差在哪」,副标题「为什么 free 很小,通常不是问题」;左上标注「面试经典 · 操作系统」,右上圆角胶囊标注「Day 03 · 11-10」 图 1:本文封面,三张注释卡分别钉住 buffers、cached、available 的语义。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。在 Linux 机器上敲一次 free -m,十个人里有八个会先盯着 free 那一列皱眉:「16G 的机器怎么就剩 400M 了?」这道题考的正是这几列各自意味着什么,以及为什么 free 小通常根本不是问题。

先对齐口径:free 的数据全部来自 /proc/meminfo。

free(1) 手册写得很清楚,它只是 /proc/meminfo 的格式化视图,每一列都对应 meminfo 里的字段:total 对应 MemTotal,free 对应 MemFree,buffers 对应 Buffers,cache 对应 Cached 与 SReclaimable 之和,buff/cache 是两者相加,available 对应 MemAvailable。把这几行字段语义搞懂,free 就没有任何神秘之处了。

buffers:内核和块设备之间的周转区。

proc 手册对 Buffers 的定义是「原始磁盘块的相对临时存储,通常不会长得很大(几十 MB 量级)」。它缓冲的是面向块设备的内容:文件系统的元数据(目录项、inode 信息)、等待合并后落盘的原始块。两个特征值得记住:一是面向「设备」而不是「文件内容」,二是体量天然有限——健康机器上 Buffers 常年只占内存的零头。如果某台机器 Buffers 异常庞大,反而要怀疑有大量未落盘的脏页堆积。

cached:页缓存,可回收内存的主力。

Cached 是从磁盘读过的文件内容留在内存里的副本,即页缓存(page cache)。再次读取同一个文件时直接命中内存,绕开磁盘 IO——这是 Linux 文件读写快的重要原因。free 的 cache 列还要加上 SReclaimable(slab 中可回收的内核缓存,比如 dentry、inode 缓存),手册原文是「page cache and slabs」。关键点在于回收属性:应用申请内存而 free 不够时,内核会把干净的页缓存直接丢弃、把脏页写回磁盘后让位,对应用完全透明。所以 cached 大不是内存被「占用」,而是被「借用」。

available:判断内存够不够,只看这一列。

available 是内核给出的估算:「不发生 swap 的前提下,现在还能给新应用分配多少内存」。它约等于 free 加上可回收的页缓存,再减去一部分必须保留的水位(正在使用的可回收 slab 等不能全算进去)。手册里另一个重要口径是:used = total − available。也就是说 free 工具自己都不把 cache 算进「已用」,这与很多人的直觉相反。MemAvailable 字段自 Linux 3.14 内核起由内核直接提供,更早的版本只能近似估算。

用一组模拟数字把三个场景算一遍。

本机没有 Linux 环境,下面用 Node.js 22.15.0 按手册口径模拟 free 各列的计算(node free-model.js):

function renderFree(m, tag) {
  const used = m.MemTotal - m.MemAvailable;                 // used = total - available
  const buffCache = m.Buffers + m.Cached + m.SReclaimable;  // buff/cache = buffers + cache
  console.log(`[${tag}] total=${m.MemTotal} used=${used} free=${m.MemFree} buff/cache=${buffCache} available=${m.MemAvailable}`);
}

const before = { MemTotal: 16000, MemFree: 400, MemAvailable: 6200, Buffers: 120, Cached: 5100, SReclaimable: 400 };
renderFree(before, '平时');

// 读了一个 3000 MB 的大文件:页缓存上涨
const afterRead = { ...before, MemFree: 100, Cached: before.Cached + 3000, MemAvailable: before.MemAvailable };
renderFree(afterRead, '读大文件后');

// 应用再申请 4000 MB:缓存被回收让位
const afterAlloc = { ...afterRead, MemAvailable: afterRead.MemAvailable - 4000, Cached: afterRead.Cached - 3900 };
renderFree(afterAlloc, '应用申请内存后');

实测输出:

[平时] total=16000 used=9800 free=400 buff/cache=5620 available=6200
[读大文件后] total=16000 used=9800 free=100 buff/cache=8620 available=6200
[应用申请内存后] total=16000 used=13800 free=100 buff/cache=4720 available=2200

三个场景串起来就是这道题的完整答案。读完大文件,free 从 400 掉到 100,但 used 纹丝不动、available 不变——内存只是换了个角色,从「闲着」变成「帮忙挡磁盘」。应用真正申请内存时,cache 立刻从 8620 缩到 4720 让位,used 才上涨。看 free 判断内存够不够,正确姿势是看 available,而不是 free。

深蓝黑背景概念图。顶部一条 1440 像素宽的内存全景横条分成四段:青色 used(进程真正占用)、紫色窄段 buffers(块设备缓冲)、半透明紫色 cached(页缓存)、深灰 free(完全空闲);横条上方一个括号标注 available 的估算区间覆盖 free 加可回收的缓存段。中部三张并排卡片:buffers 卡说明它是内核与块设备之间的缓冲,存放文件系统元数据与待合并落盘的块,体量通常很小,对应 meminfo 的 Buffers 字段;cached 卡说明它是读文件时留下的页缓存,再次读同一文件直接命中免走磁盘,应用缺内存时被回收让位,是可回收的主力,对应 Cached 加大部分 SReclaimable;available 卡说明它是内核估算「不触发 swap 还能给新进程分配多少」,约等于 free 加可回收缓存减去保底水位,判断内存够不够只看这一列。下方解释卡说明为什么 free 少不是问题:Linux 把空闲内存拿去当页缓存挡住磁盘 IO,应用需要时缓存会被回收,所以 cache 大是常态而非内存泄漏。底部总结条:够不够看 available,紧不紧张看 vmstat 的 si 与 so,cache 大是常态不是泄漏 图 2:四段内存横条与 available 估算区间的覆盖关系。

什么时候才需要真的紧张。

cache 大是常态,真正的内存紧张看两个信号:一是 available 持续走低逼近保底水位,二是 vmstat 里 si/so(每秒换入换出量)持续非零——说明内存已经不够周转,开始依赖 swap,性能会断崖式下跌。训练或推理机器的内存告警如果只盯 free 列,会把可回收的页缓存误判成不足,白白扩容;把告警阈值改成 available 加水位余量,才是与内核口径一致的配置。

回到面试现场,建议的答题结构:先一句话给出三个字段的定义(buffers 面向块设备、cached 是页缓存、available 是可启动新应用的估算),再讲清「free 小不是问题」的回收逻辑,最后补 used = total − available 这个反直觉口径与 si/so 的紧张信号。定义背对是及格线,能把「读完大文件 free 掉了但 available 没变」这个实验讲出来是加分项。你排查过「内存明明很多却一直 swap」的机器吗?最后是什么原因,欢迎分享。