当前位置:首页>Linux>Linux 进程、端口、文件句柄排查——ps / ss / lsof / fuser 一篇讲透

Linux 进程、端口、文件句柄排查——ps / ss / lsof / fuser 一篇讲透

  • 2026-10-11 07:11:33
Linux 进程、端口、文件句柄排查——ps / ss / lsof / fuser 一篇讲透

接着 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:

bash
# 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

最常用的过滤组合:

bash
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不可中断睡眠
(卡在 IO 上,连 SIGKILL 都杀不掉)
Z
僵尸进程(已死但父进程没回收)
T
已停止(被 Ctrl+Z 或 SIGSTOP 暂停)
<
高优先级
N
低优先级
s
session leader
+
前台进程组

两个高危状态:

  • D
    :通常是磁盘 / NFS 卡了,杀不掉,得排查 IO
  • Z
    :占着 pid 不占资源,父进程没 wait 回收

3. pgrep / pkill 比 ps | grep 优雅

bash
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 看进程父子关系

bash
pstree -p 1234                 # 看 1234 的子进程树pstree -p | grep nginx         # 看 nginx 工作进程结构

排查「这个进程是谁起的」必备,尤其是孤儿进程(PPID 变成 1,被 init 接管)和 daemon 化的进程。

二、端口相关:ss 完全替代 netstat

注意:netstat 在大多数发行版已经被废弃(不再维护,性能差),新机器用 ss(iproute2 套件的一部分)。

1. 常用命令对照

需求
老命令(netstat)
新命令(ss)
看所有 listen 端口
netstat -lntpss -lntp
看活跃连接
netstat -ntpss -ntp
看 UDP listen
netstat -lunpss -lunp
看连接总数统计
netstat -sss -s
按状态过滤
netstat -nt | grep TIME_WAITss -tan state time-wait

参数解读:-l listen、-n 不解析主机名(快)、-t TCP、-u UDP、-p 显示进程。

bash
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
 或 *:8080
所有网卡
,外网能连
127.0.0.1:8080只本机
,外面 telnet 不通是这个原因
[::]:8080
IPv6 全监听(很多时候等价于 IPv4 0.0.0.0)

新人最常踩的就是把 Spring Boot / Flask 起在了 127.0.0.1,外网怎么调都不通,要么改配置绑 0.0.0.0,要么用 Nginx 反代。

3. 端口被占快速定位

bash
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 堆积排查

bash
ss -tan state time-wait | wc -l    # 数有多少个 TIME_WAIT 连接ss -s                              # 全局统计

TIME_WAIT 堆积通常是「短连接太多」或「客户端没复用」。调优手段:

bash
# 推荐:开 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. 四种典型用法

bash
# 按进程查(这个进程开了哪些文件 / 连接)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 三次握手」:

bash
df -h# /dev/sda1   100G   95G   5G   95%   /du -sh /* 2>/dev/null# 加起来才 40G,剩下 55G 去哪了?

答案是有进程正在写一个被删除的文件——文件名虽然被 rm 删了,但只要进程还持有 fd,inode 就不会回收,磁盘空间也不会释放。

bash
lsof | grep deleted# java  12345  app  9w  REG  8,1  55834574848  12345  /tmp/app.log (deleted)

55GB 凶手定位完毕。三种解法:

  1. 重启占用进程:最暴力,一切干净

  2. 应用支持 reopen:Nginx 风格 kill -USR1 12345,应用会重新打开日志文件

  3. 手动清空 fd(紧急救援):

    bash
    : > /proc/12345/fd/9          # 把那个 fd 截断到 0

    这招相当于把文件 truncate,进程还在用同一个 fd 但内容清空了,能立即释放磁盘。有副作用(进程下次写可能从 0 开始覆盖),仅紧急用。

预防:日志走 logrotate + copytruncate 或应用 reopen,不要 rm 正在被写的日志。

四、fuser:谁占着这个目录 / 文件

lsof 的精简版,专门解决「这个文件 / 目录被谁占着,导致我 umount 不掉」的场景。

bash
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 列字母含义:

  • c
    :当前目录在这里
  • e
    :正在执行(exec)
  • f
    :打开了文件
  • F
    :以写方式打开文件
  • r
    :根目录在这里
  • m
    :mmap 映射

危险招:fuser -k /data 会直接杀掉所有占用进程,加 -i 互动确认:

bash
fuser -ki /data# /data:                root     1234     www      5678# Kill process 1234? (y/N)

五、/proc// 下能看什么

每个进程在 /proc 下都有目录,比 ps 更原始的信息源。

bash
ls /proc/5678/# cmdline  cwd  environ  exe  fd/  limits  maps  status  stack ...

1. 常用文件

文件
内容
例
cmdline
完整命令行(参数用 \0 分隔)
cat cmdline | tr '\0' ' '
cwd
软链到当前工作目录
ls -l cwd
exe
软链到可执行文件
ls -l exe
environ
进程的环境变量(\0 分隔)
cat environ | tr '\0' '\n'
fd/
当前打开的 fd 列表(软链)
ls -l fd/
status
状态汇总(VmRSS、Threads、Uid 等)
grep VmRSS status
limits实际生效
的 ulimit
cat limits
stack
内核态调用栈(排查 D 状态)
cat stack

2. 调试三连

排查问题时常用:

bash
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. 逐层查与改

系统全局:

bash
# 单个进程能开的最大 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):

bash
# 给某用户提到 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 服务):

ini
# /etc/systemd/system/myapp.service[Service]LimitNOFILE=65535
bash
systemctl daemon-reloadsystemctl restart myapp

容器场景:

bash
# Dockerdocker run --ulimit nofile=65535:65535 ...# K8s 通过 securityContext 或 init container(节点配置)

3. 验证真正生效的是多少

不要看 ulimit -n 输出(那是当前 shell 的,不是被排查进程的),要看实际进程的 limits:

bash
cat /proc/<pid>/limits | grep "Max open files"# Max open files    65535    65535

这才是这个进程实际能开多少 fd。然后看它现在开了多少:

bash
ls /proc/<pid>/fd/ | wc -l# 64789                                  # 已经快满了

排查清单:当前打开数接近 limit → 业务真的需要更多(调大),或有 fd 泄漏(看打开的都是什么)。

bash
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

bash
ss -lntp | grep :8080# users:(("docker-proxy",pid=12345,fd=4))

docker-proxy 是 Docker daemon 在主机和容器之间转发端口的中间进程。要找真正的应用:

bash
docker ps | grep -v 8080:               # 找哪个容器映射了 8080docker inspect <container> | grep -A2 Ports

2. 僵尸进程(Z 状态)怎么处理

bash
ps -ef | grep defunct# parent  1234  ...# child   5678  1234 ... [my-app] <defunct>

僵尸本身杀不死(它已经死了),要找父进程让它 wait,或者直接重启父进程:

bash
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 接管:

bash
docker run --init ...

3. D 状态进程杀不掉怎么办

D 状态意味着进程在等内核(通常是 IO),任何信号都打断不了。

bash
cat /proc/<pid>/stack          # 看卡在哪个内核函数dmesg -T | tail# 看有没有磁盘 / NFS 错误

NFS 卡住最常见,解决:

  • 等到 IO 超时返回(NFS 默认无超时,可能等不到)
  • umount -l
     懒卸载挂载点
  • 最终招数:重启机器

4. 信号速查表

信号
编号
含义
应用能否捕获
SIGHUP
1
挂起 / 传统 reload 信号(Nginx 用 HUP 重新读配置)
能
SIGINT
2
Ctrl+C
能
SIGQUIT
3
Ctrl+\,要求带 core dump 退出
能
SIGKILL
9强杀,无法捕获,D 状态除外
否
SIGUSR1
10
用户自定义(Nginx 用 USR1 reopen log)
能
SIGTERM
15
默认 kill 发的,优雅退出
能
SIGSTOP
19
暂停(无法捕获)
否
SIGCHLD
17
子进程退出通知
能

kill <pid> 默认发 SIGTERM;kill -9 <pid> 是 SIGKILL。程序里要捕获 SIGTERM 做优雅关闭(关连接池 / 提交事务 / 等请求结束)。

退出码 = 128 + 信号码:进程被 SIGKILL 杀掉退出码是 137(128+9),被 SIGTERM 杀掉是 143(128+15)。容器报 137 退出码 99% 是 OOM Killer 干的。

写在最后

四个工具加一个文件系统,覆盖 99% 的进程 / 端口 / 句柄类问题:

  1. ss -lntp 替代 netstat
    ,端口被占第一反应;服务起不来 80% 是绑了 127.0.0.1
  2. lsof -p / lsof -i / lsof | grep deleted 是排查神器
    ,尤其磁盘满找不到大文件必用
  3. too many open files 必查三层
    (system / user / process),改 systemd 服务别去改 limits.conf
  4. 真实生效的 ulimit 看 /proc/<pid>/limits
    ,别看当前 shell
  5. SIGKILL 杀不掉 D 状态进程、docker-proxy 会顶替业务进程占端口
    ——这两个坑记住一次少走很多弯路
  6. 退出码 137=128+9 OOM、143=128+15 优雅退出
    ,容器 / K8s 排查常用

这些命令多敲几次就熟了,比死记参数有用得多。

最新文章

随机文章