首页
关于本站
私人 DNS 加密设置教程
全部文章
推荐
加密 DNS 服务
DNS 实时战报
过滤规则订阅
搜索
网络与 DNS
加密 DNS 实操
路由器与家庭网络
自建服务
AdGuard Home
服务器运维
开发与工具
安全与隐私
登录
搜索
23-4.CN
累计撰写
59
篇文章
累计收到
3
条评论
首页
分类
网络与 DNS
加密 DNS 实操
路由器与家庭网络
自建服务
AdGuard Home
服务器运维
开发与工具
安全与隐私
页面
关于本站
私人 DNS 加密设置教程
全部文章
推荐
加密 DNS 服务
DNS 实时战报
过滤规则订阅
注册丨登录
搜索到
1
篇
标签为
Linux
的文章
2026-10-24
谁在占着这个端口:ss / lsof / /proc 三条路各自的盲区
这篇是服务器运维笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。启动服务报 bind: Address already in use,或者 EADDRINUSE——你要查「谁占了这个端口」。答案通常给三个:ss、lsof、netstat。但它们各自有盲区,而且在特定的情况下会给出「端口没被占用」这种完全错误的结论,让你去改代码。这篇把三条路都走一遍,重点是什么时候它们会骗你。下面所有输出都是我在一台 Ubuntu 20.04 上实际跑出来的。一、先看工具在不在for t in ss netstat lsof fuser; do printf "%-10s " $t command -v $t >/dev/null && echo yes || echo NO done实测这台机器:ss yes netstat NO lsof yes fuser yesnetstat 已经不在默认安装里了(net-tools 包)。这不是意外——ss 是它的替代品,而且更快。所以下面主要讲 ss。二、ss:默认选择,但有三个盲区最常用的一条:ss -tlnp参数含义:-t TCP、-l 只听 LISTEN、-n 不做名字解析、-p 显示进程。实测输出(截前几行):State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 511 0.0.0.0:2096 0.0.0.0:* users:(("nginx",pid=1253931,fd=27),("nginx",pid=1253930,fd=27),...) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1253931,fd=6),...) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=895990,fd=13)) LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=886753,fd=4))注意 0.0.0.0:2096 那一行有五个 nginx pid——master + 4 个 worker 都继承了同一个 fd。这很正常,不是重复占用。盲区 1:-p 需要权限不加 sudo 时,别人进程的 Process 列是空的:LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=886753,fd=4)) ← root 跑的,能看 LISTEN 0 100 0.0.0.0:9000 0.0.0.0:* ← 别的用户跑的,pid 看不到你看到端口被占了,但看不到是谁。先加 sudo 再下结论。盲区 2:UDP 和 TCP 是两个独立命名空间ss -tlnp 只查 TCP。如果你要查的端口是 UDP 的(DNS、QUIC、syslog 都可能是),这条命令会告诉你「没被占用」,而实际上是占着的。# UDP 要单独查 ss -ulnp这是最常见的误判来源:一个服务同时监听 TCP 和 UDP 的 53 端口,你只查了 TCP,以为端口空着,然后 bind UDP 失败。盲区 3:IPv4 和 IPv6 是两个独立 socket(除非开了 dual-stack)ss -tlnp | grep ':443 '可能只返回 0.0.0.0:443(纯 IPv4),而 [::]:443(IPv6)是另一个 socket。如果你 bind 的时候没指定地址族,可能碰到「IPv4 空着但 IPv6 被占」。ss -4 / ss -6 可以分开看:ss -4 -tlnp | grep ':443 ' ss -6 -tlnp | grep ':443 '三、lsof:信息更全,但更慢lsof -iTCP -sTCP:LISTEN -P -n-iTCP 只看 TCP;-sTCP:LISTEN 只看监听状态;-P 不把端口号转成服务名(必须加,否则 443 会显示成 https,grep 不到);-n 不做 IP 反解(不加会很慢,因为要对每个 IP 查 DNS)。实测:COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nginx 2715 root 6u IPv4 38108 0t0 TCP *:80 (LISTEN) nginx 2715 root 7u IPv6 38109 0t0 TCP *:80 (LISTEN) nginx 2715 root 11u IPv4 10248389 0t0 TCP *:8443 (LISTEN)lsof 的优势:能看到同一个进程占的所有端口和文件,而且 IPv4/IPv6 分得清清楚楚(上面 *:80 出现了两次,一次 IPv4 一次 IPv6)。它的劣势:慢。lsof 会遍历 /proc 下所有进程的所有 fd,机器上进程多的时候要几秒。踩坑点:不加 -P 是新手最常见的错误。lsof -iTCP -sTCP:LISTEN -n | grep 443 # ← 可能一条都匹配不到 lsof -iTCP -sTCP:LISTEN -P -n | grep 443 # ← 正确因为默认会把端口 443 显示成 https,你 grep 443 自然匹配不上。四、/proc:不用任何工具,也不受权限限制(部分)这一条最值得学,因为它在容器里、精简系统里、ss 和 lsof 都没有的时候仍然可用。原理:/proc/net/tcp 里记录了所有 TCP socket,其中有一列是 inode。而每个进程的 /proc/PID/fd/* 是符号链接,指向 socket:[inode]。两边一对,就知道谁持有这个 socket。第 1 步:从 /proc/net/tcp 找到端口的 inode/proc/net/tcp 里端口是十六进制的。443 = 0x01BB,80 = 0x0050。换算命令(注意别踩下面的坑):for p in 80 443 853 53; do printf "%-5s -> %s\n" "$p" "$(awk -v p="$p" 'BEGIN{printf "%04X", p}')" done实测:80 -> 0050 443 -> 01BB 853 -> 0355 53 -> 0035踩坑点(我实际踩到的):不要写 printf "%04X" $p。$ printf "%04X" 443 0001 ← 错!sh 的 printf 把 443 当成十进制解析后没有按 %X 处理 $ awk 'BEGIN{printf "%04X", 443}' 01BB ← 对sh(dash)的内置 printf 处理 %X 时行为和 bash 不一致,会把数字截断成奇怪的结果(443 变成 0001,80 变成 0028)。用 awk 算十六进制最稳。反方向验证:$ echo $((0x01BB)) 443 $ echo $((0x0050)) 80第 2 步:反查谁持有这个 inodeinode=$(awk 'NR>1 && $2 ~ /:01BB$/ {print $10; exit}' /proc/net/tcp) echo "inode=$inode"inode=142089然后拿它去反查:for p in /proc/[0-9]*; do for fd in $p/fd/*; do [ -L "$fd" ] && [ "$(readlink $fd)" = "socket:[$inode]" ] && \ echo "持有者: $p $(cat $p/comm 2>/dev/null)" done done 2>/dev/null实测输出:持有者: /proc/1253928 nginx 持有者: /proc/1253929 nginx 持有者: /proc/1253930 nginx 持有者: /proc/1253931 nginx 持有者: /proc/2715 nginx五个 nginx 进程——master 加四个 worker,和 ss 的输出完全对得上。这条路的盲区/proc/PID/fd 里,别人进程的 fd 你读不到(需要 root)。所以非 root 跑这段,只会看到自己的进程。但和 ss -p 不同的是:你能确定「端口确实被占着」(inode 存在),只是不知道是谁。这已经足够区分「端口没被占」和「端口被占但我没权限看」——而这正是排查里最关键的一步。五、还有两种「端口被占」的假象假象 1:TIME_WAIT 让你以为端口还在用ss -tan | grep ':443 ' | awk '{print $1}' | sort | uniq -c如果输出里大量 TIME_WAIT,说明连接刚关闭,内核还在等 2MSL。这通常不影响 bind(除非你用了 SO_REUSEADDR 之外的选项),但会让 ss 的输出看起来很吓人。TIME_WAIT 不是「被占用」,是「等超时」。区别很重要:前者要杀进程,后者等 60 秒自己就没了。假象 2:SO_REUSEPORT 允许多个进程监听同一端口ss -tlnp | grep ':80 '如果看到多个不同的进程名都监听 :80,那不是异常,是用了 SO_REUSEPORT(nginx 多进程、某些负载均衡器会这么干)。内核会把新连接分给其中一个。这时「谁占着端口」这个问题的答案就是「不止一个,它们是设计好的」。六、对照表:什么时候用哪个场景用哪个理由快速看一眼ss -tlnp最快,默认就有看不到 pid加 sudo-p 需要权限查 UDPss -ulnp-tlnp 完全不覆盖 UDP查 IPv6ss -6 -tlnpv4/v6 是独立 socket要完整上下文lsof -iTCP -sTCP:LISTEN -P -n能看进程的所有资源精简系统 / 容器/proc/net/tcp + /proc/*/fd不依赖任何工具只想知道端口通不通nc -z host port不关心是谁,只关心能不能连七、一页排查脚本PORT=443 HEX=$(awk -v p="$PORT" 'BEGIN{printf "%04X", p}') echo "PORT=$PORT HEX=$HEX" echo "=== TCP 监听 ===" ss -tlnp | grep ":$PORT " echo "=== UDP 监听(别漏)===" ss -ulnp | grep ":$PORT " || echo "(UDP 无)" echo "=== IPv4 / IPv6 分开看 ===" echo -n "v4: "; ss -4 -tlnp | grep -c ":$PORT " echo -n "v6: "; ss -6 -tlnp | grep -c ":$PORT " echo "=== /proc 反查(不依赖工具)===" for inode in $(awk -v p=":$HEX" 'NR>1 && index($2,p) {print $10}' /proc/net/tcp); do for pr in /proc/[0-9]*; do for fd in $pr/fd/*; do [ -L "$fd" ] && [ "$(readlink "$fd")" = "socket:[$inode]" ] && \ echo " inode=$inode -> $pr $(cat $pr/comm 2>/dev/null)" done done 2>/dev/null done echo "=== 状态分布(TIME_WAIT 不是占用)===" ss -tan | grep ":$PORT " | awk '{print $1}' | sort | uniq -c实测这台机器对 443 跑完整脚本的输出:PORT=443 HEX=01BB === TCP 监听 === LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1253931,fd=17),...,("nginx",pid=2715,fd=17)) LISTEN 0 511 [::]:443 [::]:* users:(("nginx",pid=1253931,fd=18),...,("nginx",pid=2715,fd=18)) === UDP 监听(别漏)=== (UDP 无) === IPv4 / IPv6 分开看 === v4: 1 v6: 1 === 状态分布 === 43 ESTAB 2 FIN-WAIT-2 2 LISTEN 7 TIME-WAIT注意 v4: 1 和 v6: 1——同一个 443 端口有两个独立的 LISTEN socket(0.0.0.0:443 和 [::]:443)。如果只查 IPv4,你会以为「只有一个」,而实际 bind 失败的可能是 IPv6 那一个。验证做对了没有:ss 和 /proc 两条路给出的进程列表应该完全一致(这里都是那 5 个 nginx pid)。如果不一致,说明你漏了 UDP 或者漏了 IPv6——那才是真正需要继续查的地方。八、一句话总结ss -tlnp 是起点,但它只覆盖 TCP + IPv4 + 你自己有权限看的进程。这三个限制之外的每一种情况,都会让它给你一个「端口空着」的错误答案。加 sudo、加 -u、加 -6,或者退回 /proc 自己查——这四步能覆盖几乎所有场景。最后补一个容易忽略的点:别拿 netstat 的肌肉记忆直接套 ss。两者参数名看着一样(netstat -tulnp / ss -tulnp),但 ss 对权限更敏感,而且参数的语义有细微差别。如果你在网上抄来的命令跑出来是空的,先 ss --help 确认这个参数在你的版本里存在——老版本 ss 遇到不认识的参数时,有些情况是静默返回空结果而不是报错。这又是一个「看起来端口没被占用、其实是工具骗了你」的来源。相关阅读进程被 OOM Killer 杀掉之后:dmesg 怎么读、怎么防止再发生服务器磁盘满了怎么查:df / du / inode / 日志轮转的可执行排查顺序nginx error.log 里那 5 类 warn 到底要不要管
2026年10月24日
2 阅读
0 评论
0 点赞