查看服务器监听端口及对应进程
Linux 运维脚本:列出当前监听端口,并关联进程的 PID、用户、名称与可执行文件路径。

排查「某个端口被谁占用」「服务到底监听在哪些地址」是日常运维的高频场景。手工组合 netstatlsofps 既慢又容易漏掉 UDP 或 IPv6。

下面是一个可直接保存使用的 Bash 脚本:以 ss 为主数据源,从 /proc 补全进程的 PID、USER、NAME、PATH;对 Docker 发布端口,额外解析 CONTAINER 容器名(docker-proxy 时尤其有用)。

脚本功能

  1. 列出所有监听套接字:TCP(LISTEN)与 UDP(UNCONN),含 IPv4 / IPv6。
  2. 关联进程信息:PID、运行用户、进程名、可执行文件绝对路径(readlink /proc/$pid/exe)。
  3. 关联 Docker 容器:通过 docker inspect 的端口映射,显示宿主机端口对应的 CONTAINER 名称。
  4. FAMILY 列:标明 ipv4 / ipv6;同一 PORT 两行常为 v4、v6 各监听一次。
  5. IFACE 列(网卡):由 ip addr 将绑定 IP 映射到 eth0bond0 等;0.0.0.0 / [::] 显示 all。多网卡分别绑定 192.168.1.10:8010.0.0.5:80 时各占一行并显示不同网卡。
  6. 可选过滤-t / -u / -p-m 合并同协议+端口(默认不合并)。
  7. 排序输出:按端口号、再按 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 inspectNetworkSettings.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 会直接是容器内进程。

常见特殊情况

  1. PID 显示为 -:内核线程、或当前用户无权查看该套接字所属进程(加 sudo)。
  2. 同一 PORT 两行:常为 FAMILY 不同(ipv4 的 0.0.0.0 与 ipv6 的 [::]);或 IFACE 不同(多卡各绑 192.168.x.x / 10.x.x.x)。Docker 的 v4/v6 可能对应两个 docker-proxy PID。
  3. UDP 的 STATE 为 UNCONN:UDP 无连接状态,绑定即显示为 UNCONN,不代表异常。
  4. 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