ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内存管理排查实战:free、top、pmap、vmstat命令行工具详解

Linux内存管理排查实战:free、top、pmap、vmstat命令行工具详解 做研发和运维这些年我最深的体会是内存管理这件事绝不能靠猜要靠命令行把系统状态一点点挖出来。很多人一遇到服务卡顿、进程被杀、Swap疯涨第一反应是乱加内存第二反应是重启但真正懂行的人会先打开终端用 free、top、ps、pmap、vmstat 这一套内存相关命令把现场数据拿到手里再下结论。这些命令看起来老掉牙却不花哨、不绕弯遇到OOM、内存泄漏、内核缓存回收异常时它们往往比任何监控面板都好用。这篇文章我想结合自己的实际排查经验把这些内存相关命令拆开讲透。我会先讲清楚内存管理到底在管理什么再逐个拆解常用命令的输出字段和背后逻辑然后聊一聊怎么用它们定位泄漏和异常最后给出一套可以抄作业的组合打法。不管你是服务端研发、运维工程师、平时写C/C/Go/Rust的选手还是正在备考“408内存管理”相关章节的学生这篇文章都会让你在终端里看内存时不再是一脸懵。1. 内存管理到底在“管”什么1.1 现代计算机的存储金字塔与观测的价值内存管理这个词听起来像操作系统的内部事情但实际上它跟每个开发者写的每一行代码都相关。现代计算机的存储体系是一层一层叠起来的CPU寄存器最快容量最小L1/L2/L3缓存速度次之主内存RAM再慢一个量级到了Swap交换区本质上就是拿磁盘顶替内存用速度会更慢几个数量级。操作系统在这一整套层级里要做的事情就是通过虚拟内存、分页、缺页中断、页面置换、Page Cache等机制让每个进程都觉得“我有足够大的连续空间可以用”。这也是为什么在没有图形界面的服务器上命令行依然是观测内存的第一手段。内存不像CPU使用率那样有个单一百分比就能说清楚它涉及物理内存总量、进程虚拟空间、共享库的重复计数、内核文件缓存、匿名页、Swap换入换出等一堆维度。你要是不看数据光凭感觉判断“内存是不是不够了”十次有八次会误判。比如我见过不少同事看着free里used很高就急着加内存结果一查真正占用高的全是可回收的Page Cache根本不构成压力。观测的价值就在于此内存相关命令能帮你把“模糊的内存焦虑”转化成“明确的事实判断”。free看整体水位top看进程实时占用ps做排名快照pmap钻进单个进程的地址空间vmstat观察换页行为。每一层都有对应工具组合起来就是一套完整的内存体检方案。1.2 一个进程的内存布局不是一张平铺的数组命令输出的很多字段比如VIRT、RES、PSS背后对应的是操作系统对进程地址空间的划分。一个C程序跑起来它的虚拟地址空间从低地址到高地址大致是这样排列的代码段text、已初始化数据段data、未初始化数据段BSS、堆heap向上增长、内存映射区mmap共享库、动态映射文件都在这最后是栈stack向下增长。堆既可以用brk系统调用扩展也可以用mmap分配大块匿名内存栈则是线程私有每个线程一创建就分配好一套栈空间。搞清楚这个布局再看命令输出就会顺很多。比如pmap pid看到一堆[anon]段基本就是进程在堆上申请的内存看到某个共享库的映射段变大了就要考虑是不是动态依赖被加载多了。再比如top里的VIRT和RES经常有人分不清我习惯用一个类比解释VIRT是“合同面积”是你虚拟地址空间里约定的总大小哪怕你根本没读写过那些页RES是“实际入住面积”是当前真的占用了物理内存的页面。一个程序malloc了10GB虚拟内存但只写了前面1GB那么VIRT会显示10GB左右RES可能只有1GB左右。这就是虚拟内存的惰性分配特性也是“看内存不能只看VIRT”的根本原因。理解了进程的内存布局和VIRT/RES的区别再往深走一层RES里面还有共享内存和私有内存之分。比如Nginx开了8个worker进程共享的.so文件段在物理内存里只有一份但每个进程的RES里都会算一次。如果你直接把8个RES加起来数字很可能超过机器总内存会造成“内存泄漏”的假象。真正科学的口径是PSSProportional Set Size按共享比例分摊每个进程只均摊自己用到的共享页那一份。后面说的smem工具就是专门干这个的。2. 绕不开的命令组free/top/ps/pmap 逐个拆解2.1 free先看整体水位排查内存问题我几乎都是从free开始因为它最直观地告诉你整个系统的内存到底处在什么水位。先看个实际输出$ free -h total used free shared buff/cache available Mem: 15Gi 7.8Gi 1.2Gi 1.1Gi 6.2Gi 6.5Gi Swap: 2.0Gi 256Mi 1.8Gi第一行里的total是物理内存总量没什么好说的。used是系统当前已经用掉的物理内存但要注意在不同版本的free里这个字段的算法有过变化。现在的procps-ng版本里used total - free - buff/cache也就是说它已经把buff/cache独立拿出来看了不把内核的文件缓存算进“进程真正用的内存”。所以看到used偏高别急着慌真正的关键在最后一个字段available。available是我最看重的指标它表示“在不触发Swap的前提下还能分配给新进程的内存估计值”。内核会动态估算哪些页是可回收的比如干净的文件缓存、可回收的页表项等。available比free能反映真实压力的多因为free只统计完全未分配的物理页而available把“现在虽被缓存占着、但随时可以回收分给别人”的部分也算进去了。这也是为什么有时候free很小、available却还有好几个G系统一点问题没有反过来如果free还挺多但available已经很低说明这些free页是保留页或碎片化严重实际压力并不小。常用参数再记几个free -g按GB显示、free -m按MB显示适合脚本解析free -s 5 -c 3每5秒输出一次连续输出3次适合观察内存变化趋势free -w把buff/cache拆成buff和cache两列显示能看得更细。拆开之后buffers主要对应块设备或文件系统元数据的原始缓冲cache则对应页缓存Page Cache和文件缓存包括tmpfs占用的内存也会记在cache里。识别这一点在容器和临时文件场景里特别重要因为/dev/shm或者tmpfs写多了cache会涨得很高但那不是进程内存泄漏。2.2 top看进程维度的实时消耗free解决的是“系统还有没有内存”的问题但真出了问题你还需要知道“到底是谁在吃内存”这时候top就该上场了。启动top后按M键可以让进程按照内存占用从高到低排序按P则是按CPU排序。用批处理模式也可以拿到一次性的快照$ top -o %MEM -b -n 1 | head -30-o %MEM指定排序字段-b是批处理模式-n 1只输出一轮配合head截取前面二三十行既简洁又适合放到脚本里。top输出里和内存相关的核心字段是VIRT、RES、SHR、%MEM。VIRT是虚拟内存总大小前面说过的“合同面积”RES是常驻物理内存也就是真实占用的物理页包含私有页和共享页SHR是其中的共享部分主要是共享库、共享内存段和tmpfs里的页%MEM就是RES除以物理总内存的百分比。RES虽然比VIRT实在但也不是万能口径。两个进程同时依赖同一个巨型共享库RES会把这份库各算一次加起来有可能超过物理内存。另外RES里面还可以再拆成匿名页RSS Anon和文件页RSS File这些在/proc/pid/status文件里看得很清楚$ grep -E VmPeak|VmSize|VmRSS|RssAnon|RssFile|RssShmem /proc/1234/status VmPeak: 1048576 kB VmSize: 524288 kB VmRSS: 61440 kB RssAnon: 20480 kB RssFile: 36864 kB RssShmem: 4096 kBVmPeak是历史峰值VmSize是当前虚拟内存总大小VmRSS就是RES。RssAnon是私有匿名页一般对应堆上malloc出来的内存RssFile是文件映射页比如mmap打开的文件和共享库RssShmem是共享内存页。判断泄漏的时候我会优先看RssAnon的曲线因为大部分泄漏都集中在堆上RssAnon持续增长基本就是堆没释放而RssFile增长则要看是不是文件缓存回收出了问题。2.3 ps 与 smem从进程排名到真实占用的修正top适合交互式观察但想快速拿到“当前内存占用TOP10”还是用ps最顺手。我最常用的命令$ ps aux --sort-%mem | head -10这条命令把当前所有进程的内存占用从高到低排出来。也可以用更精确的字段选择方式ps -eo pid,ppid,rss,vsz,%mem,comm --sort-rss | head -15这样输出的字段干净方便写脚本。这里有个特别容易踩的坑默认情况下ps -o rss输出单位是KB不是字节也不是MB。我看到过有人用脚本读RSS以为单位是字节直接除1024得到KB结果数据全错了。建议用ps -o rss的时候心里默认单位就是KiB要转成MB就除1024别多绕一道。单纯用ps按RES排序还有一个精度问题共享库被重复计算。这时候就需要smem出马。smem是我个人很推荐的命令它专门用PSS口径重新统计每个进程的内存占用。PSS就是把共享页按进程数量分摊比如一个100MB的共享库被10个进程用每个进程算10MB。这样把所有进程PSS加起来基本能对上物理内存总量不会出现“加起来比内存还大”的荒谬结果。$ smem -kt -p --sortpss-k自动选择合适单位-t显示总计-p按百分比显示。smem还能按用户汇总smem -u适合看“这个多用户机器上到底哪个用户把内存吃完了”这种场景。比如一台跑着多个Java服务的机器用top看每个进程RES都不高但物理内存还是紧张很可能就是共享内存段或者共享库重复计算造成的错觉这时候smem给出的PSS数据才会让你真正看清是哪批服务在消耗内存。2.4 pmap钻进单个进程的地址空间如果你已经锁定了某个进程下一步就是钻进它的地址空间看细节。pmap -x pid -d是我在排内存泄漏时最常用的组合不加-d也可以$ pmap -x 1234 Address Kbytes RSS Dirty Mode Mapping 0000557d8a8f4000 1024 1024 0 r-xp binary 00007f2f6c000000 65536 65536 65536 rw-p [anon] ...输出里每一行代表进程地址空间里的一段映射。Kbytes是该段虚拟大小RSS是实际占用物理内存Dirty是脏页数量Mapping是对应的映射名。对于排查堆内存问题重点看[anon]段因为匿名段绝大多数来自于堆上的malloc或mmap分配。如果一个进程的[anon]段越来越大那你基本可以断定它在持续分配内存接下来要做的是结合valgrind或代码审查定位泄露点。pmap还有一个容易被忽略的用途就是看文件映射段。比如你发现某个进程RSS里File部分涨得厉害用pmap能看到它到底映射了哪些文件、映射了多少。曾经我排查过一个日志系统内存上涨的问题最后就是用pmap发现进程把日志文件用mmap映射了一大块文件不断增长RSS也跟着涨。这种问题不看pmap只盯着RSS总量可能得排查半天才发现是文件映射而非堆泄漏。3. 用命令定位内存异常泄漏、缓存、交换3.1 内存泄漏的“土办法”与高精度工具很多人说到内存泄漏第一反应就是上valgrind但在生产环境我建议先用“土办法”做一轮快速筛查锁定嫌疑进程后再上高精度工具。土办法的核心思路很简单周期性记录目标进程的RSS观察是否存在单调递增且不下降的趋势。一个简单的循环脚本就能实现#!/bin/bash TARGET$1 LOG/tmp/mem_scan_rss.log for i in $(seq 1 120); do echo $(date %F_%T) $(ps -o rss -p $TARGET 2/dev/null | awk {printf %d, $1}) $LOG sleep 1 done运行一两分钟然后用awk {print $2} $LOG | sort -n | tail -5看最大值再配合cat $LOG看时间序列。如果RSS从几万KB一路涨到几百MB且没有回落迹象基本上可以判断是内存泄漏或内存持续膨胀。这种方法的优点是开销极低、部署简单直接在生产环境跑都没问题缺点是分不清泄漏和缓存堆积所以只能用来“锁定目标”。锁定目标之后再用高精度工具验证。C/C项目首选valgrind的memcheck$ valgrind --leak-checkfull --show-leak-kindsall --track-originsyes ./your_program输出会分成几个类别definitely lost确定泄漏失去指针引用、indirectly lost间接泄漏被确定泄漏的块引用、possibly lost可能泄漏指针指向块内部而不是开头、still reachable仍可达但未释放进程结束时残留。排查时要重点关注definitely lost和possibly lost。definitely lost通常意味着程序把指针弄丢了或者覆盖了possibly lost则常见于指针被偏移保存的场景也要认真看。用valgrind有个很大的教训它会比正常程序慢十到五十倍内存开销也会翻几倍直接跑生产或大规模压测肯定扛不住。正确姿势是先准备一个小规模复现用例或者用压测工具发少量请求只要能触发泄漏路径就行。另外valgrind检测的是用户态内存分配对于mmap映射的大块内存和共享库问题它提供的线索有限这时候还是要回头用pmap和/proc/pid/smaps分析。3.2 缓存回收与Swap波动怎么看要不要管内存问题里最容易引起误判的就是buff/cache那张大额资产。很多新人看到free -h显示cache好几个G、free不到1G第一反应是“内存被吃光了”开始满世界找谁在缓存文件。实际上Page Cache是Linux内存管理的正常机制文件读写后内核会把数据留在内存里下次访问就能直接命中这是速度优化不是故障。真正判断内存压力的指标是available低不低以及Swap有没有频繁换入换出。Swap也是最容易触发恐慌的指标之一。free里Swap一栏出现了使用量或者vmstat里si、so两列有持续的数值说明内核正在把内存页换到磁盘或者从磁盘换回来。偶尔的换出不可怕内核可能只是想回收一些不活跃的匿名页给Page Cache腾地方但如果si、so持续大于零同时available很低就意味着系统确实在承受内存压力进程的访问可能因为换页而卡顿。这时候我会组合三条命令一起看$ free -h $ vmstat 1 5 $ sar -B 1 5vmstat里需要关心的是r运行队列、b阻塞进程、si每秒从磁盘换入内存、so每秒从内存换出到磁盘、bi/bo块设备读写。如果si、so长期非零基本可以断定Swap在拖后腿。sar -B则能看到更细的换页统计和错误页。看完数据如果确认是Swap压力我会先排查高占用进程而不是直接调vm.swappiness参数。swappiness默认60是内核的“倾向性”参数数值越大越积极地把匿名页换出但它不是简单的“内存用到60%就开始换出”。生产环境我很少调整它除非有很明确的业务特征比如大批量后台任务愿意牺牲响应时间换吞吐。还有一个小众但高频的操作争议点手动清理Page Cache。sysctl vm.drop_caches3可以强制释放页缓存但我说句实话在正常生产环境里几乎没有必要这么做——内核本来就会在内存压力下自动回收干净页你手动清掉反而会让接下来的文件访问全部走磁盘性能瞬间变差。除非你是刚做完一次性的批量导入、想立刻腾出内存给下一个任务否则不要碰这个参数。如果发现cache持续超大且不回收先看看是不是有tmpfs在疯狂写文件或者有进程mmap了超大文件不释放这才是根本原因。3.3 系统级水位监控组合单个命令只能看到某个瞬间的切片真实排查里我通常会把几个命令组合起来轮询形成一条时间线上的数据。最简单的组合是用watch把几个命令拼在一起展示$ watch -n 1 free -h; echo ---; ps aux --sort-%mem | head -10这样每秒钟刷新一次既能看整体水位也能看头部进程。如果需要记录历史数据就用vmstat 1 30跑半分钟然后把输出重定向到文件里事后分析。对生产服务器我更推荐用sar做定时采集因为sar默认由sysstat服务每十分钟记录一次系统指标事后可以直接sar -r查看内存历史曲线比从零搭监控要省事得多。在容器和云原生环境里还有一个必须知道的点容器内看到的free往往是宿主机的物理内存不代表容器可以用的额度。现在主流容器用cgroup做隔离看内存水位要去cgroup的统计文件里看。cgroup v2的路径是/sys/fs/cgroup/memory.current和/sys/fs/cgroup/memory.max如果current持续接近max说明容器要OOM了cgroup v1则在/sys/fs/cgroup/memory/memory.usage_in_bytes和memory.limit_in_bytes里。很多线上事故就是容器内free显示还有好几个G可用但容器被杀原因正是cgroup限制已经打满。这种时候看系统的内存相关命令要记得加一层cgroup视角。4. 内存命令的组合打法监控脚本、决策对照与高频踩坑4.1 把命令变成可复用的监控脚本排查经验多了之后我习惯把常用命令固化成一个监控脚本遇到问题直接跑不用临时敲一遍。下面这个是我电脑里存了很久的版本适合定位某个进程的内存走势#!/bin/bash TARGET$1 INTERVAL${2:-5} LOG${3:-/tmp/mem_scan.log} while true; do TIME$(date %F_%T) RSS_KB$(ps -o rss -p $TARGET 2/dev/null | awk {printf %d, $1}) VIRT_KB$(ps -o vsz -p $TARGET 2/dev/null | awk {printf %d, $1}) AVAIL_MB$(free -k | awk /^Mem:/{printf %.1f, $7/1024}) SWAP_MB$(free -k | awk /^Swap:/{printf %.1f, $3/1024}) echo $TIME rss_kb$RSS_KB virt_kb$VIRT_KB avail_mb$AVAIL_MB swap_mb$SWAP_MB $LOG sleep $INTERVAL done跑起来之后tail -f /tmp/mem_scan.log就能实时盯数据。RSS持续增长、available持续下滑、Swap开始出现并上升这三个信号同时出现基本就是进程在快速消耗内存且系统进入了交换状态不用再犹豫直接把进程打上“重点观察”标签。脚本里只记录不能杀进程生产环境先做观测永远比先动手干预要稳。4.2 值得记住的一组数值与误区内存相关命令很多但真正决定你能不能正确解读的往往是一些细节。单位问题首当其冲free的-h默认按人可读单位显示但脚本解析时我推荐固定用free -k加awk提取ps的RSS默认是KiB而vmstat输出里free列单位是KiB/proc/pid/status和smaps里都是KiB。混用单位是人最容易犯的错我见过一个监控脚本把free -m的输出和ps的KRSS直接相加误报了一整夜的内存告警。命令之间参数多、输出格式杂写脚本一定要先打印一行样例数据确认单位。第二个要记住的认知误区是malloc了一大块内存不代表RES一定会立刻涨。Linux的虚拟内存是惰性分配的malloc只是改了进程地址空间的页表映射真正拍板分配物理页是“第一次写入”时触发缺页中断。所以进程VIRT高、RES低是完全正常的。反过来RES并不总等于物理内存“独享量”因为里面包含共享库和共享内存。所以判断一个进程是不是真的异常不能只看某一条命令的单个数字要结合RssAnon和PSS一起看。这个原则对写Java、Go、Rust、Julia甚至Python的工程师都适用。我自己用Julia做过一段性能优化当时最直观的判断方式就是跑任务前记录ps -o rss任务结束后再看回落情况。如果结束以后RSS没有明显下降要么是进程本身没有归还物理页要么是运行时的分配器把内存留在自己池子里了。系统命令不会撒谎它只负责把现象摊在桌面上真正定位修复还是得回到语言层面的profiler里。4.3 常见问题与排查速查表最后整理一份我在实战里反复用到的速查表按现象分门别类方便你遇到相似问题时快速定位。这些条目看起来简单但每一条背后都是我踩过的坑现象最先看的命令典型结论处理建议服务进程被系统杀掉日志无异常dmesg -T | tail -50OOM Killer触发查看被杀进程内存占用定位高占用进程优化或扩容慎重调整overcommit参数单进程RSS持续升高不回落watch -n 1 ps -o pid,rss,vsz,%mem,comm -p PID堆内存未释放可能是泄漏结合valgrind或语言profiler复现定位free显示available极低但进程RES都不高cat /proc/meminfo | grep -E MemAvailable|Slab|Dirty内核Slab或Dirty Page占用过多检查内核模块或文件系统写负载考虑适当触发内存回收多个进程RES相加超过物理内存smem -kt -p --sortpss共享库/共享内存被重复计算用PSS口径重新评估确认是否真的超卖Swap占用增长系统卡顿vmstat 1 5看si/so内存压力大正在换页排查高占用进程停止非必要任务必要时关闭或扩大Swap容器内进程被杀但宿主机free还有余量/sys/fs/cgroup/memory.current和memory.maxcgroup限制打满调高容器内存限制或者优化进程内存占用cache长期占据大量内存怀疑无法回收watch -n 1 cat /proc/meminfo | grep -E Cached|Dirty|WritebackPage Cache正常Dirty或Writeback堆积才有问题等待磁盘刷新不用手动清cache除非有明确的性能测试目的这个表不是万能的但能覆盖我实际工作中遇到的大部分内存问题。很多看似惊悚的内存告警按照这个顺序跑一遍命令往往十分钟内就能得出一个靠谱的判断。我个人在实际操作中的体会是内存排查有一套固定的“心法”先读后动、先整体后局部、先系统后进程。先读是不管什么环境先执行只读命令拿现场不要上来就重启或清缓存先整体是看一眼free和vmstat确定系统级别的压力方向先系统后进程是从整体WATER落到进程RSS最后再用pmap钻进地址空间。这套顺序能避免很多无效操作比如在system cache导致的假性告警里白白把业务重启了一轮。最后再分享一个小技巧压测前把每个关键进程的RSS快照和当前/proc/meminfo里MemAvailable、MemFree的数值拍个照存成文件。压测结束后再拍一次对比这组数据你就能轻松分辨出“压测后内存到底有没有还回来”。它不需要监控平台不需要额外工具只需要这几条最基础的内存相关命令却往往能在问题复盘时省下大半天扯皮的时间。Linux的内存相关命令终究不是花架子关键看你有没有把每一条都用在正确的场景里。
返回列表