Linux 内核技术实战课 · 内存泄漏模块:从 RSS 到 Shmem,把“消失的内存“找回来
Linux 内核技术实战课 · 内存泄漏模块:从 RSS 到 Shmem,把"消失的内存"找回来
作者:资深内核稳定性工程师
实验环境:ecs-fddb-0002(Ubuntu 24.04 LTS / 内核 6.8.0-106-generic / 8C16G)
声明:本文所有命令与观测数据均来自真实服务器实测,未做任何编造。
一、开篇:内存泄漏为什么"难找"?
内存泄漏是 Linux 系统中最常见、也最隐蔽的稳定性问题。轻则单个进程 RSS 持续膨胀,重则整机OOM(Out Of Memory)假死,SSH 都连不上。更头疼的是——有时候你top翻遍了也找不到内存去哪了,free却显示shared吃掉几个 G。
本模块通过6 个真实实验,带你建立起一套完整的内存泄漏排查体系:看懂指标 → 定位进程 → 锁定类型 → 找到根因。读完本文,你将能回答以下三个问题:
- 进程哪些内存类型"容易泄漏"?
- 如何把泄漏"关进笼子"避免整机假死?
- 遇到"内存消失了"(
top找不到去向)时怎么查?
二、基础篇 · 进程哪些内存类型容易泄漏
2.1 先读懂/proc/meminfo—— 内存的"CT 报告"
内存泄漏排查的第一步,是看懂系统级内存指标。在服务器上执行:
cat/proc/meminfo以下是本机基线实测数据(节选关键行):
| 指标 | 本机值 | 与内存泄漏的关系 |
|---|---|---|
AnonPages | 4,477,228 kB (≈4.4 GB) | 用户态匿名页(堆/匿名映射/栈),这是"最常见泄漏"涨的地方 |
Mapped | 126,936 kB | 被映射到进程地址空间的文件页,文件映射泄漏也在此体现 |
Shmem | 2,624 kB | tmpfs/共享内存,不属于任何进程的 RSS,是"消失的内存"主角 |
Slab | 194,100 kB | 内核对象缓存总和 =SReclaimable + SUnreclaim |
SReclaimable | 106,084 kB | 可回收内核 slab(dentry/inode),不算泄漏 |
SUnreclaim | 88,016 kB | 不可回收内核 slab,若只涨不跌 → 内核态泄漏 |
PageTables | 13,712 kB | 进程页表占用的物理内存,进程数/映射多时会涨 |
记忆口诀:用户态泄漏看
AnonPages;共享内存看Shmem;内核态泄漏看SUnreclaim;文件缓存看Cached但一般可回收。
2.2 经典案例:malloc 不 free,RSS 12 秒涨 6 倍
我们写一个最简单的 C 程序——每秒malloc50MB 并逐页写脏(确保物理页真正分配),然后"忘掉"free。用实验 2的真实输出来看:
----- 第 1 次采样 ----- VmRSS: 104144 kB VmSize: 105088 kB ----- 第 2 次采样 ----- VmRSS: 206544 kB VmSize: 207496 kB ----- 第 3 次采样 ----- VmRSS: 308944 kB VmSize: 309904 kB ----- 第 4 次采样 ----- VmRSS: 411344 kB VmSize: 412312 kB ----- 第 5 次采样 ----- VmRSS: 513744 kB VmSize: 514720 kB ----- 第 6 次采样 ----- VmRSS: 616144 kB VmSize: 617128 kB12 秒,RSS 从 104MB 涨到 616MB,每步约 50MB,与代码逻辑完全一致。这就是"进程内存只涨不跌"的典型泄漏特征。
2.3VmRSSvsVmSize:谁才是"真吃内存"?
VmSize(VSZ):进程虚拟地址空间总大小,包含了已mmap但尚未分配物理页的区域。光看 VSZ 暴涨未必是真泄漏。VmRSS(RES):实际占用的物理内存(Resident Set Size)。top的%MEM基于 RSS 计算。
本例中两者几乎相等,因为我们对每块内存都"写脏"触发了缺页中断,物理页真的被分配了。实战中,RSS 持续增长才是真吃物理内存,VSZ 增长只说明虚拟地址被预留。
2.4 进程哪些内存类型容易"悄悄泄漏"?
按本课程框架,最容易泄漏的 5 类内存:
- 堆(heap):
malloc/new只分配不free—— 最常见,也是本文实验 2 的主角。 - 匿名映射:
mmap(MAP_ANONYMOUS)只 map 不munmap。 - 文件映射泄漏:
mmap打开文件后忘记munmap,或持续open不close撑大filp/inodeslab。 - 共享内存:
shmget/shmat后不shmdt/shmctl(IPC_RMID)。 - 页表/内核对象:漏关 fd、socket、句柄,间接推高
PageTables与SUnreclaim。
三、案例篇 · 预防内存泄漏导致系统假死
3.1 不加限制的泄漏会怎样?
上面实验 2 的泄漏进程如果不加限制,会一直涨到吃光所有可用内存,触发全局 OOM Killer。全局 OOM 的最大问题是:内核不知道杀谁最合适,可能杀掉你的关键进程(如 Nginx、MySQL),甚至杀掉 SSH 守护进程导致你再也连不上服务器——这就是"假死"。
3.2 用 cgroup 把泄漏"关进笼子"(实验 3 真实数据)
核心思想:给每个易泄漏的服务设置内存上限,泄漏到顶就杀它自己,别连累整机。
在本实验中,我们用systemd-run把泄漏进程放进一个受控的 scope,并设置MemoryMax=512M:
dmesg-C# 清空环形缓冲,干净采集systemd-run--scope-pMemoryMax=512M ./leak真实输出:
[leak] step=1 allocated=50 MB pid=12849 [leak] step=2 allocated=100 MB pid=12849 ... [leak] step=10 allocated=500 MB pid=12849 systemd-run 返回码: 137 # 128+9 = SIGKILL,被 OOM Killer 终结dmesg 中 OOM 记录:
[ 2208.355205] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null), cpuset=/,mems_allowed=0, oom_memcg=/system.slice/run-....scope, task=leak,pid=12849,uid=0 [ 2208.355219] Memory cgroup out of memory: Killed process 12849 (leak) total-vm:565924kB, anon-rss:523136kB, file-rss:1536kB, shmem-rss:0kB, UID:0 pgtables:1080kB oom_score_adj:03.3 关键解读与生产建议
constraint=CONSTRAINT_MEMCG:这是关键证据——OOM 发生在内存 cgroup 内,仅杀该 scope 的进程。整机其他进程、SSH、系统服务完全不受影响。如果是全局 OOM,会是CONSTRAINT_NONE,且可能杀掉任意进程。task=leak pid=12849:谁被杀了、吃了多少内存,一目了然。- 返回码 137= 128 + SIGKILL(9),从用户态也能确认"进程是被 kill 掉的"。
生产建议:
- 对关键服务(Web 服务器、API 网关、数据库代理)在
systemd单元中加MemoryMax=/MemoryHigh=。 - 或者手动写 cgroup v2:
echo 512M > /sys/fs/cgroup/<name>/memory.max。 - 监控 OOM 事件:
journalctl -k | grep -i oom或接入 Prometheus + Node Exporter 的node_vmstat_oom_kill指标。 - 不要依赖全局 OOM Killer,它是"最后一道防线"而非"预防手段"。
四、案例篇 · Shmem:进程没消耗内存,内存哪去了
4.1 一个真实的"灵异事件"
假设你top看了一遍,所有进程 RSS 加起来不到 4GB,但free -h显示已用 8GB,还剩 2GB 可用——“消失的 2GB 去哪了?”这时候,罪魁祸首很可能是Shmem。
4.2 实验 4 真实数据:往/dev/shm写 1GB
基线:
free -h (shared): ... shared 2.6Mi ... /proc/meminfo Shmem: Shmem: 2624 kB df -h /dev/shm: tmpfs 7.4G 0 7.4G 0% /dev/shm写入 1GB 后:
free -h (shared): ... shared 1.0Gi ... /proc/meminfo Shmem: Shmem: 1051196 kB df -h /dev/shm: tmpfs 7.4G 1.0G 6.4G 14% /dev/shm没有任何进程的 RSS 体现这 1GB 内存。free的shared列和/proc/meminfo的Shmem同步上涨,但top里找不到任何一个进程多吃了 1GB。
4.3 再配合 SysV 共享内存验证
我们还用shmget+shmat创建了 256MB SysV 共享内存段,并写脏页面:
写脏前 Shmem: 1051196 kB (来自 /dev/shm 的 1GB) 写脏后 Shmem: 1313280 kB (又涨了 ~256MB)这里有一个重要细节:shmget创建段时仅分配内核对象,不占物理页;只有shmat后真正写脏页面才让物理内存落地。这也是为什么ipcs -m能看到段,但Shmem在写脏前不变。
4.4 实战结论
当free -h显示shared很大、但你用top/ps找不到对应的高 RSS 进程时,第一反应去看Shmem:
# 三件套快速排查cat/proc/meminfo|grepShmemdf-h/dev/shm# 查看 tmpfs 使用量ipcs-m# 查看 SysV 共享内存段常见场景:程序把大文件塞进/dev/shm、滥用共享内存做 IPC、容器 tmpfs 卷挂载过大、数据库使用 POSIX 共享内存但不及时清理。
五、分析篇 · 内核内存泄漏基础分析
5.1 什么是内核态内存泄漏?
用户态泄漏(malloc 不 free)进程退出后内存自然回收。但内核态泄漏指内核对象(如文件描述符、socket、inode)在没有进程持有后仍占用 slab 缓存不释放,表现为SUnreclaim持续增长。只有重启才能回收,是更严重的泄漏类型。
5.2 实验 5 真实数据:观测filpslab 暴涨
我们制造了"持有 10 万个打开 fd"的压力,用slabtop和/proc/slabinfo观测前后变化。
基线:
SUnreclaim: 88296 kB SReclaimable: 107748 kB filp OBJ(基线): 1888压力中(持有 100,000 个 fd):
filp OBJ(压力中): 101888 ← 约 +10 万,与 fd 数严格对应 SUnreclaim(压力中): 118260 kB ← +约 30MB进程退出后:
filp OBJ(回收后): 1948 SUnreclaim(回收后): 88428 kB ← 回到基线5.3 判漏准则
上述实验只是"正常压力"(进程退出后 slab 被回收)。真正的内核态泄漏判断标准是:
在无明显业务压力、且对象应已释放的情况下,
SUnreclaim(及nr_slab_unreclaimable)持续单调上升、不回落,基本可判定为内核态内存泄漏。
观测手段:
# 1. 看全局趋势watch-n5'grep SUnreclaim /proc/meminfo'# 2. 看哪个 slab 在涨slabtop-o# 3. 精确看某个 slab 的对象数grepfilp /proc/slabinfo# 或 grep sock_inode_cache /proc/slabinfo # socket 泄漏六、分析篇 · 一步步找到根因(工具链实战)
6.1 工具链全景
从"系统内存少了"到"定位到代码行",需要四级工具链:
系统级(meminfo/free)→ 进程级(smem/top)→ 内存类型级(pmap/smaps)→ 代码级(valgrind/memleak)6.2 Step 1:smem找出"谁吃 PSS 最多"(实验 6 A1)
传统top的 RSS 会把共享库重复计算,而PSS(Proportional Set Size)按比例分摊共享页,更公平。
smem-spss-r-p真实输出:
PID User Command Swap USS PSS RSS 14420 root /tmp/leak_exp/leak N/A 1.73% 1.73% 1.74% 6114 root [migration/0] N/A 0.15% 0.17% 0.22% 394 root /sbin/multipathd -d -s N/A 0.14% 0.15% 0.18%泄漏进程 PSS 1.73% 排第一,远超其他进程,一目了然。
6.3 Step 2:pmap -x+smaps锁定内存类型(实验 6 A2-A3)
确认是 PID 14260 后,用pmap看详细内存布局:
pmap-x14260关键行:
total kB 309908 308944 307304 RSS=308944, Dirty=307304再深入看/proc/<pid>/smaps中[heap]段:
Rss: 308944 kB Pss: 307357 kB Private_Dirty: 307304 kB ← 几乎全是私有脏页!结论:RSS 约 309MB,其中[heap]段的Private_Dirty占307MB,说明是私有堆泄漏,即 malloc 不 free 的经典模式。
6.4 Step 3:valgrind定位代码行(实验 6 B)
在测试环境用 valgrind 跑泄漏程序:
valgrind --leak-check=full ./leak真实输出:
==14278== LEAK SUMMARY: ==14278== definitely lost: 12,582,912 bytes in 11 blocks # 12MB,11个泄漏块 ==14278== indirectly lost: 0 bytes in 0 blocks ==14278== possibly lost: 0 bytes in 0 blocks ==14278== still reachable: 0 bytes in 0 blocks加--show-leak-kinds=all还能打印出每次malloc的调用栈,直接指向泄漏代码行。这是用户态泄漏定位的"杀手锏"(缺点:运行慢约 10–20 倍,只适合离线/测试环境)。
6.5 排查套路总结
| 步骤 | 命令/文件 | 目标 |
|---|---|---|
| 1 | free -h//proc/meminfo | 判断是AnonPages↑(用户态)、Shmem↑(共享内存)还是SUnreclaim↑(内核态) |
| 2 | smem -s pss -r/top | 找出"吃内存最多"的进程 |
| 3 | pmap -x <pid>//proc/<pid>/smaps | 区分[heap]、[stack]、mmap文件哪种在涨 |
| 4 | valgrind --leak-check=full/kmemleak/bpftrace | 用户态用 valgrind、内核态用 slabtop + kmemleak 定位代码行 |
七、本模块要点速记
- 内存泄漏三看:
AnonPages(用户态)→Shmem(共享内存)→SUnreclaim(内核态)。 - RSS 持续增长才是真泄漏,VSZ 增长只是虚拟地址预留。
- 用 cgroup 给服务加内存上限(
MemoryMax=),泄漏只杀自己,不连累整机。 top找不到去向时查Shmem:cat /proc/meminfo | grep Shmem+df -h /dev/shm+ipcs -m。SUnreclaim只涨不跌 = 内核态泄漏,用slabtop/grep filp /proc/slabinfo锁定哪个 slab。- 排查工具链:
smem(找进程)→pmap/smaps(定类型)→valgrind(定位代码行)。
八、下一篇预告
TCP 重传模块:当应用端看到"请求超时"、"连接重置"时,如何从内核态(/proc/net/snmp、ss -i、tcpretrans、tcpdump)区分是网络丢包还是对端异常?我们将用真实发包实验,演示 TCP 重传的完整排查链路。
附录:本文所有实验日志原文见
/tmp/ml_log.md,脚本见scripts/目录。欢迎读者在同等内核版本(6.8+)上复现验证。
[exit=0]