☰
Linux /proc 系统详解:进程排查与内核参数调优实战
2026/10/16 10:10:49 网站建设 项目流程

第一次接触 /proc,我是把它当成一个“放着各种监控文件的神奇目录”来用的。看内存就看 meminfo,看 CPU 就看 cpuinfo,感觉就是一个只读的指标宝库。真正让我改变认知的是一次进程假死排查:突然某个服务端口还在监听,但请求全部超时,用 strace 看半天没输出,最后是cat /proc/[pid]/stack和/proc/[pid]/status里的进程状态救了我。那一刻我才意识到,/proc 里每一个文件都是内核实时生成的“窗口”,它不是磁盘上的普通文本,而是你伸手进内核内部摸状态、甚至改参数的通道。

这篇文章我打算用我自己的使用路线图来讲,不堆砌教科书名词,直接说说哪些目录值得记、哪些字段容易被误读、哪些文件平时不用但关键时刻能救命。如果你是做后端开发、运维、SRE,或者单纯喜欢把 Linux 底层机制玩明白,应该能从里面找到一些可以立刻上手的东西。

1. 先认清 /proc 是什么:它不是磁盘目录,而是内核的改造窗口

1.1 为什么叫它“虚拟文件系统”

很多人第一次看到/proc这个名字,会以为它是“process”的缩写,里面存的是进程相关的文件。这个理解方向没错,但不完整。/proc 是一个挂载在内存里的虚拟文件系统,挂载类型是proc,大部分文件在磁盘上并不存在。你ls -l看到的大小通常写 0,但cat又永远有内容,因为每次读取时,内容都是内核现场生成并返回的。

这个设计最早是为了给进程相关的调试信息提供一个简单的文件接口。后来扩展到系统级信息:内存、CPU、中断、设备、网络栈、内核参数,几乎都被塞了进来。你可以把它理解成“内核的一张可读可写的工作台”:读文件是向内核要状态,写文件是向内核下达配置变更。正因为这样,/proc 里的文件才不建议用常规的文本编辑器改,也不建议用普通的cp去备份,读写时机错了就会读到奇怪的结果。

理解“动态生成”这一点很重要,它决定了你排查问题的姿势。比如你写一个脚本循环读取/proc/meminfo,会发现某些字段在不停跳动;如果你把它当成静态快照去分析,就容易得出错误结论。还有,因为文件是虚拟的,很多文件看起来很大,实际上读取并不消耗多少磁盘 IO,代价主要在内核生成内容的那一点 CPU 上。这也是我放心在批量脚本里频繁读/proc/[pid]/status的原因。

1.2 /proc 和 /sys 的分工

我经常被人问到:“/proc 和 /sys 到底有啥区别,感觉都在暴露内核参数?”简单说,两者的侧重点不一样。/proc偏向进程和系统运行状态,是看“现在怎么样”的地方;/sys(更准确说是/sys/devices、/sys/class、/sys/kernel)偏向设备模型和内核子系统,是看“设备怎么组织、驱动怎么配置”的地方。

比如要看磁盘设备队列的调度策略,去/sys/block/sda/queue/scheduler;要看网卡队列数,去/sys/class/net/eth0/queues/rx-0。但如果你要排查当前打开了多少文件、某个进程的线程状态、内存碎片到了什么程度,那还是得回到/proc。

我自己的经验是:业务故障排查优先走/proc,硬件和驱动问题优先走/sys,两者都不确定时先用dmesg看内核日志。它们之间有些文件确实重叠,比如/proc/sys/kernel和/sys/kernel都可能出现类似名字,但内容含义不完全一样,使用时一定要确认你改的是哪个层级的参数。

2. 单进程目录的读法:/proc/[pid] 里的高频文件和我的使用习惯

2.1 先搭建一个“进程快照”脚本

排查单进程的时候,最怕在ps输出里猜。我通常会直接进/proc/<pid>目录,先看几个基础文件,形成一个“快照要点”。

pid=12345 echo "进程名: $(cat /proc/$pid/comm)" echo "状态: $(cat /proc/$pid/status | grep '^State')" echo "虚拟内存: $(grep VmSize /proc/$pid/status)" echo "常驻内存: $(grep VmRSS /proc/$pid/status)" echo "打开的文件数: $(ls -l /proc/$pid/fd | wc -l)" echo "启动命令: $(tr '\0' ' ' < /proc/$pid/cmdline)" echo "工作目录: $(readlink /proc/$pid/cwd)" echo "启动时间: $(cat /proc/$pid/stat | awk '{print $22}')"

这个脚本的核心思路是“一次读取多个维度”,把 CPU、内存、文件句柄、路径变化放在一起看。大多数时候,问题的苗头藏在交叉点:比如一个进程VmRSS不高,但fd数量持续上涨;或者State一直是D(不可中断睡眠),说明它在等块设备 IO,你等再久也不会主动释放。

cmdline 用tr把\0替换成空格来处理,因为内核在/proc/<pid>/cmdline里用空字符分隔参数,直接用cat会看到一坨。这个方法我一直在用,比某些工具输出的“可读命令”更接近原始执行形态。

2.2 status 中的字段不能想当然

/proc/<pid>/status里的字段名很直观,但有几个特别容易踩坑。最常见的是把VmSize当成“内存占用”来做监控,这会在内存告警时产生大量误报。VmSize是虚拟地址空间大小,包含了进程可能用到但根本没分配物理页的地址区间;一个进程声明了 10GB 虚拟地址空间,实际物理内存可能只有几十 MB。真正看物理内存占用,优先看VmRSS,它是当前驻留在物理内存中的部分。

但VmRSS也不是完美的。它包含共享库、共享内存映射中被该进程实际触达的页面。也就是说,如果多个进程加载同一个动态库,每个进程的VmRSS里都算了一份副本,这既有误导也有合理性。更准确的观测手段是看/proc/<pid>/smaps里每个映射的Pss(Proportional Set Size),它会把共享页按进程数量分摊。后面第 5 节我会再说这个文件的用法。

另外State字段里那些缩写也容易让人迷惑:S是可中断睡眠,D是不可中断睡眠,R是运行态,T是暂停,Z是僵尸。我在排查“进程卡死”问题时,看到D的优先级比看到Z高得多,因为D状态往往意味着内核态 IO 在等待某种资源,等待原因可能是磁盘故障、也可能是 NFS 网络断了,这时候操作系统层面基本无能为力。

2.3 fd、maps、smaps 怎么看

文件描述符目录/proc/<pid>/fd里是一堆指向真实对象的符号链接,ls -l可以直接看到每个 fd 指向哪里。我最常用的两条命令:

# 统计进程打开了多少 fd ls -l /proc/<pid>/fd | wc -l # 按 fd 归一化列出目标文件 ls -l /proc/<pid>/fd | awk '{print $11}' | sort | uniq -c | sort -rn

第二条命令很实用,经常能一眼发现连接泄漏或者日志文件被异常打满。比如一个 Java 进程的 fd 里有成百上千个/tmp/xxx或 sockets,就知道大概率是连接池没有回收,或者临时文件没有清理。

/proc/<pid>/maps是进程的虚拟地址空间地图,每一行代表一个映射区域。排查“为什么一个进程 RSS 这么大”时,我会先看 maps 里哪些段是可写的,再配合 smaps 计算:搜索smaps中的Pss:字段,汇总后就能得到这个进程对物理内存的真实贡献。

如果你需要快速得到跨进程共享到底占了多少,也可以直接看/proc/<pid>/smaps_rollup,它是内核提供的汇总文件,读起来比逐个解析 smaps 快得多。新版内核支持这个文件,老内核没有,遇到语法错误时先确认内核版本。

2.4 通过 io、stack 看卡死进程

进程假死时,很多人第一反应是 gdb 或者 pstack,但线上环境装调试器不方便。我的替代方案是:

# 看当前系统调用 cat /proc/<pid>/syscall # 看内核栈 cat /proc/<pid>/stack

/proc/<pid>/syscall显示进程当前正在执行的系统调用号及参数,需要对照系统调用表才能看懂数字,但配合strace -p使用效率很高。/proc/<pid>/stack则直接显示内核栈的调用符号,能看到进程卡在内核的哪个函数里。当年排查那个假死问题,就是看到栈里停在某个文件系统的 lock 上,才定位到是文件锁的冲突。

注意,这两个文件通常需要 root 权限或ptrace权限,普通用户可能读不了。另外,读取stack在某些内核版本上会被拦,需要开启/proc/sys/kernel/stack_guarding之类的配置。我一般把它当成“黑盒环境下的临时探针”,不作为一个默认常开的监控手段。

3. 全局文件读懂系统状态:meminfo、loadavg、stat、zoneinfo、buddyinfo

3.1 meminfo 的正确打开方式

/proc/meminfo是 Linux 内存状态最直接的源头,free命令本质上就是读它翻译后输出的。字段很多,我最关注这几个:

  • MemTotal:物理内存总量。
  • MemFree:完全未分配的空闲页。
  • MemAvailable:估算的“可用于启动新程序的内存”,这个值比MemFree + Cached更实际,因为内核考虑到了回收页面的代价。
  • Buffers:块设备缓存。
  • Cached:页面缓存和 tmpfs 占用的内存。
  • SReclaimable:可回收的内核内存,通常是 slab 里可回收的部分。

很多人看内存只盯MemFree,结果发现明明剩余很多却 OOM,或者free显示很少但系统没事。原因就在于Cached和SReclaimable都是“可以依赖于内核自动回收”的,只有当这些都不能及时释放时,才可能出现分配失败。所以我判断内存是否紧张,先看MemAvailable,再看Cached的变化趋势。

如果要做更细的内存压力分析,我会用/proc/pressure/memory。它通过some和full两组指标表示内存分配踩踏的概率。some高但full低,说明部分进程间歇性等内存;full高则意味着系统整体在等待内存。这两个文件比单纯看数值曲线更接近“体感”,我在调大规模 Java 容器时经常用。

3.2 loadavg 与 CPU 排队不是一回事

/proc/loadavg内容很短,通常长这样:

0.52 0.58 0.60 2/451 22345

前三个数字分别是 1 分钟、5 分钟、15 分钟的平均负载,第四个数字是“当前可运行实体/总实体数”,最后一个是最新创建进程的 PID。很多人把 load average 直接等价于 CPU 使用率,这是个大坑。load average 统计的是处于可运行状态和不可中断睡眠状态的进程数量,D状态任务也被算进去。

所以当磁盘 IO 卡死时,load average 可能飙到几十甚至几百,但 CPU 使用率可能很低。我遇到过一次 MySQL 所在机器 load 高达 30,CPU 却只有 5%,排查下来是存储阵列超时导致大量 IO 在等待。那之后我看负载不再单独看 loadavg,而是配合/proc/stat里的 CPU 时间片变化和/proc/pressure/io一起判断。

3.3 stat、interrupts、buddyinfo 的底层数据

/proc/stat第一行是 CPU 汇总,后面cpu0、cpu1等是每核心时间片。字段顺序是user nice system idle iowait irq softirq steal guest guest_nice,单位是 USER_HZ,通常约等于 100 分之一秒。做 CPU 使用率监控时,不建议直接保存某个时刻的idle值,而是隔一段时间取两个快照做差分,这样才能得到瞬时占用。很多监控系统都是这么算的,自己写的时候也尽量用同样的思路,避免单次读数的偏差。

/proc/interrupts可以看到每个中断号在每个 CPU 核上的触发次数。排查网卡单核被打满的问题时,我会看某个网卡中断是不是全集中在一个 CPU 上,如果是,再配合irqbalance或者设置RPS(Receive Packet Steering)来分散中断。

/proc/buddyinfo显示的是内存页块的分配情况,它按 order(2^order 个连续页)列出每个内存区域剩余的页块数量。看到高 order 空闲页为 0,说明内存碎片很严重,后续即使整体内存还剩很多,也可能出现order-4以上的分配失败。虚拟机和容器环境里,这类问题通常需要靠内存 compaction 或调整 numa 策略缓解。

4. 把 /proc/sys 当运行时控制台:内核参数改写的边界与经验

4.1 从文件路径到 sysctl 键名的转换规则

/proc/sys下面并不是普通状态展示,而是可以直接修改的内核运行时参数。规则很简单:把路径里的/proc/sys/前缀去掉,把/换成.,就是对应的sysctl键名。

比如:

/proc/sys/net/ipv4/ip_forward -> net.ipv4.ip_forward /proc/sys/vm/swappiness -> vm.swappiness /proc/sys/fs/file-max -> fs.file-max

直接用sysctl命令或者写文件都可以。写文件的命令是:

echo 1 > /proc/sys/net/ipv4/ip_forward

系统里推荐使用sysctl -w net.ipv4.ip_forward=1,它会做参数校验,还能顺手把变更记录到日志。直接 echo 到文件不是不行,但没有那层保护,写错类型可能毫无提示。

4.2 参数立即生效但不会持久化

这是最容易踩的坑之一。往/proc/sys写入的值,会立刻影响当前运行的内核,但重启后基本都会还原。除非你把配置写进/etc/sysctl.conf或/etc/sysctl.d/*.conf,否则掉电后一切回到内核编译或启动参数决定的值。

我见过的线上事故里,有一种是有人改了vm.overcommit_memory或者某个网络参数,当时看起来没事,但后续机器重启,参数还原,业务表现完全变了,大家误以为是代码发布引起的。所以我的习惯是:任何/proc/sys改动之前,先记录原始值,改动之后立刻验证效果;如果确定要长期保留,一定同步落盘,并且用注释说明改这个参数的原因和预期收益。

4.3 我常用的三个安全参数组

这里说“安全”是相对而言,所有内核参数都要在理解基础上小范围验证。我最常用的是这三组:

第一,vm.swappiness。它控制内核回收内存时,对匿名页和文件缓存页的偏好程度。范围是 0 到 100,默认通常是 60。跑数据库或延迟敏感应用的机器,我会把它调到 10 左右,降低内核把“进程堆栈”换出到交换分区的倾向。调之前要看磁盘性能,如果 swap 本身很慢,调低确实有帮助;如果压根没有 swap,这个参数影响很小。

第二,fs.file-max和fs.nr_open。前者是全局文件句柄上限,后者是单进程可以打开的上限。遇到Too many open files错误,先看这两个值是否到顶,再结合第 5 节的排查链路确定是配置不够还是代码泄漏。直接调大能临时缓解,但不根治问题。

第三,net.core.somaxconn和net.ipv4.tcp_max_syn_backlog。这是承接高并发 TCP 连接时防止握手队列溢出的常用参数。注意单个参数调整并不能包治百病,如果连接积压严重,先看应用本身是否在处理完请求前就把队列占满。

4.4 改错了怎么恢复

恢复 /proc/sys 有几个思路。如果只是临时改,用 sysctl 改回即可;如果忘了原始值,可以通过sysctl -a查当前值,但查不到启动时的值。最稳妥的办法是在改动前把关键参数的值存到一个文本文件里。

另外,不要把“不懂的参数”全部翻出来改。/proc/sys/kernel/*里有一堆看起来很高级但影响面极大的开关,比如kernel.unknown_nmi_passthrough、kernel.pty_reserve,这些外部资料少、行为差异大,普通场景完全没必要动。我的原则是:没有监控数据支撑的调优,一律不做;做了调优但没有回滚方案的,先把回滚命令写成注释放在变更记录里。

5. 一次模拟故障排查:文件描述符耗尽的完整链路

5.1 从报错到现场取证

假设线上服务突然开始报java.io.IOException: Too many open files,或者日志里出现socket: too many open files。常规操作是看 ulimit,但 ulimit 显示的只是单进程上限,真正的全局消耗要看/proc/sys/fs/file-nr。我先执行:

cat /proc/sys/fs/file-nr

这个文件会输出三个数字:已分配的文件句柄数、从未用过的句柄数、系统支持的最大句柄数。第一个数接近第三个数时,说明全局快满了。

很多人忽略了第三个数字,实际它就是fs.file-max的值。如果全局数值接近上限,再大的 ulimit 也救不了,因为系统已经分配不出新的文件对象了。

5.2 逐进程扫 fd,找出谁在泄漏

全局指标确认后,下一步就是找“谁在大量占 fd”。写一个轻量循环扫描所有进程:

for d in /proc/[0-9]*; do pid=${d#/proc/} cnt=$(ls -l $d/fd 2>/dev/null | wc -l) echo "$cnt $pid $(cat $d/comm 2>/dev/null)" done | sort -nr | head -20

这个脚本在进程数量较多的机器上也能跑完,但注意ls -l $d/fd在权限不足时会报错。需要 root 执行。如果某个进程 fd 数量特别高,再单独看它是 socket 还是普通文件:

ls -l /proc/<pid>/fd | awk '/socket:/{s++} /\.log/{l++} {print $11}' | sort | uniq -c | sort -rn | head

大多数泄漏场景里,你会看到大量处于CLOSE_WAIT状态的 socket。这是因为对端已经关闭连接,但本地应用没有关闭自己的 fd。这时候光看ss -tan | grep CLOSE_WAIT也行,但联系不到进程很麻烦;用/proc/<pid>/fd符号链接里的 socket inode,再配合/proc/net/tcp可以反查那个 socket 的本地和远端端口,整个证据链就闭环了。

5.3 临时缓解的正确姿势

找到泄漏进程后,如果的业务不能立即重启,临时方案是提高进程的 fd 限制。如果用的是 systemd 管理,可以临时通过systemctl show查看当前限制,但运行时修改通常还是得靠应用自身的配置或者prlimit。

prlimit --pid <pid> --nofile=65535:65535

prlimit可以在不重启进程的情况下调整资源限制,这是我最常用的临时手段。但必须强调,这只是给排查争取时间。真正根因还是代码里的连接没有 close,或者连接池里的连接没有被正确检测为失效。如果不想等到告警再处理,可以把前面那个扫描 fd 的脚本做成低频定时任务,当一个进程的 fd 数量在短时间内翻倍时直接报警,比看到Too many open files再介入会从容很多。

5.4 内核态 vs 用户态的混淆点

还有一个容易混淆的点:fd 耗尽时不一定是用户态进程的问题,也可能是内核线程在消耗文件对象。比如某些内核驱动、NFS 客户端、rpc 服务会在内存中持有大量文件引用。这种时候ps列表里看到的进程 fd 都很低,但/proc/sys/fs/file-nr已经很高。

此时我会看/proc/slabinfo里和文件对象相关的 slab 缓存,比如filp、dentry、inode_cache。filp是内核文件对象的高速缓存,它的 active 对象数如果持续增长且不回落,基本可以确定有内核态路径在泄漏文件。这类问题排查起来更麻烦,可能需要排查挂载和驱动模块,但至少 /proc 给了你一个直接观察内核对象数量的入口。

6. 一些容易错过但非常有价值的 /proc 冷门文件

6.1 /proc/[pid]/task:一个进程里每个线程的状况

ps -eLf能看到线程,但不够细。/proc/<pid>/task目录下列出了这个进程的所有线程,每个线程以 TID 命名,目录结构跟进程目录相似。想找出进程中哪个线程在消耗 CPU,可以遍历:

for tid in /proc/<pid>/task/*; do t=${tid#/proc/<pid>/task/} util=$(cat /proc/<pid>/task/$t/stat | awk '{print $14+$15}') echo "$t $util" done

这里的$14和$15是 utime 和 stime,单位是 tick。通过两次采集做差值,就能算出单线程的瞬时 CPU 占用,定位到哪个线程在空转或阻塞。很多 Java 应用线程 dump 不方便看实时状态时,用这个办法从内核侧拿线程数据,准确度甚至比 jstack 更接近真实现象。

6.2 /proc/pressure:内核自带的“压力指数”

/proc/pressure是 cgroup v2 和 PSI(Pressure Stall Information)功能提供的接口。会看到cpu、memory、io三个文件,每个文件里的some表示至少有一个任务在等待资源的时间占比,full表示所有任务都在等待的时间占比,后面是最近 10 秒、60 秒、300 秒、总时长的滚动平均。

我通常用它来做容量规划。比如某个容器some avg10=80%但full avg10=10%,说明有一些任务经常在等 CPU,但整体还没有完全阻塞;如果full长时间超过 50%,那说明这个容器已经跑不动了,加配置或换机型不能解决根本问题,得找一下是不是 IO 模式的瓶颈。这个指标比看负载均值更有说服力,因为它直接告诉你有多少比例的墙内时间在空等。

6.3 /proc/interrupts、/proc/softirqs、/proc/schedstat

这几个文件我在做网络性能调优时必看。/proc/interrupts能看出硬件中断在各个 CPU 核心上的分布;/proc/softirqs能看出软中断的分布,比如网络收包软中断集中在一个核上,就可能成为瓶颈。/proc/schedstat是内核调度器的统计信息,内容比较难读,但里面有cpuN的调度次数和运行时间分布,做 CPU 绑核和调度延迟分析时是底层依据。

初次接触的人建议先从cat /proc/interrupts开始,配合watch -d看变化。你会发现网卡中断几乎绑在固定的 CPU 上,这不是故障,而是驱动和中断亲和性的默认行为。只有当队列数多、CPU 核数也多但中断却集中在一两个核时,才需要主动干预。

6.4 /proc/slabinfo 和 /proc/zoneinfo 什么时候用

/proc/slabinfo是内核 slab 分配器的内部状态,按缓存名列出对象数量、活跃对象、页面数等。排查内核态内存泄漏、文件系统缓存异常增长、驱动模块异常占用时,这个文件是少有的可以直达内核对象级的信息源。

/proc/zoneinfo则是 NUMA 架构下各内存节点的水位线和碎片状态。当系统出现分配失败或者page allocation failure日志时,先看zoneinfo里每个 zone 的low、min、high水位,再看free和nr_free_pages。如果某个 zone 的空闲页低于 low 水位,说明该 NUMA 节点内存已经吃紧,应用在该节点上分配内存会触发回收甚至失败。这类问题在虚拟化环境和多路服务器上尤其常见,理解/proc/zoneinfo能让你在业务感知到异常之前就发现风险。

6.5 kcore、kmsg 这类文件不要乱读

/proc/kcore是内核核心内存的映像,/proc/kmsg是内核日志的原始通道。这两个文件非常特殊:kcore通常大小显示为一个超大数字,因为它表示整个内核虚拟地址空间;用普通工具去读可能直接卡死或者读到无法解释的内容。kmsg如果被多个程序同时读,行为也比较绕,一般还是用dmesg来消费内核日志更合适。

我的建议是:知道它们存在,但不要在日常脚本里碰。它们的价值主要在于专用的 crash 分析工具和安全研究,普通业务场景根本不需要直接读取。对待这种“高危高价值”的 /proc 文件,第一原则是不乱动,第二原则是先查资料再动。

我在实际使用中的体会是,/proc 这套接口的价值不在于某个单独文件,而在于它能帮你把“进程、内存、CPU、IO、内核对象”这几个维度拼起来。很多疑难问题,单看监控图表发现不了,但只要把/proc/<pid>/fd、/proc/meminfo、/proc/loadavg、/proc/slabinfo这几个关键文件在同一时间点抓下来,逻辑链路很快就能理清。建议你也把这几个命令存成一个小工具箱,平时不觉得,等到线上告警出现时,你会感谢自己有这套顺手的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询