接着 W1:Linux 性能排查工具大全 的思路,这次专门聊一类高频问题:「这个进程是谁的子进程」「8080 端口被谁占了」「磁盘满了但 du 找不到大文件」「报 too many open files 但 ulimit 看着不大啊」。
这四个问题日常排查的频率比性能问题还高。底下其实就四个工具:ps / ss / lsof / fuser,再加 /proc 文件系统。这篇把它们的常用姿势 + 各类经典坑都串一遍。
一、进程相关:ps / pgrep / pkill
1. ps aux vs ps -ef
这俩是历史上两套语法(BSD 风格 vs SysV 风格),输出列略有差异,记住一个就行,我自己习惯 ps -ef:
# BSD 风格ps aux# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND# SysV 风格(更结构化)ps -ef# UID PID PPID C STIME TTY TIME CMD
最常用的过滤组合:
ps -ef | grep nginx # 找 nginx 相关进程ps -ef --forest # 树状显示进程层级ps -ef --sort=-%mem | head# 按内存倒序 top 10ps -eo pid,ppid,user,%cpu,%mem,cmd --sort=-%mem | head
2. STAT 字段每个字母什么意思
ps aux 输出里的 STAT 列很多人忽略,其实信息量大:
| |
|---|
R | |
S | |
D | 不可中断睡眠 |
Z | |
T | 已停止(被 Ctrl+Z 或 SIGSTOP 暂停) |
< | |
N | |
s | |
+ | |
两个高危状态:
D:通常是磁盘 / NFS 卡了,杀不掉,得排查 IOZ:占着 pid 不占资源,父进程没 wait 回收
3. pgrep / pkill 比 ps | grep 优雅
pgrep -fl nginx # 列出含 nginx 的进程,-f 匹配完整命令行,-l 显示命令pgrep -u www # 按用户筛pkill -f "java.*MyApp"# 按命令行正则杀pkill -USR1 nginx # 发指定信号
pkill 比 ps | grep | awk | xargs kill 这种串联干净一万倍,且不会误杀同名命令(grep 本身的进程)。
4. pstree 看进程父子关系
pstree -p 1234 # 看 1234 的子进程树pstree -p | grep nginx # 看 nginx 工作进程结构
排查「这个进程是谁起的」必备,尤其是孤儿进程(PPID 变成 1,被 init 接管)和 daemon 化的进程。
二、端口相关:ss 完全替代 netstat
注意:netstat 在大多数发行版已经被废弃(不再维护,性能差),新机器用 ss(iproute2 套件的一部分)。
1. 常用命令对照
| | |
|---|
| netstat -lntp | ss -lntp |
| netstat -ntp | ss -ntp |
| netstat -lunp | ss -lunp |
| netstat -s | ss -s |
| netstat -nt | grep TIME_WAIT | ss -tan state time-wait |
参数解读:-l listen、-n 不解析主机名(快)、-t TCP、-u UDP、-p 显示进程。
ss -lntp# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process# LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))# LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("java",pid=5678,fd=12))
最后一列 users: 段就是占用进程。
2. 端口绑定地址的玄机
看 Local Address,有三种典型:
| |
|---|
0.0.0.0:8080 | 所有网卡 |
127.0.0.1:8080 | 只本机 |
[::]:8080 | IPv6 全监听(很多时候等价于 IPv4 0.0.0.0) |
新人最常踩的就是把 Spring Boot / Flask 起在了 127.0.0.1,外网怎么调都不通,要么改配置绑 0.0.0.0,要么用 Nginx 反代。
3. 端口被占快速定位
ss -lntp | grep :8080# LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=5678,fd=12))ps -p 5678 # 看具体是什么命令ls -l /proc/5678/cwd # 看工作目录cat /proc/5678/cmdline | tr'\0'' '# 看完整启动命令行
4. TIME_WAIT 堆积排查
ss -tan state time-wait | wc -l # 数有多少个 TIME_WAIT 连接ss -s # 全局统计
TIME_WAIT 堆积通常是「短连接太多」或「客户端没复用」。调优手段:
# 推荐:开 tcp_tw_reuse(允许复用 TIME_WAIT 的端口给新连接)sysctl -w net.ipv4.tcp_tw_reuse=1
注意:net.ipv4.tcp_tw_recycle 在 4.12 内核已被删除,老博客让你开 tcp_tw_recycle 的,过时了别照抄——它和 NAT 网络冲突,会引发隐蔽的连接失败。
三、lsof:万能工具
lsof 全称 “list open files”,但 Linux 里"一切皆文件"——socket、管道、设备、目录、共享内存都是文件,所以它能查的东西远不止文件。
1. 四种典型用法
# 按进程查(这个进程开了哪些文件 / 连接)lsof -p 5678# 按文件查(谁开了这个文件)lsof /var/log/nginx/access.log# 按端口查lsof -i :8080lsof -i tcp:80 # 限 TCPlsof -iTCP -sTCP:LISTEN # 所有 TCP LISTEN# 按用户查lsof -u nginx
输出列:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAMEnginx 5678 nginx 6u IPv4 12345 0t0 TCP *:80 (LISTEN)nginx 5678 nginx 8w REG 8,1 12345678 9876 /var/log/nginx/access.log
FD 列读法:
- 数字 +
u/r/w:fd 编号 + 打开模式(u 读写 / r 只读 / w 只写) - 特殊值:
cwd 当前目录、rtd 根目录、txt 可执行文件、mem 内存映射文件
2. 经典场景:删了文件磁盘没释放
这个故事在面试里出现频率仅次于「TCP 三次握手」:
df -h# /dev/sda1 100G 95G 5G 95% /du -sh /* 2>/dev/null# 加起来才 40G,剩下 55G 去哪了?
答案是有进程正在写一个被删除的文件——文件名虽然被 rm 删了,但只要进程还持有 fd,inode 就不会回收,磁盘空间也不会释放。
lsof | grep deleted# java 12345 app 9w REG 8,1 55834574848 12345 /tmp/app.log (deleted)
55GB 凶手定位完毕。三种解法:
重启占用进程:最暴力,一切干净
应用支持 reopen:Nginx 风格 kill -USR1 12345,应用会重新打开日志文件
手动清空 fd(紧急救援):
: > /proc/12345/fd/9 # 把那个 fd 截断到 0
这招相当于把文件 truncate,进程还在用同一个 fd 但内容清空了,能立即释放磁盘。有副作用(进程下次写可能从 0 开始覆盖),仅紧急用。
预防:日志走 logrotate + copytruncate 或应用 reopen,不要 rm 正在被写的日志。
四、fuser:谁占着这个目录 / 文件
lsof 的精简版,专门解决「这个文件 / 目录被谁占着,导致我 umount 不掉」的场景。
fuser -mv /data# USER PID ACCESS COMMAND# /data: root 1234 ..c.. bash# www 5678 F.... nginxumount /data# umount: /data: target is busy# fuser -mv 找凶手 → 杀掉或让它退出 → 再 umount
ACCESS 列字母含义:
危险招:fuser -k /data 会直接杀掉所有占用进程,加 -i 互动确认:
fuser -ki /data# /data: root 1234 www 5678# Kill process 1234? (y/N)
五、/proc// 下能看什么
每个进程在 /proc 下都有目录,比 ps 更原始的信息源。
ls /proc/5678/# cmdline cwd environ exe fd/ limits maps status stack ...
1. 常用文件
| | |
|---|
cmdline | | cat cmdline | tr '\0' ' ' |
cwd | | ls -l cwd |
exe | | ls -l exe |
environ | | cat environ | tr '\0' '\n' |
fd/ | | ls -l fd/ |
status | 状态汇总(VmRSS、Threads、Uid 等) | grep VmRSS status |
limits | 实际生效 | cat limits |
stack | | cat stack |
2. 调试三连
排查问题时常用:
PID=5678echo"=== Cmdline ==="cat /proc/$PID/cmdline | tr'\0'' '; echoecho"=== CWD ==="ls -l /proc/$PID/cwdecho"=== Open FDs ==="ls /proc/$PID/fd/ | wc -lecho"=== Limits ==="grep "Max open files" /proc/$PID/limitsecho"=== Memory ==="grep -E "VmRSS|VmSize" /proc/$PID/status
六、too many open files:三层限制必须懂
这是文件句柄类问题里最高频的。报错之后第一反应是「ulimit 加大」,但常常加了不生效,原因是没分清三层限制。
1. 三层限制
内核全局上限(fs.nr_open / fs.file-max) ↓用户级限制(/etc/security/limits.conf) ↓进程级限制(fork 时继承 / systemd unit / Docker ulimit)
真实生效的是这三层里的最小值,少一层不查都可能找错原因。
2. 逐层查与改
系统全局:
# 单个进程能开的最大 fd 数(硬上限)cat /proc/sys/fs/nr_open# 1048576# 全系统所有进程能开的 fd 总和cat /proc/sys/fs/file-max# 9223372036854775807 8.0 以来基本是无穷大# 临时改sysctl -w fs.nr_open=1048576# 永久改:写 /etc/sysctl.conf
用户级(/etc/security/limits.conf):
# 给某用户提到 65535echo"app soft nofile 65535" >> /etc/security/limits.confecho"app soft nofile 65535" >> /etc/security/limits.confecho"app hard nofile 65535" >> /etc/security/limits.conf# 重新登录该用户生效ulimit -n # 看当前 shell 的 soft limitulimit -Hn # 看硬 limit
注意 PAM:/etc/security/limits.conf 只在通过 PAM 登录时生效(ssh / login),cron / systemd 启动的进程不读这里。
进程级(systemd 服务):
# /etc/systemd/system/myapp.service[Service]LimitNOFILE=65535
systemctl daemon-reloadsystemctl restart myapp
容器场景:
# Dockerdocker run --ulimit nofile=65535:65535 ...# K8s 通过 securityContext 或 init container(节点配置)
3. 验证真正生效的是多少
不要看 ulimit -n 输出(那是当前 shell 的,不是被排查进程的),要看实际进程的 limits:
cat /proc/<pid>/limits | grep "Max open files"# Max open files 65535 65535
这才是这个进程实际能开多少 fd。然后看它现在开了多少:
ls /proc/<pid>/fd/ | wc -l# 64789 # 已经快满了
排查清单:当前打开数接近 limit → 业务真的需要更多(调大),或有 fd 泄漏(看打开的都是什么)。
ls -l /proc/<pid>/fd/ | awk '{print $11}' | sort | uniq -c | sort -rn | head# 53000 socket:[xxx] ← socket 泄漏(连接没关)# 200 /var/log/...
七、其它常见疑难场景
1. 端口被占凶手没在 ps 里:docker-proxy
ss -lntp | grep :8080# users:(("docker-proxy",pid=12345,fd=4))
docker-proxy 是 Docker daemon 在主机和容器之间转发端口的中间进程。要找真正的应用:
docker ps | grep -v 8080: # 找哪个容器映射了 8080docker inspect <container> | grep -A2 Ports
2. 僵尸进程(Z 状态)怎么处理
ps -ef | grep defunct# parent 1234 ...# child 5678 1234 ... [my-app] <defunct>
僵尸本身杀不死(它已经死了),要找父进程让它 wait,或者直接重启父进程:
ps -o ppid= -p 5678 # 找父 pid# kill -CHLD 1234 # 仅当父进程注册了 SIGCHLD handler 才会触发 wait,多数情况无效# 最可靠的做法:重启父进程systemctl restart <parent-service>
注意:SIGCHLD 本来就是子进程退出时由内核自动发的,父进程没写 wait() / waitpid() 才出现僵尸。手动 kill -CHLD 在父进程没注册处理器时没用。所以重启父进程才是稳妥兜底。
容器场景下,PID 1 常常是没写 reaper 的应用,建议用 --init 或 tini 接管:
3. D 状态进程杀不掉怎么办
D 状态意味着进程在等内核(通常是 IO),任何信号都打断不了。
cat /proc/<pid>/stack # 看卡在哪个内核函数dmesg -T | tail# 看有没有磁盘 / NFS 错误
NFS 卡住最常见,解决:
- 等到 IO 超时返回(NFS 默认无超时,可能等不到)
umount -l
4. 信号速查表
| | | |
|---|
| | 挂起 / 传统 reload 信号(Nginx 用 HUP 重新读配置) | |
| | | |
| | | |
| 9 | 强杀,无法捕获,D 状态除外 | |
| | 用户自定义(Nginx 用 USR1 reopen log) | |
| 15 | | |
| | | |
| | | |
kill <pid> 默认发 SIGTERM;kill -9 <pid> 是 SIGKILL。程序里要捕获 SIGTERM 做优雅关闭(关连接池 / 提交事务 / 等请求结束)。
退出码 = 128 + 信号码:进程被 SIGKILL 杀掉退出码是 137(128+9),被 SIGTERM 杀掉是 143(128+15)。容器报 137 退出码 99% 是 OOM Killer 干的。
写在最后
四个工具加一个文件系统,覆盖 99% 的进程 / 端口 / 句柄类问题:
ss -lntp 替代 netstat,端口被占第一反应;服务起不来 80% 是绑了 127.0.0.1lsof -p / lsof -i / lsof | grep deleted 是排查神器- too many open files 必查三层(system / user / process),改 systemd 服务别去改 limits.conf
- 真实生效的 ulimit 看
/proc/<pid>/limits SIGKILL 杀不掉 D 状态进程、docker-proxy 会顶替业务进程占端口- 退出码 137=128+9 OOM、143=128+15 优雅退出
这些命令多敲几次就熟了,比死记参数有用得多。