Linux 内核技术实战课 · 内存泄漏模块:从 RSS 到 Shmem,把“消失的内存“找回来

发布时间:2026/7/20 10:39:35
Linux 内核技术实战课 · 内存泄漏模块:从 RSS 到 Shmem,把“消失的内存“找回来 Linux 内核技术实战课 · 内存泄漏模块从 RSS 到 Shmem把消失的内存找回来作者资深内核稳定性工程师实验环境ecs-fddb-0002Ubuntu 24.04 LTS / 内核 6.8.0-106-generic / 8C16G声明本文所有命令与观测数据均来自真实服务器实测未做任何编造。一、开篇内存泄漏为什么难找内存泄漏是 Linux 系统中最常见、也最隐蔽的稳定性问题。轻则单个进程 RSS 持续膨胀重则整机OOMOut Of Memory假死SSH 都连不上。更头疼的是——有时候你top翻遍了也找不到内存去哪了free却显示shared吃掉几个 G。本模块通过6 个真实实验带你建立起一套完整的内存泄漏排查体系看懂指标 → 定位进程 → 锁定类型 → 找到根因。读完本文你将能回答以下三个问题进程哪些内存类型容易泄漏如何把泄漏关进笼子避免整机假死遇到内存消失了top找不到去向时怎么查二、基础篇 · 进程哪些内存类型容易泄漏2.1 先读懂/proc/meminfo—— 内存的CT 报告内存泄漏排查的第一步是看懂系统级内存指标。在服务器上执行cat/proc/meminfo以下是本机基线实测数据节选关键行指标本机值与内存泄漏的关系AnonPages4,477,228 kB (≈4.4 GB)用户态匿名页堆/匿名映射/栈这是最常见泄漏涨的地方Mapped126,936 kB被映射到进程地址空间的文件页文件映射泄漏也在此体现Shmem2,624 kBtmpfs/共享内存不属于任何进程的 RSS是消失的内存主角Slab194,100 kB内核对象缓存总和 SReclaimable SUnreclaimSReclaimable106,084 kB可回收内核 slabdentry/inode不算泄漏SUnreclaim88,016 kB不可回收内核 slab若只涨不跌 → 内核态泄漏PageTables13,712 kB进程页表占用的物理内存进程数/映射多时会涨记忆口诀用户态泄漏看AnonPages共享内存看Shmem内核态泄漏看SUnreclaim文件缓存看Cached但一般可回收。2.2 经典案例malloc 不 freeRSS 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谁才是真吃内存VmSizeVSZ进程虚拟地址空间总大小包含了已mmap但尚未分配物理页的区域。光看 VSZ 暴涨未必是真泄漏。VmRSSRES实际占用的物理内存Resident Set Size。top的%MEM基于 RSS 计算。本例中两者几乎相等因为我们对每块内存都写脏触发了缺页中断物理页真的被分配了。实战中RSS 持续增长才是真吃物理内存VSZ 增长只说明虚拟地址被预留。2.4 进程哪些内存类型容易悄悄泄漏按本课程框架最容易泄漏的 5 类内存堆heapmalloc/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并设置MemoryMax512Mdmesg-C# 清空环形缓冲干净采集systemd-run--scope-pMemoryMax512M ./leak真实输出[leak] step1 allocated50 MB pid12849 [leak] step2 allocated100 MB pid12849 ... [leak] step10 allocated500 MB pid12849 systemd-run 返回码: 137 # 1289 SIGKILL被 OOM Killer 终结dmesg 中 OOM 记录[ 2208.355205] oom-kill:constraintCONSTRAINT_MEMCG,nodemask(null), cpuset/,mems_allowed0, oom_memcg/system.slice/run-....scope, taskleak,pid12849,uid0 [ 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 关键解读与生产建议constraintCONSTRAINT_MEMCG这是关键证据——OOM 发生在内存 cgroup 内仅杀该 scope 的进程。整机其他进程、SSH、系统服务完全不受影响。如果是全局 OOM会是CONSTRAINT_NONE且可能杀掉任意进程。taskleak pid12849谁被杀了、吃了多少内存一目了然。返回码 137 128 SIGKILL(9)从用户态也能确认进程是被 kill 掉的。生产建议对关键服务Web 服务器、API 网关、数据库代理在systemd单元中加MemoryMax/MemoryHigh。或者手动写 cgroup v2echo 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 共享内存验证我们还用shmgetshmat创建了 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 个 fdfilp OBJ(压力中): 101888 ← 约 10 万与 fd 数严格对应 SUnreclaim(压力中): 118260 kB ← 约 30MB进程退出后filp OBJ(回收后): 1948 SUnreclaim(回收后): 88428 kB ← 回到基线5.3 判漏准则上述实验只是正常压力进程退出后 slab 被回收。真正的内核态泄漏判断标准是在无明显业务压力、且对象应已释放的情况下SUnreclaim及nr_slab_unreclaimable持续单调上升、不回落基本可判定为内核态内存泄漏。观测手段# 1. 看全局趋势watch-n5grep 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/memleak6.2 Step 1smem找出谁吃 PSS 最多实验 6 A1传统top的 RSS 会把共享库重复计算而PSSProportional 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 2pmap -xsmaps锁定内存类型实验 6 A2-A3确认是 PID 14260 后用pmap看详细内存布局pmap-x14260关键行total kB 309908 308944 307304 RSS308944, Dirty307304再深入看/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 3valgrind定位代码行实验 6 B在测试环境用 valgrind 跑泄漏程序valgrind --leak-checkfull ./leak真实输出14278 LEAK SUMMARY: 14278 definitely lost: 12,582,912 bytes in 11 blocks # 12MB11个泄漏块 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-kindsall还能打印出每次malloc的调用栈直接指向泄漏代码行。这是用户态泄漏定位的杀手锏缺点运行慢约 10–20 倍只适合离线/测试环境。6.5 排查套路总结步骤命令/文件目标1free -h//proc/meminfo判断是AnonPages↑用户态、Shmem↑共享内存还是SUnreclaim↑内核态2smem -s pss -r/top找出吃内存最多的进程3pmap -x pid//proc/pid/smaps区分[heap]、[stack]、mmap文件哪种在涨4valgrind --leak-checkfull/kmemleak/bpftrace用户态用 valgrind、内核态用 slabtop kmemleak 定位代码行七、本模块要点速记内存泄漏三看AnonPages用户态→Shmem共享内存→SUnreclaim内核态。RSS 持续增长才是真泄漏VSZ 增长只是虚拟地址预留。用 cgroup 给服务加内存上限MemoryMax泄漏只杀自己不连累整机。top找不到去向时查Shmemcat /proc/meminfo | grep Shmemdf -h /dev/shmipcs -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上复现验证。[exit0]