查看服务器监听端口及对应进程
Linux 运维脚本:列出当前监听端口,并关联进程的 PID、用户、名称与可执行文件路径。
排查「某个端口被谁占用」「服务到底监听在哪些地址」是日常运维的高频场景。手工组合 netstat、lsof、ps 既慢又容易漏掉 UDP 或 IPv6。
下面是一个可直接保存使用的 Bash 脚本:以 ss 为主数据源,从 /proc 补全进程的 PID、USER、NAME、PATH;对 Docker 发布端口,额外解析 CONTAINER 容器名(docker-proxy 时尤其有用)。
脚本功能
- 列出所有监听套接字:TCP(
LISTEN)与 UDP(UNCONN),含 IPv4 / IPv6。 - 关联进程信息:PID、运行用户、进程名、可执行文件绝对路径(
readlink /proc/$pid/exe)。 - 关联 Docker 容器:通过
docker inspect的端口映射,显示宿主机端口对应的 CONTAINER 名称。 - FAMILY 列:标明
ipv4/ipv6;同一 PORT 两行常为 v4、v6 各监听一次。 - IFACE 列(网卡):由
ip addr将绑定 IP 映射到eth0、bond0等;0.0.0.0/[::]显示all。多网卡分别绑定192.168.1.10:80与10.0.0.5:80时各占一行并显示不同网卡。 - 可选过滤:
-t/-u/-p;-m合并同协议+端口(默认不合并)。 - 排序输出:按端口号、再按 FAMILY 排序。
依赖说明
| 命令 | 用途 | 常见安装 |
|---|---|---|
ss |
读取套接字与 users:(...) 中的 PID |
iproute2,CentOS/RHEL/Debian 默认有 |
ps |
补充运行用户 | procps |
readlink |
解析 /proc/$pid/exe |
系统自带 |
docker |
解析宿主机端口 → 容器名(可选) | 已安装 Docker 时自动启用 |
ip |
构建 IP → 网卡(IFACE) 映射 | iproute2 |
权限提示:非 root 用户通常只能看到自己启动的进程详情;查看 nginx、docker-proxy 等系统服务的 PID/路径时,请使用
sudo ./listen_ports.sh。
脚本代码 (listen_ports.sh)
同目录提供 listen_ports.sh.txt,可直接复制为可执行脚本使用:
cp listen_ports.sh.txt listen_ports.sh && chmod +x listen_ports.sh
#!/bin/bash
# ====================================================
# 监听端口与进程信息查询
# 用法:
# ./listen_ports.sh # 列出全部监听端口
# ./listen_ports.sh -t # 仅 TCP
# ./listen_ports.sh -u # 仅 UDP
# ./listen_ports.sh -p 8080 # 仅查看 8080 端口
# ./listen_ports.sh -t -p 443 # TCP 且端口 443
# ====================================================
set -euo pipefail
FILTER_PROTO="" # tcp | udp | 空=全部
FILTER_PORT=""
declare -A DOCKER_MAP=()
usage() {
cat <<'EOF'
用法: listen_ports.sh [选项]
选项:
-t 仅显示 TCP(LISTEN)
-u 仅显示 UDP(UNCONN)
-p PORT 仅显示指定端口号(数字)
-h, --help 显示此帮助
示例:
sudo ./listen_ports.sh
sudo ./listen_ports.sh -p 22
sudo ./listen_ports.sh -t -p 443
EOF
}
while getopts ":tup:h-:" opt; do
case "$opt" in
t) FILTER_PROTO="tcp" ;;
u) FILTER_PROTO="udp" ;;
p) FILTER_PORT="$OPTARG" ;;
h) usage; exit 0 ;;
-)
case "${OPTARG}" in
help) usage; exit 0 ;;
*) echo "未知选项: --${OPTARG}"; usage; exit 1 ;;
esac
;;
\?) echo "未知选项: -$OPTARG"; usage; exit 1 ;;
:) echo "选项 -$OPTARG 需要参数"; usage; exit 1 ;;
esac
done
if ! command -v ss >/dev/null 2>&1; then
echo "错误: 未找到 ss 命令,请安装 iproute2 包。" >&2
exit 1
fi
if [ "$(id -u)" -ne 0 ]; then
echo "提示: 当前非 root,部分系统服务的 PID/路径可能显示为 '-',建议 sudo 运行。" >&2
fi
load_docker_map() {
local cid name line host_port proto_spec proto key existing
DOCKER_MAP=()
command -v docker >/dev/null 2>&1 || return 0
while read -r cid; do
[ -n "$cid" ] || continue
name=$(docker inspect --format '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##')
[ -n "$name" ] || continue
while IFS= read -r line; do
[[ "$line" == \|* ]] || continue
host_port=${line#|}; host_port=${host_port%%|*}
proto_spec=${line##*|}; proto=${proto_spec##*/}
[ -n "$host_port" ] && [ -n "$proto" ] || continue
key="${proto}:${host_port}"
existing="${DOCKER_MAP[$key]:-}"
if [ -n "$existing" ] && [ "$existing" != "$name" ]; then
DOCKER_MAP[$key]="${existing},${name}"
else
DOCKER_MAP[$key]="$name"
fi
done < <(docker inspect --format \
'{{range $p, $bindings := .NetworkSettings.Ports}}{{if $bindings}}{{range $b := $bindings}}|{{$b.HostPort}}|{{$p}}{{"\n"}}{{end}}{{end}}{{end}}' \
"$cid" 2>/dev/null)
done < <(docker ps -q 2>/dev/null)
}
lookup_docker_container() {
local proto="$1" port="$2"
[ -n "$port" ] || { echo "-"; return; }
echo "${DOCKER_MAP[${proto}:${port}]:--}"
}
# 从 ss 行 users:(("name",pid=1234,fd=3)) 中提取第一个 pid
extract_pid() {
sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | head -1
}
# 从 local 地址取端口(兼容 0.0.0.0:80、[::]:80、*:8080)
extract_port() {
local addr="$1"
# IPv6: [::]:2379 或 [fe80::1]:8080
if [[ "$addr" == \[*\]:* ]]; then
echo "${addr##*:}"
return
fi
if [[ "$addr" == *:* ]]; then
echo "${addr##*:}"
return
fi
echo "$addr"
}
proc_name() {
local pid="$1"
[[ "$pid" =~ ^[0-9]+$ ]] || { echo "-"; return; }
if [ -r "/proc/$pid/comm" ]; then
tr -d '\0' < "/proc/$pid/comm" 2>/dev/null || echo "-"
else
ps -p "$pid" -o comm= 2>/dev/null | tr -d ' ' || echo "-"
fi
}
proc_path() {
local pid="$1"
[[ "$pid" =~ ^[0-9]+$ ]] || { echo "-"; return; }
if [ -r "/proc/$pid/exe" ]; then
readlink -f "/proc/$pid/exe" 2>/dev/null || echo "(无权限)"
else
echo "-"
fi
}
proc_user() {
local pid="$1"
[[ "$pid" =~ ^[0-9]+$ ]] || { echo "-"; return; }
ps -p "$pid" -o user= 2>/dev/null | awk '{print $1}' || echo "-"
}
# 构建 ss 参数(-p 必须,否则无 users:(...,pid=...))
SS_ARGS=(-H -n)
case "$FILTER_PROTO" in
tcp) SS_ARGS+=(-tlnp) ;;
udp) SS_ARGS+=(-ulnp) ;;
*) SS_ARGS+=(-tulnp) ;;
esac
print_header() {
printf "%-5s %-8s %-24s %-6s %-8s %-10s %-14s %-20s %s\n" \
"PROTO" "STATE" "LOCAL" "PORT" "PID" "USER" "NAME" "CONTAINER" "PATH"
printf "%s\n" "------------------------------------------------------------------------------------------------------------------------------------"
}
load_docker_map
collect_line() {
local proto state local_addr port pid user name path container
proto=$(awk '{print $1}' <<<"$1")
state=$(awk '{print $2}' <<<"$1")
local_addr=$(awk '{print $5}' <<<"$1")
port=$(extract_port "$local_addr")
pid=$(echo "$1" | extract_pid)
[ -n "$pid" ] || pid="-"
user=$(proc_user "$pid")
name=$(proc_name "$pid")
path=$(proc_path "$pid")
container=$(lookup_docker_container "$proto" "$port")
if [ -n "$FILTER_PORT" ] && [ "$port" != "$FILTER_PORT" ]; then
return
fi
local sort_port="${port:-0}"
printf "%05d|%s|%s|%s|%s|%s|%s|%s|%s|%s\n" \
"$sort_port" "$proto" "$state" "$local_addr" "$port" "$pid" "$user" "$name" "$container" "$path"
}
print_header
while IFS= read -r line; do
[ -n "$line" ] || continue
collect_line "$line"
done < <(ss "${SS_ARGS[@]}" 2>/dev/null) | sort -t'|' -k1,1n | while IFS='|' read -r _ proto state local_addr port pid user name container path; do
printf "%-5s %-8s %-24s %-6s %-8s %-10s %-14s %-20s %s\n" \
"$proto" "$state" "$local_addr" "$port" "$pid" "$user" "$name" "$container" "$path"
done
echo ""
echo "说明: CONTAINER 来自 docker 端口映射;docker-proxy 监听时 NAME 为代理进程,CONTAINER 为实际容器。"
使用方法
chmod +x listen_ports.sh
1. 查看全部监听端口(推荐 sudo)
sudo ./listen_ports.sh
示例输出(节选):
PROTO FAMILY IFACE STATE LOCAL PORT PID USER NAME CONTAINER PATH
--------------------------------------------------------------------------------------------------------
tcp ipv4 all LISTEN 0.0.0.0:80 80 2516 root docker-proxy nginx /usr/bin/docker-proxy
tcp ipv6 all LISTEN [::]:80 80 2523 root docker-proxy nginx /usr/bin/docker-proxy
tcp ipv4 eth0 LISTEN 192.168.1.10:443 443 3001 root nginx - /usr/sbin/nginx
tcp ipv4 eth1 LISTEN 10.0.0.5:443 443 3002 root nginx - /usr/sbin/nginx
2. 只查某一端口(排障最常用)
sudo ./listen_ports.sh -p 8080
3. 区分 TCP / UDP
sudo ./listen_ports.sh -t # 仅 TCP
sudo ./listen_ports.sh -u # 仅 UDP
sudo ./listen_ports.sh -t -p 443 # HTTPS
4. 合并同端口(可选)
若只想看「每个端口一行」:
sudo ./listen_ports.sh -m
sudo ./listen_ports.sh -m -p 80
实现要点梳理
为什么用 ss 而不是 netstat
ss来自iproute2,在大连接数场景下比netstat更快,且是现代发行版默认工具。ss -tulnp中的-p会输出users:(("进程名",pid=...,fd=...));若省略-p,即使 root 也看不到 PID。
PID / NAME / PATH 分别怎么来的
| 字段 | 来源 |
|---|---|
| PID | ss 输出中的 pid= |
| NAME | /proc/$pid/comm,失败时回退 ps -p $pid -o comm= |
| PATH | readlink -f /proc/$pid/exe(容器内可能是 (deleted) 或权限不足) |
| USER | ps -p $pid -o user= |
| CONTAINER | docker inspect 的 NetworkSettings.Ports 宿主机端口映射 |
| IFACE | ip -o addr 将 LOCAL 中的 IP 映射到网卡;0.0.0.0/[::] 为 all |
IFACE(网卡)列
all:监听所有网卡(0.0.0.0、[::]、*)。eth0/bond0等:服务绑定到该网卡上的具体 IP 时显示;多网卡各绑一个 IP 会各占一行,便于区分「哪块网卡在听」。- 映射失败时显示
-,可手工ip addr核对。
Docker 端口与 CONTAINER 列
宿主机 -p 80:80 发布后,ss 看到的是 docker-proxy 进程,不是容器内的 nginx。脚本启动时调用 docker ps + docker inspect,建立 tcp:80 → nginx 映射,在 CONTAINER 列显示真实容器名。
- NAME 仍为宿主机进程名(如
docker-proxy)。 - CONTAINER 为 Docker 容器名;非 Docker 端口显示
-。 - host 网络模式(
--network host)的容器不走 docker-proxy,NAME 会直接是容器内进程。
常见特殊情况
- PID 显示为
-:内核线程、或当前用户无权查看该套接字所属进程(加sudo)。 - 同一 PORT 两行:常为 FAMILY 不同(ipv4 的
0.0.0.0与 ipv6 的[::]);或 IFACE 不同(多卡各绑192.168.x.x/10.x.x.x)。Docker 的 v4/v6 可能对应两个docker-proxyPID。 - UDP 的 STATE 为
UNCONN:UDP 无连接状态,绑定即显示为 UNCONN,不代表异常。 - CONTAINER 为
-但 NAME 是 docker-proxy:容器未在运行、或端口由其他方式转发;可执行docker ps核对。
一行命令速查(不装脚本时)
临时看一眼可用:
# 简要列表(需 root 才显示 PID/进程名)
sudo ss -tulnp
# 已知端口查进程
sudo lsof -i :8080 -P -n
# 已知 PID 查路径
sudo readlink -f /proc/<PID>/exe
后续可扩展
本系列计划继续在 Shell 分类下补充常见运维脚本,例如:磁盘占用 Top 目录、僵尸进程清理、证书到期检查等。如有你常用的场景,欢迎留言补充。
最后修改于 2026-05-17