FreeBSD 吃掉我的内存
摘要
一篇解释为什么 FreeBSD 看起来占用大量内存的文章,将其归因于磁盘缓存和虚拟内存管理,类似于 Linux 的“吃掉我的内存”现象。
暂无内容
查看缓存全文
缓存时间: 2026/07/03 20:16
# FreeBSD 吃掉了我的内存! 来源:https://crocidb.com/post/freebsd-ate-my-ram/
上个月我发文讲述了将网站服务器从老旧的 Ubuntu 迁移到 FreeBSD 的过程(https://crocidb.com/post/this-blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd/)。Hacker News 上的一些人注意到,当我展示 `fastfetch` 结果时,我说我对 RAM 使用率与 `btop` 显示的差异感到困惑,并评论说 `fastfetch` 可能*更*准确。我决定深入这个兔子洞,尝试理解为什么在现代操作系统中报告空闲或已用内存比看起来更复杂。另一位用户分享了 Linux ate my RAM(https://www.linuxatemyram.com/),它快速解释了 Linux 上的相同现象。如果你也想快速了解 FreeBSD 的答案:使用率有时看起来异常,是因为操作系统会将磁盘中所有能缓存的东西都缓存到 RAM 中以提升整体性能,但这个缓存是易失的,在需要更多内存时会被释放。如果你想看稍长一点的答案,请继续阅读。但在此之前先快速声明一下:我不是操作系统内核方面的专家,尤其是 FreeBSD。这篇文章是我利用空闲时间花了数周时间研究这个领域的成果。如果你发现任何特别错误的地方,请评论:分享(知识)就是关爱!
## RAM 使用率很难定义
Linux ate my RAM(https://www.linuxatemyram.com/)的核心观点是解释未使用的 RAM 就是浪费的 RAM。就像 CPU 缓存会缓存 RAM 内容(因为 CPU 访问 RAM 更快)一样,RAM 会缓存磁盘数据以改善用户体验。这个缓存的工作原理稍微复杂一些,但在此之前,了解内核如何管理 RAM 很重要。
大多数现代操作系统都有虚拟内存(VM)系统。它基本上将物理内存划分为通常为 4KiB 的页面。每个页面随后被添加到不同的队列中,这样内核可以灵活调度它们,确保所有进程在需要时都能获得内存,并且整个系统能在资源短缺时继续工作。
例如:**交换**内存。我以前从未仔细想过交换内存是如何使用的,只知道它是磁盘上一个独立的空间,会在需要时临时存储部分 RAM。但概括来说,当操作系统发现已分配的 RAM 使用不频繁时,它会将这些页面设置为可以被写入磁盘的状态,以便在需要更多内存时腾出空间。当这些页面被所属程序再次请求时,它们会被移回 RAM。
每个操作系统都有不同的页面集合和相应的管理规则。在 FreeBSD 上,页面队列的类型如下:
```
#define PQ_NONE 255
#define PQ_INACTIVE 0
#define PQ_ACTIVE 1
#define PQ_LAUNDRY 2
#define PQ_UNSWAPPABLE 3
#define PQ_COUNT 4
```
你可以在 `sys/vm/vm_page.h`(https://github.com/freebsd/freebsd-src/blob/main/sys/vm/vm_page.h#L322C1-L327C19)找到这些定义。所有其他基于 Unix 的系统也会有类似的定义:[Linux](https://github.com/torvalds/linux/blob/master/include/linux/mmzone.h#L387)、[OpenBSD](https://github.com/openbsd/src/blob/master/sys/uvm/uvm_page.h#L140)、[NetBSD](https://github.com/NetBSD/src/blob/trunk/sys/uvm/uvm_page.h#L245)、[DragonFlyBSD](https://github.com/DragonFlyBSD/DragonFlyBSD/blob/master/sys/vm/vm_page.h#L242)。
如果我们查看 `top`,会发现它不仅报告内存使用情况,还将其分为几个类别:
top 详细报告了内存、交换和磁盘缓存的各个部分
- **active**:活动页面是(主要是)用户态进程正在积极使用的页面
- **inactive**:一段时间内未被这些进程访问的页面会被移入非活动队列
- **laundry**:这是等待写入交换空间的页面队列。当系统需要分配不在空闲队列中的空间时,它会将非活动页面移入此队列
- **wired**:这是 `PQ_NONE`、`PQ_UNSWAPPABLE` 中的内存,以及内核自身使用的、不由 VM 管理的内存
- **free**:完全未使用的内存
> 当曾经是**inactive**、进入**laundry**、被写入磁盘(swap)的内存被其所属进程再次请求时,它会从磁盘重新加载到**inactive**,最后回到**active**。
现在我们可以开始理解为什么很难准确知道已用内存和空闲内存各有多少。**free** 队列中的内存保证是空闲的,但我们可以认为 **inactive** 队列中的内存也是空闲的,因为它可以回收——当需要更多内存时内核会释放它。**Wired** 内存大部分是锁定的,但磁盘缓存也属于其中,所以 **wired** 中的*一部分*也是可回收的,因此也可以算作“空闲”!
## 磁盘缓存
**ZFS** 是现在 FreeBSD 的默认文件系统,它拥有 **ARC**(自适应替换缓存),这是一个专门的系统,用于在内存中缓存最近使用的数据,从而改善重复读取磁盘的性能。当系统需要更多内存时,这个缓存会缩小。内核本身也有实现这种缓存的机制,但 ARC 绕过了内核机制。所有这些统计数据可以通过内核参数 `kstat.zfs.misc.arcstats.*` 访问。使用 `sysctl`,我们可以获取所有信息:
```
sysctl kstat.zfs.misc.arcstats
```
这会显示所有可用参数,但以下这些是重要的:
```
sysctl -n kstat.zfs.misc.arcstats.size
sysctl -n kstat.zfs.misc.arcstats.c_min
sysctl -n kstat.zfs.misc.arcstats.c_max
```
这些会显示当前缓存大小以及配置的最小和最大值,单位都是字节。使用 `gnumfmt` 可以转换为可读单位:
```
$ sysctl -n kstat.zfs.misc.arcstats.size | gnumfmt --to=iec
3.1G
```
这些信息也会在 `top` 中显示,并且更详细。这是针对 ZFS 的,但你可以 FreeBSD 上运行其他文件系统(https://docs.freebsd.org/en/books/handbook/filesystems/)。
## 为什么 `fastfetch` 和 `btop` 报告不同?
现在到了有趣的部分。这两个工具(以及许多其他工具,如 `htop`)都试图报告内存使用情况,以便用户(或系统管理员)了解系统状况。为此,它们都需要选择一种*启发式方法*;实际上就是决定他们将什么称为**已用内存**。而所有的差异都源于它们使用了不同的启发式方法。
我深入研究了每个工具的源代码,以找出它们是如何确定的。
`fastfetch` 是这样做的:
```
free memory = free + inactive + cache*
used memory = total - free memory
```
> 关于 `cache` 稍后再说!
在我的旧 ThinkPad X230(运行 FreeBSD 15.0-RELEASE)上,看起来是这样的:
82% 的已用内存!
而 `btop` 是这样做的:
```
available memory = total memory - active - wired
free memory = free
used memory = active + wired
```
与 `fastfetch` 同时运行时,它给我显示的是:
只有 7% 已用?!
为了使情况更有趣,我还检查了 `htop`,虽然它会在条形图中分别显示内存类别,但在条形图末尾显示已用内存为:4.49G/5.69G
使用的启发式方法是:
```
used memory = wired + active + laundry
```
然后我写了一个 Python 脚本,一次性显示所有启发式方法。你可以在这里找到它(https://github.com/crocidb/freebsd-memory-monitor-heuristics)。
Pastedimage20260701215700.png
看起来是正确的,除了 `btop`,它偏离得很远。但如果你仔细看,还会注意到之前截图中 `cache` 的值也是空的。我花了数周时间才意识到这一点。所以我开始更深入地挖掘它们的代码。
## `btop` 在 FreeBSD 上的内存报告相当错误
在其源代码中,特别查看 `src/freebsd/btop_collect.cpp`(https://github.com/aristocratos/btop/blob/main/src/freebsd/btop_collect.cpp),它获取内存信息的地方:
```
int mib[4];
u_int memActive, memWire, cachedMem, freeMem;
size_t len;
len = 4;
sysctlnametomib("vm.stats.vm.v_active_count", mib, &len);
len = sizeof(memActive);
sysctl(mib, 4, &(memActive), &len, nullptr, 0);
memActive *= Shared::pageSize;
len = 4;
sysctlnametomib("vm.stats.vm.v_wire_count", mib, &len);
len = sizeof(memWire);
sysctl(mib, 4, &(memWire), &len, nullptr, 0);
memWire *= Shared::pageSize;
mem.stats.at("used") = memWire + memActive;
mem.stats.at("available") = Shared::totalMem - memActive - memWire;
len = sizeof(cachedMem);
len = 4;
sysctlnametomib("vm.stats.vm.v_cache_count", mib, &len);
sysctl(mib, 4, &(cachedMem), &len, nullptr, 0);
cachedMem *= Shared::pageSize;
mem.stats.at("cached") = cachedMem;
len = sizeof(freeMem);
len = 4;
sysctlnametomib("vm.stats.vm.v_free_count", mib, &len);
sysctl(mib, 4, &(freeMem), &len, nullptr, 0);
freeMem *= Shared::pageSize;
mem.stats.at("free") = freeMem;
```
它使用 `sysctl`(FreeBSD 的 libc 中的函数)并从 `vm.stats.vm.*` 直接获取每个队列的页面数量。对于 active 使用 `vm.stats.vm.v_active_count`,wired 使用 `vm.stats.vm.v_wire_count`,没问题。然后通过将这些队列中的页面数量乘以 `Shared::pageSize` 来计算字节数。
不过有一个问题。它将数据存储在 `memActive` 中,这是一个 `u_int`,一种*无符号 32 位整数*。不用做太多计算,我记得当我们转向 64 位 CPU 时,我听到最多的一句话是:“现在你可以拥有超过 4GB 的内存了”。我的下一个纹身就是这个!好吧,我们说的是一个**无符号** 32 位整数,所以最大值是 **4,294,967,295**。如果以字节表示 4GiB,那就是 **4,294,967,296**。这意味着任何超过该值的数字都会**回绕**。看看我的脚本输出中 btop 的**已用内存**为 `4.42 GiB`,而 btop 自己显示为 `422 MiB`,很明显是回绕了。
### 空缓存问题
在我写那篇关于将博客服务器迁移到 FreeBSD 的文章(https://crocidb.com/post/this-blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd/)时,我最初没有意识到的另一件事是,**Cached** 内存桶是空的。为了进一步测试,我创建了两个虚拟机,分别运行 FreeBSD 13.5-RELEASE 和 15.1-RELEASE,分别使用 ZFS 和 UFS 文件系统,另外还有我的笔记本电脑运行 15.0-RELEASE 使用 ZFS。过去一个月我安装了 FreeBSD 超过 10 次,我想我闭着眼睛都能完成安装程序了
安装完 `btop`(以及其他工具)后,我开始测试:我尝试了不同的文件系统,以检查是否在非 ZFS 系统上遗漏了什么
在两者中,我们都可以看到 **Cached** 桶是空的。在检测代码中,它从 `vm.stats.vm.v_cache_count` 获取信息:
```
len = sizeof(cachedMem);
len = 4;
sysctlnametomib("vm.stats.vm.v_cache_count", mib, &len);
sysctl(mib, 4, &(cachedMem), &len, nullptr, 0);
cachedMem *= Shared::pageSize;
mem.stats.at("cached") = cachedMem;
```
当我尝试自己用 `sysctl` 获取它时:
```
$ sysctl -n vm.stats.vm.v_cache_count
0
```
我了解到 `-d` 会显示每个参数的描述,但结果发现大多数参数都没有文档。不过 `v_cache_count` 是有描述的:
```
$ sysctl -d vm.stats.vm.v_cache_count
vm.stats.vm.v_cache_count: 兼容性虚拟参数
```
没错,它返回 0,因为这是一些遗留代码。实际上,从 FreeBSD 12.0 开始它就显示为“兼容性虚拟参数”!我们可以查看 FreeBSD 12.0(https://github.com/freebsd/freebsd-src/blob/release/12.0.0/sys/vm/vm_meter.c#L419)中 `v_cache_count` 的描述差异:
```
#ifdef COMPAT_FREEBSD11
/*
* Provide compatibility sysctls for the benefit of old utilities which exit
* with an error if they cannot be found.
*/
SYSCTL_UINT(_vm_stats_vm, OID_AUTO, v_cache_count, CTLFLAG_RD, SYSCTL_NULL_UINT_PTR, 0, "Dummy for compatibility");
SYSCTL_UINT(_vm_stats_vm, OID_AUTO, v_tcached, CTLFLAG_RD, SYSCTL_NULL_UINT_PTR, 0, "Dummy for compatibility");
#endif
```
而之前的版本,FreeBSD 11.4(https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_meter.c#L301):
```
VM_STATS_VM(v_cache_count, "Pages on cache queue");
```
但有趣的是,缓存队列自 FreeBSD 6.3.0(https://github.com/freebsd/freebsd-src/blob/release/6.3.0/sys/vm/vm_page.h#L210)起就不存在了。我安装了 11.4,虽然 `sysctl -d vm.stats.vm.v_cache_count` 返回描述“Pages on cache queue”,但实际上获取值总是返回 0。我也试了 6.3.0,`v_cache_count` 也是 0。所以不清楚为什么。
`btop` 中 `v_cache_count` 的使用自第一个 FreeBSD 构建版本就存在了,该版本是在 FreeBSD 12.0 之后发布的,所以也不清楚它为什么能一直用到现在。
### 制定修复方案
然后我开始着手修复。首先,我将存储队列字节的变量的精度提高了一倍,这样就不会再有回绕问题。然后我思考如何正确跟踪文件系统缓存。深入 `htop` 源代码寻求指导时,我发现了这个注释:
```
// 作者:Pierre-Marie Baty
//
// FreeBSD 有以下内存类别:
// active: 当前映射到物理内存的用户态页面(即正在使用)
// wired: 当前映射到物理内存的内核页面,不可被换出或交换
// buffers: 'wired' 的一个子类别,对应文件系统缓存
// free: 尚未分配或已释放的页面
//
// 使用 ZFS 时,ARC 区域不计入 'buffers' 类别,但仍计入 'wired' 类别。
// 因此必须从 'wired' 类别中减去 ARC 总量,并将其加到 'buffer' 类别中,
// 以便结果(ARC 显示在 buffersMem 中)与 ZFS 用户的预期一致。
// 此调整在 freebsd/Platform.c 的 Platform_setMemoryValues() 中完成。
```
更具体地说:“必须从 'wired' 类别中减去 ARC 总量”。这说得通,但我注意到**它并没有这么做**。这让我写了一个 PR(https://github.com/htop-dev/htop/pull/2033)来修复 `htop` 中的这个问题,同时统一了 `cache` 和 `buffers` 内存类别。这是一个支线任务中的支线任务,但这个 PR 已经被合并了:
`vfs.bufspace` 报告 FreeBSD 自身的文件系统元数据缓冲区缓存,该缓存被 ARC 绕过
然后我制定了一个在 `btop` 中继续的计划:基本上是从 wired 内存中减去 ARC 缓存的*可变*大小。同时,像 `htop` 一样,将 `vfs.bufspace` 视为可回收缓存。这是我的 PR(https://github.com/aristocratos/btop/pull/1728)。在撰写本文时,它还没有被审核或批准。以下是我 PR 中 `src/freebsd/btop_collect.cpp` 处理缓存的代码:
```
// cached
len = 2;
if (sysctlnametomib("vfs.bufspace", mib, &len) == 0) {
uint64_t bufSpace = 0;
len = sizeof(bufSpace);
if (sysctl(mib, 2, &bufSpace, &len, nullptr, 0) == 0) {
cachedBytes += bufSpace;
}
}
len = 5;
if (sysctlnametomib("kstat.zfs.misc.arcstats.size", mib, &len) == 0) {
uint64_t arcSize = 0;
len = sizeof(arcSize);
if (sysctl(mib, 5, &arcSize, &len, nullptr, 0) == 0) {
uint64_t arcMin = 0;
len = 5;
if (sysctlnametomib("kstat.zfs.misc.arcstats.c_min", mib, &len) == 0) {
len = sizeof(arcMin);
sysctl(mib, 5, &arcMin, &len, nullptr, 0);
}
cachedBytes += (arcSize > arcMin) ? (arcSize - arcMin) : 0;
}
}
// free
len = 4;
sysctlnametomib("vm.stats.vm.v_free_count", mib, &len);
len = sizeof(freeMem);
sysctl(mib, 4, &(freeMem), &len, nullptr, 0);
uint64_t activeBytes = (uint64_t)memActive * Shared::pageSize;
uint64_t wireBytes = (uint64_t)memWire * Shared::pageS
相似文章
FreeBSD 的 AI 审计
一次 AI 辅助的安全审计揭示了 FreeBSD 内核中的 15 个漏洞,包括权限提升和虚拟机逃逸,并详细描述了与 FreeBSD 团队协作报告和修补漏洞的过程。
FreeBSoD:利用语言模型发现和利用内核漏洞(上)
本文介绍了 Praetorian 的研究人员如何使用 Claude Opus(通过 Claude Code)发现并利用 FreeBSD 内核中的漏洞,包括一个允许逃离 FreeBSD 监狱的栈溢出漏洞(CVE-2026-3038)。第一部分重点介绍寻找漏洞的方法。
FreeBSD ports 冻结
FreeBSD 冻结了其 ports 仓库,原因是某维护者意外提交了一个 150MB 的 Linux Copilot 二进制文件,导致 GitHub 镜像中断并引发许可问题。
FreeBSD 基金会执行董事尝试在笔记本电脑上日常使用 FreeBSD
FreeBSD 基金会执行董事 Deb Goodkin 成功在 Framework 笔记本电脑上日常使用 FreeBSD,克服了长期存在的硬件支持问题。
我在 Apple 的 fsck_hfs 中发现了一个 Bug
macOS Sequoia 上 Apple 的 fsck_hfs 工具存在一个 Bug,在配备 8GB RAM 的机器上,对大型 HFS+ 卷(24TB 以上)进行检测时会误报损坏错误,而文件系统本身并无问题。