psc (ps container) 是一款快速进程扫描工具,利用 eBPF 迭代器与 Google CEL,精确且完整地查询系统状态,并提供完整的容器上下文。
psc 使用 eBPF 迭代器直接从内核数据结构读取进程和文件描述符信息。这种方法具备以下优势:
/proc 无法暴露的内核信息。我计划后续按需提供访问 task_struct 某些字段的高级用法,目前这仍是一个设想/proc 文件系统,提供无法被用户态 rootkit 或 LD_PRELOAD 技巧隐藏的可见性传统的 Linux 工具(如 ps、lsof、ss)功能强大但缺乏灵活性。它们输出固定格式,需要大量通过 grep、awk 和 sed 进行管道处理:
# 传统方式:查找 root 拥有的 nginx 进程
ps aux | grep nginx | grep root | grep -v grep
# psc:直接表达你的意图
psc 'process.name == "nginx" && process.user == "root"'
# 传统方式:查找端口 443 上的已建立连接进程
ss -tnp | grep ESTAB | grep :443 | awk '{print $6}' | cut -d'"' -f2
# psc:一条清晰的表达式
psc 'socket.state == established && socket.dstPort == 443'
psc 使用 通用表达式语言 (CEL) 来过滤进程。CEL 表达式读起来几乎像自然语言,使脚本具有自文档性和可维护性,无需再分析复杂的 grep | awk | sed | xargs 管道。
-o 标志允许直接输出你需要的字段,完全省去后处理步骤:
psc 'socket.state == listen' -o process.name,socket.srcPort
此外,还提供了输出预设,可快速打印常用信息:
psc 'socket.type == tcp && socket.dstPort == 443' -o sockets
传统工具没有容器概念。获取容器信息需要解析 cgroup 路径、查询容器运行时并手动关联 PID:
# 传统方式:查找容器化进程(脆弱且不完整)
ps aux | xargs -I{} sh -c 'cat /proc/{}/cgroup 2>/dev/null | grep -q docker && echo {}'
# psc:原生容器支持
psc 'container.runtime == docker'
psc 自动提取 Docker、containerd、CRI-O 和 Podman 的容器上下文(ID、名称、镜像、运行时、标签)。直接从宿主机调试任意容器的进程、文件和网络连接:
# 显示特定容器中的所有进程
psc 'container.name == "my-app"' --tree
# 查找以 root 运行的容器
psc 'container.runtime == docker && process.user == "root"'
# 列出容器及其镜像
psc 'container.id != ""' -o process.pid,process.name,container.name,container.image
在 Debian/Ubuntu 上:
sudo apt-get install clang llvm libbpf-dev linux-headers-$(uname -r) linux-tools-$(uname -r)
在 Fedora/RHEL 上:
sudo dnf install clang llvm libbpf-devel kernel-devel bpftool
# 生成 vmlinux.h(每个内核版本只需运行一次)
make vmlinux
# 构建二进制文件
make build
或手动构建:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > bpf/vmlinux.h
go generate ./...
go build -o psc
sudo make install
psc 需要 root 权限 来加载 eBPF 程序。
# 列出所有进程
sudo psc
# 以树形结构列出所有进程
sudo psc --tree
将 CEL 表达式作为第一个参数传递以过滤进程:
# 按进程名过滤
psc 'process.name == "nginx"'
# 按用户过滤
psc 'process.user == "root"'
# 按命令行内容过滤
psc 'process.cmdline.contains("--config")'
# 按 PID 范围过滤
psc 'process.pid > 1000 && process.pid < 2000'
# 组合条件
psc 'process.name == "bash" || process.name == "zsh"'
# 仅显示容器化进程
psc 'container.id != ""'
# 按容器运行时过滤(常量:docker, containerd, crio, podman)
psc 'container.runtime == docker'
# 按容器名称过滤
psc 'container.name == "nginx"'
# 按容器镜像过滤
psc 'container.image.contains("nginx:latest")'
# 以树形显示容器进程层级
psc 'container.runtime == docker' --tree
理解进程存在的原因通常需要查看其打开的文件描述符和网络连接:
# 查找有监听 TCP 套接字的进程
psc 'socket.type == tcp && socket.state == listen'
# 查找有已建立连接的进程
psc 'socket.state == established'
# 查找连接到特定端口的进程
psc 'socket.dstPort == 443'
# 查找使用 Unix 套接字的进程
psc 'socket.family == unix'
# 查找在 /etc 下打开文件的进程
psc 'file.path.startsWith("/etc")'
进程字段 (process.X):
name - 进程名称 (string)pid - 进程 ID (int)ppid - 父进程 ID (int)tid - 线程 ID (int)euid - 有效用户 ID (int)ruid - 真实用户 ID (int)suid - 保存的设置用户 ID (int)user - 用户名 (string)cmdline - 完整命令行 (string)state - 进程状态 (uint)能力字段 (process.capabilities.X):
effective - 有效能力位掩码 (uint)permitted - 允许的能力位掩码 (uint)inheritable - 可继承的能力位掩码 (uint)命名空间字段 (process.namespaces.X):
net - 网络命名空间 inode (int)pid - PID 命名空间 inode (int)mnt - 挂载命名空间 inode (int)uts - UTS 命名空间 inode (int)ipc - IPC 命名空间 inode (int)cgroup - Cgroup 命名空间 inode (int)容器字段 (container.X):
id - 容器 ID (string)name - 容器名称 (string)image - 容器镜像 (string)runtime - 容器运行时 (string)labels - 容器标签 (map)文件/套接字段 (file.X 或 socket.X):
path - 文件路径 (string)fd - 文件描述符编号 (int)srcPort - 源端口 (int)dstPort - 目标端口 (int)type - 套接字类型 (tcp, udp)state - 套接字状态(过滤时使用常量,如 listen、established)family - 地址族 (unix, inet, inet6)unixPath - Unix 套接字路径 (string)fdType - FD 类型 (file_type, socket_type)在表达式中使用时无需加引号:
docker、containerd、crio、podmantcp、udpunix、inet、inet6established、listen、syn_sent、syn_recv、fin_wait1、fin_wait2、、、、、注意:输出使用
ss风格的状态名:ESTAB、LISTEN、SYN-SENT等。对于 UDP 套接字,由于 UDP 是无连接的,只显示UNCONN(未连接)或ESTAB(已连接)。
CEL 提供了字符串操作函数:
.contains("子串") - 检查字符串是否包含子串.startsWith("前缀") - 检查字符串是否以指定前缀开头.endsWith("后缀") - 检查字符串是否以指定后缀结尾--tree、-t - 以树形显示进程--no-color - 禁用彩色输出-o、--output - 自定义输出列(逗号分隔的字段名或预设)psc fields - 列出所有可用的 CEL 字段、常量和输出预设psc version - 显示版本信息-o 自定义输出-o 标志允许你精确指定要显示的字段。你可以使用 预设(针对常见用例)或指定单个字段。
预设:
sockets - 进程信息 + 完整的套接字详情(地址族、类型、状态、地址、端口)files - 进程信息 + 文件描述符详情(fd、类型、路径)containers - 进程信息 + 容器详情(名称、镜像、运行时)network - 紧凑的网络视图(pid、名称、类型、状态、端口)# 使用预设
psc 'socket.state == listen' -o sockets
psc 'container.id != ""' -o containers
# 或指定单个字段
psc -o process.pid,process.name,process.user
psc 'socket.state == listen' -o process.pid,process.name,socket.srcPort,socket.state
当输出包含文件/套接字段,并且过滤器匹配到每个进程的多个文件时,每个匹配项会单独占一行:
$ psc 'socket.state == listen' -o network
PID NAME TYPE STATE SRCPORT DSTPORT
1234 nginx tcp LISTEN 80 0
1234 nginx tcp LISTEN 443 0
5678 sshd tcp LISTEN 22 0
查找所有 Web 服务器:
psc 'process.name == "nginx" || process.name == "apache2" || process.name == "httpd"'
查找在特权端口上监听的进程:
psc 'socket.state == listen && socket.srcPort < 1024'
查找位于不同网络命名空间中的进程(适用于容器/Pod 检查):
psc 'process.namespaces.net != 4026531840' -o process.pid,process.name,process.namespaces.net
显示特权进程的能力:
psc 'process.euid == 0' -o process.pid,process.name,process.capabilities.effective,process.capabilities.permitted
查找通过 SUID 二进制文件提升权限的进程(真实 UID 与有效 UID 不同):
psc 'process.ruid != process.euid'
查找与外部服务建立连接的进程:
psc 'socket.state == established && socket.dstPort == 443'
使用自定义列显示网络连接:
psc 'socket.state == established' -o process.pid,process.name,socket.srcPort,socket.dstPort,socket.dstAddr
MIT
time_waitcloseclose_waitlast_ackclosingfile_type、socket_type