1. 从“消失的内存”说起:你的内存去哪儿了?
不知道你有没有遇到过这种情况:服务器或者自己的电脑,明明物理内存很大,free -m 或者 top 命令一看,可用内存却所剩无几,系统开始变得卡顿,甚至触发了 OOM(Out-Of-Memory)杀手,把一些进程给“杀”掉了。你可能会疑惑,我跑的程序看起来也没用那么多内存啊,这些内存到底被谁“吃”掉了?
这时候,很多朋友的第一反应就是去查看 /proc/meminfo 这个文件。它就像是 Linux 系统内存的“体检报告”,里面密密麻麻地列出了各种内存指标的详细数据。原始文章已经给了我们一份非常清晰的参数列表和基础解释,比如 MemTotal、MemFree、Cached、Buffers 等等。照着这个列表去加加减减,理论上应该能把所有内存的来龙去脉算清楚,对吧?
但现实往往很骨感。我自己在运维线上服务时就踩过这个坑。有一次,一台 32GB 内存的机器,MemFree 加上 Cached、Buffers、Slab 这些看起来可以算作“已使用”的内存,加起来怎么算都比 MemTotal 少了将近 2GB。这 2GB 内存就像凭空蒸发了一样,在 meminfo 的报告里找不到任何踪迹。这就是我们今天要深入探讨的 “内存黑洞”。
这个黑洞并不是 bug,而是 Linux 内核内存管理机制中一个有意为之的设计。简单来说,有一部分通过特定方式分配的内核内存,没有被纳入 /proc/meminfo 的常规统计项里。它们被直接用于内核的核心运作,但对用户态的程序来说,就像是“隐形”的。如果我们不了解这个黑洞,就会在评估系统内存压力、进行容量规划时产生严重误判,以为内存还够,实际上系统已经站在了悬崖边上。
所以,这篇文章的目标很明确:我们不仅要看懂 meminfo 这张“体检报告”,更要学会发现报告里“没写出来”的那些问题。我会结合自己处理过的实际案例,带你一步步拆解内存黑洞的成因,并重点讲解系统如何通过 LRU 算法 和 内存回收机制 来应对压力,以及我们能做哪些优化来减少这种“资源浪费”。无论你是运维工程师、后端开发者,还是对系统性能感兴趣的技术爱好者,理解这些内容都能帮你更好地掌控自己的系统。
2. 拆解 /proc/meminfo:超越基础解读
原始文章已经对 /proc/meminfo 里的主要字段做了很好的解释。我们在这里不再重复罗列所有参数,而是挑出几个关键且容易混淆的“组合”,深入聊聊它们背后的联系和实际意义。理解这些,是发现内存黑洞的前提。
2.1 Active/Inactive:内存的“热度”榜单
这是 LRU(最近最少使用)算法的直接体现。系统把内存页分成了两个“榜单”:Active(活跃)和 Inactive(非活跃)。每个榜单下又根据内存页的类型,细分为 Anon(匿名页,如堆栈、malloc 分配的内存)和 File(文件页,如程序代码、内存映射的文件)。
- Active(anon) / Active(file):可以理解为“最近访问过的热门数据”。比如,一个正在频繁进行计算的进程的堆内存(anon),或者一个被反复读取的配置文件内容(file)。系统会尽量让它们留在内存里,因为很快可能又要用到。
- Inactive(anon) / Inactive(file):相当于“冷数据”。它们曾经被使用过,但已经有一段时间没人碰了。当系统需要空闲内存时,首先会从这里开刀。
一个关键机制:当 Inactive 列表里的一个页被再次访问时,它会被提升回 Active 列表。反之,如果 Active 列表里的页太久没被访问,它会被慢慢“冷却”,降级到 Inactive 列表。这个过程是内核持续在后台进行的。
怎么看:如果 Inactive(file) 的值很大,通常是好事,说明系统缓存了很多文件数据,能加速磁盘IO,并且这部分内存是可以被快速、无代价地回收的(直接丢弃即可,因为磁盘有备份)。但如果 Inactive(anon) 很大,就要小心了,这可能意味着很多进程分配了内存但后来不怎么用,它们就是内存回收时的主要目标,而回收匿名页通常需要交换到 Swap 空间,代价较大。
2.2 Cached, Buffers, Slab:内核的“缓存江湖”
很多人知道 Cached 是缓存,但常常和 Buffers、Slab 搞混。你可以这样理解:
- Cached (Page Cache):这是最大的一块缓存,目的是加速对文件的读写。当你用
cat查看一个文件,或者用vi编辑它时,文件内容就会被加载到这里。下次再读时,直接从内存取,飞快。它缓存的是具体的“文件内容”。这部分内存是最容易回收的,直接丢弃就行,下次需要再从磁盘读。 - Buffers:这块比较古老,现在主要用来缓存文件系统的元数据,比如目录结构、inode 信息等。它也是可以回收的。
- Slab:这是内核对象的缓存。内核自己运行也需要频繁创建和销毁很多小对象(比如进程描述符
task_struct、网络套接字socket等)。每次都向物理内存申请释放效率太低,于是内核搞了个“对象池”,这就是 Slab 分配器。Slab又分为:SReclaimable:可回收的 Slab。比如缓存的文件系统 dentry(目录项)、inode 对象。当内存紧张时,内核可以清理这部分缓存。SUnreclaim:不可回收的 Slab。通常是内核核心数据结构,无法释放,否则系统就崩溃了。
一个实用公式:MemAvailable 这个值,就是内核帮你估算的、当前应用程序真正可用的内存。它大致等于 MemFree + Buffers + Cached + SReclaimable。这个值比单纯的 MemFree 更有参考价值。当你看到 MemAvailable 很低而 Cached 很高时,别慌,系统只是把内存用来做缓存了,应用需要时它会立刻让出来。
2.3 那些容易被忽略的“固定开销”
有些内存是“铁打”的,不管系统负载如何,它们都会占用一部分空间,在 meminfo 里也能看到:
- KernelStack:每个用户线程都有一个对应的内核栈,用于执行系统调用时的内核代码。线程越多,这部分开销越大。64位系统上通常是 16KB/线程,看着小,但成千上万个线程时,总量就很可观了。
- PageTables:用来管理进程虚拟地址到物理地址的映射关系。当进程使用了大量内存(特别是通过
mmap映射了大量虚拟空间)时,页表就会膨胀。我曾经遇到一个使用超大内存映射的文件数据库,PageTables占用了近 1GB 内存。 - KReclaimable:这是一个比较新的统计项(在较新内核中),它把
SReclaimable和一部分可回收的Kernel内存(如某些类型的文件系统缓存)合并在一起,让你更直观地看到内核有多少“可回收”内存。
把这些都加起来,我们似乎能算清总账了:MemFree + Active/Inactive + Cached/Buffers + Slab + KernelStack + PageTables + ...。但正如开头所说,依然对不上 MemTotal。那么,缺失的那部分到底在哪?
3. 揭秘“内存黑洞”:alloc_pages 的隐身术
现在,我们进入核心地带——内存黑洞。原始文章点出了关键:通过 alloc_pages / __get_free_page 接口分配的内存,没有在 /proc/meminfo 中统计。
为什么? 这涉及到 Linux 内核内存管理的层次。/proc/meminfo 的统计主要位于一个叫 vmalloc 的层次之上。而 alloc_pages 是更底层、更直接的物理页面分配器。
- vmalloc/slab 分配器:它们像是“高级经理”,分配内存时会记录明细账(比如在
VmallocUsed或Slab中体现),方便管理和回收。它们分配的内存可能虚拟地址连续但物理地址不连续(vmalloc),或者是特定大小的对象(slab)。 - alloc_pages/__get_free_page:它们则是“底层仓库管理员”,直接从物理内存的“页框”仓库里搬出整页整页的内存(通常一页 4KB)。这个过程非常高效,但就像从仓库直接提货没有经过经理登记一样,在“高级经理”(
meminfo的统计体系)的账本上,这笔支出没有单独列项。
那么,哪些内核模块在用 alloc_pages?
很多内核核心功能和驱动会使用这种方式,例如:
- DMA 缓冲区:一些硬件设备(如网卡、磁盘控制器)需要进行 DMA(直接内存访问),它们需要物理地址连续的大块内存。
alloc_pages是获取这种内存的常用方式。 - 文件系统页缓存(Page Cache)的“后备存储”:是的,虽然
Cached统计了文件缓存的内容,但缓存数据本身所占的物理页框,有一部分可能就是通过alloc_pages分配的,然后再挂到 Page Cache 体系里。它的“管理开销”在Cached,但部分“实体”可能没完全体现在其他明细账里。 - 某些网络协议栈的内存:比如 TCP 的发送和接收缓冲区(
sk_buff)在需要大量、快速分配时,可能会绕过 slab 直接申请页面。 - 内核临时大对象:一些内核操作需要临时的大块内存,也会直接调用
alloc_pages。
如何探测这个黑洞?
虽然 meminfo 不直接统计,但我们有间接方法。最常用的命令是 sudo slabtop 和查看 /proc/buddyinfo。
buddyinfo 显示了底层页框分配器的状态。它展示了不同阶数(order,即连续页面的数量,如 2^0=1页, 2^1=2页...)的连续空闲页面块有多少。当系统内存碎片化严重,或大量内存被 alloc_pages 以高阶形式占用时,buddyinfo 里高阶(如 order=5,6,7...)的自由块会很少。这可以作为一个侧面证据。
但更直接的方法是使用 /proc/vmallocinfo 和内核跟踪工具。原始文章提到了 vmallocinfo。我们可以用 sudo cat /proc/vmallocinfo | grep -v "0xffffff" 来过滤掉固定的内核虚拟地址,主要看动态分配的部分。不过,这仍然只能看到 vmalloc 的分配。对于纯粹的 alloc_pages,需要使用更高级的工具,如 ftrace 或 systemtap 来跟踪内核函数 __alloc_pages 的调用。这对于普通用户来说门槛较高,但却是定位深层内存问题的终极武器。
黑洞的影响:它使得我们通过 meminfo 计算出的“已使用内存”低于实际值。在内存压力下,系统实际可用的空闲内存比 MemAvailable 估算的还要少,可能导致回收机制启动过晚,或者 OOM 杀手在管理员毫无察觉的情况下突然触发。
4. 系统的自救:LRU 与内存回收机制详解
当内存不够用时(具体来说是 MemAvailable 低于某个阈值),Linux 内核不会坐以待毙,它会启动一套复杂的 内存回收机制。这套机制的核心就是围绕着我们前面讲的 LRU 列表 来工作的。
4.1 回收的优先级与策略
内核回收内存是有明确优先级的,遵循“代价最小化”原则:
- 回收 Page Cache (
Cached和Buffers):这是首选,因为代价几乎为零。对于干净的文件页(未被修改),直接丢弃即可。对于脏页(被修改过),需要先写回磁盘再丢弃。回收的力度由内核参数vm.vfs_cache_pressure影响(默认值100,值越大回收越积极)。 - 回收可回收的 Slab (
SReclaimable):清理那些可回收的内核对象缓存,比如 dentry 和 inode 缓存。 - 交换匿名页 (
AnonPages):这是最后的手段,因为代价最大。匿名页没有磁盘备份,要回收它,必须把它写到 Swap 分区(或文件) 中去。这个过程涉及磁盘IO,速度很慢。
那么,具体从 LRU 列表的哪里开始回收呢?
内核有一个后台线程叫 kswapd。当内存水位较低时,kswapd 会被唤醒。它主要扫描 Inactive 列表。策略如下:
- 它会先扫描
Inactive(file)列表,尝试回收文件页。 - 如果回收的文件页不够,它就会去扫描
Inactive(anon)列表,把里面旧的匿名页交换出去。 - 如果
Inactive列表都快扫完了还不够,它就会去Active列表里找那些“老化”的页面(通过检查访问标志),把它们降级到Inactive列表,然后再进行回收。
这个过程由几个重要的内核参数控制:
vm.swappiness:这个值在 0 到 100 之间。它控制内核在回收内存时,有多倾向于使用交换(Swap)。- 值越高(如60),内核越早、越积极地交换匿名页到磁盘。
- 值越低(如10甚至0),内核会尽量避免交换,更努力地回收文件缓存。
- 注意:即使设为0,在内存极度紧张时,内核仍然会进行交换。
vm.min_free_kbytes:系统保留的绝对最小空闲内存(KB)。当可用内存低于这个值时,内核会启动直接回收,这比kswapd的后台回收更激进、更同步,可能引起进程的短暂停顿。
4.2 内存压缩(Compaction)与透明大页(THP)
在现代内核中,还有两个机制会影响内存回收和分配:
- 内存压缩:为了解决内存碎片化问题(这会影响
alloc_pages分配连续内存),内核会尝试移动正在使用的内存页,把小的空闲块合并成大的连续块。这个过程由kcompactd线程完成。你可以通过/proc/buddyinfo观察其效果。 - 透明大页:为了提升性能,内核会自动将连续的 4KB 小页合并成 2MB 或 1GB 的“大页”。这虽然减少了页表开销和 TLB 未命中,但也会加剧内存碎片化,因为分配大页需要连续的物理内存。当系统开启 THP 时,
alloc_pages分配高阶内存的请求会变多,可能加剧内存黑洞的感知。/proc/meminfo中的AnonHugePages就统计了这部分匿名大页。
5. 实战优化:减少黑洞影响与提升回收效率
了解了原理,我们就能采取实际行动了。优化目标有两个:一是尽量减少“黑洞”内存的不可控性;二是让系统的内存回收更平滑、更高效,避免突发的性能下降。
5.1 监控与诊断进阶
-
建立综合监控:不要只看
free或MemAvailable。建立一个包含以下指标的监控面板:MemAvailable的趋势图(核心指标)。SwapUsed的趋势图(观察交换是否发生)。PageTables、KernelStack、Slab的大小(观察固定和内核开销)。vmstat命令输出的si(swap in)和so(swap out)速率。如果持续大于0,说明系统正在频繁交换,性能已受影响。sar -B输出的pgscank(kswapd 扫描的页面数)和pgscand(直接回收扫描的页面数)。如果这些值很高,说明系统一直在努力回收内存。
-
使用更专业的工具:
smem:可以更准确地计算进程的 PSS(比例集大小),能更好地反映共享内存的真实占用。numastat:在 NUMA 架构的服务器上,查看内存的节点分布是否均衡。perf:可以跟踪内核中的mm_page_alloc等事件,分析页面分配的热点。
5.2 内核参数调优(谨慎操作)
调整内核参数需要根据实际负载测试,切忌直接在生产环境套用。
-
调整
vm.swappiness:- 对于数据库服务器(如 MySQL, Redis)或内存计算应用(如 Spark),它们的数据集应尽量留在内存。建议将
swappiness设置为 1-10 的低值,让系统优先清理缓存,极力避免交换。 - 对于通用应用服务器或桌面,保持默认值 60 通常没问题。
- 设置方法(临时):
sudo sysctl vm.swappiness=10 - 永久生效:在
/etc/sysctl.conf中添加vm.swappiness=10,然后执行sysctl -p。
- 对于数据库服务器(如 MySQL, Redis)或内存计算应用(如 Spark),它们的数据集应尽量留在内存。建议将
-
调整
vm.vfs_cache_pressure:- 这个值控制内核回收 dentry 和 inode 缓存 的倾向。默认值100。
- 如果系统运行大量文件操作(如 Web 静态服务器、文件存储服务器),可以适当提高这个值(如 200-500),让内核更积极地回收这些缓存,为应用代码和数据腾出空间。
- 如果系统主要运行计算密集型应用,文件访问很少,可以降低这个值(如 50),保留更多的文件系统缓存可能也没坏处。
-
管理透明大页:
- 对于某些已知与 THP 配合不佳的应用(如早期版本的 MongoDB),可以考虑关闭 THP。
- 查看状态:
cat /sys/kernel/mm/transparent_hugepage/enabled - 关闭(建议在启动脚本中设置):
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag
5.3 应用层最佳实践
- 合理设置应用内存限制:对于 Java、Go 等有托管堆的应用,务必设置合理的堆大小(
-Xmx,-Xms)。不要让它无限制地使用内存,避免一个应用吃光所有资源,触发全局 OOM。 - 使用内存控制组:cgroups,特别是 v2 版本,是管理内存的利器。你可以为不同的服务或用户组设置内存上限(
memory.max)、软限制(memory.high)以及 Swap 使用限制。当达到软限制时,内核会开始对该组进行内存回收,而不是等到全局紧张。这能实现更精细、更公平的内存隔离。 - 优化代码内存使用:对于开发者来说,避免内存泄漏、使用对象池减少分配/释放开销、合理选择数据结构和算法,是从根源上减轻内存压力的最好方法。
内存管理是一个复杂但极其有趣的领域。/proc/meminfo 就像一扇窗,让我们窥见内核世界的忙碌景象。所谓的“内存黑洞”并非设计缺陷,而是高效与透明之间的一种权衡。通过今天这种从表象到原理、从机制到实战的梳理,我希望你能在下次面对内存压力告警时,不再只是简单地重启服务,而是能够从容地打开 meminfo,分析 LRU 列表,检查 Swap 活动,并做出有针对性的优化决策。真正的掌控感,就来自于对这些底层细节的理解和运用。

455

被折叠的 条评论
为什么被折叠?



