跳到正文

目录

witr:把“这个进程为什么在跑”讲明白的跨平台归因工具

端口被占了,你能查到是谁占的;难的是下一步——它是谁拉起来的、现在归谁管、能不能停。ps、top、lsof、ss、systemctl、docker ps 各自能摊开一段状态,但因果链得读者自己在脑子里拼。

pranshuparmar/witr 把这段拼装搬进了工具本身。给它进程名、PID(进程号)、端口、文件或容器,它返回一条“这个进程为什么存在”的因果链、一个主来源判断、若干条风险警告,全部在同一个屏幕里。这篇文章按它的实现顺序拆开看:入口怎么归一、主来源怎么择一、警告按什么阈值触发,以及在四个平台上哪些能力其实不等价。

它补的是哪一段

witr 的自我介绍只有一句话:Why is this running? 它不问“有什么在跑”,而是问“这个东西为什么会在”。这两件事在工具链上是分开的——前者是查询,后者需要跨层拼装。

一个进程的成因常常是间接的。PM2 拉起的 Node.js 应用,父进程链会穿过 systemd;容器里的进程,宿主机上看到的直接父进程可能是容器运行时,而不是你敲命令的那个 shell;套接字激活(socket activation)的服务,端口甚至先于进程存在。手工确认要连着用好几个工具,还得记住它们的输出怎么对齐。

witr 的处理方式是不再让读者对齐,而是把结果拼好。它的核心概念是把所有目标先归一成 PID,再从 PID 往上走:

README 把这条路径要回答的问题列成四个:在跑的是什么、它是怎么起来的、是什么在维持它、它属于哪个上下文。前三个是归因,最后一个是定位。

维度内容
项目定位进程归因与溯源,提供 CLI(命令行工具)与 TUI(终端交互界面)两种形态
输入入口进程名、PID、端口、文件、容器
输出因果链、主来源、上下文、警告
支持平台Linux、macOS、Windows、FreeBSD(x86_64 / arm64)
语言 / 许可证Go / Apache-2.0
当前版本v0.3.3(2026-06-24 发布)
仓库关注度GitHub Star 约 2.24 万(2026-09-19)

五种入口,一个归一化目标

位置参数会被当作进程名或服务名处理,默认走子串匹配。

场景命令说明
先按名字摸排witr node子串匹配,容易多命中
避免名字误命中witr nginx -x--exact,只匹配同名进程
已知 PID,直接追根witr -p 14233从别的工具拿到 PID 时最省一步
查端口被谁占witr -o 5000端口 → PID 的反查
查文件被谁持有witr -f /var/lib/dpkg/lock四个平台都支持,Windows 走 Restart Manager
查容器witr -c redis按容器名、镜像、命令或 compose 服务标签匹配
一次查多个目标witr nginx --port 5432 --pid 1234目标参数可重复、可混用,按输入顺序依次输出

--container 这条入口值得单说。它覆盖 Docker、Podman、nerdctl、K8s/crictl、Incus、LXC、LXD 和 FreeBSD jails,加 --verbose 还能把挂载点、网络和 compose 元数据带出来。前提是相应的运行时命令行在 PATH 上——工具不自己实现容器运行时,只做归因。少了这个前提,容器名查不到时很容易被误判成“进程不存在”。

端口这条入口也留了一个回退。当端口归属 PID 1(常见于 systemd 套接字激活或容器运行时托管)时,witr 不会停在“是 init 占的”,而是改走容器侧映射去解释这个端口。这是它比 lsof -i 多迈出的一步。

主来源是择一,不是罗列

Source 字段只给一个结果,不给候选清单。这个取舍看着武断,实际是被现实逼出来的。一个进程往往同时满足好几个来源特征,全列出来等于让读者重新做一遍判断。

更关键的是判定顺序。internal/source/detect.go 里的 Detect() 按固定次序逐个尝试,第一个命中就返回:

container → ssh → shell → systemd → launchd → bsd rc
          → supervisor → cron → windows service → init → unknown

源码里对这件事有明确注释:把平台特有的 init 体系排在通用 supervisor 检测之前,是为了避免误报。顺序本身就是结论的一部分,而且这个设计有具体的受益者。detectSupervisor() 依赖一张 knownSupervisors 名字表,里面既有 pm2、supervisord、gunicorn、runit、tini、docker-init,也收录了 systemd 和 launchd。假若只跑通用检测,systemd 托管的进程只会落进那张表,拿到一个笼统的归类;detectSystemd() 排在它前面,给出的才是单元名、Description 和单元文件路径。容器排在全表最前,为的是让容器化进程不被上游那个恰好还在的 shell 抢走判定。

读 Source 时值得记住它是尽力探测(best effort)。README 列出的可能来源包括:systemd 单元(timer 触发的还会带调度信息)、launchd 服务(带调度与触发方式)、SSH 会话(含远端 IP 和终端)、容器、pm2、cron、交互式 shell(能识别 tmux / screen 会话)、Linux 上的 Snap / Flatpak 沙箱。判不出来时它给 unknown,不会硬凑一个。

一次端口占用会走过哪些步骤

README 里的几段示例输出,串起来正好对应排查过程中会发生的三个动作。第一个动作,端口被占:

witr --port 5000 --short
systemd (pid 1) → PM2 v5.3.1: God (pid 1481580) → python (pid 1482060)

--short 只留祖先链,一眼看出这不是手工启动的服务。链路末端挂在 PM2 下,意味着真正该动的是 PM2,而不是直接 kill 最上面那个 python。

第二个动作是看完整输出的形状。下面这段来自 README 按名字查询的示例,查的是另一个进程,但字段组织方式与上面同源:

Target      : node

Process     : node (pid 14233)
User        : pm2
Command     : node index.js
Started     : 2 days ago (Mon 2025-02-02 11:42:10 +05:30)

Why It Exists :
  systemd (pid 1) → pm2 (pid 5034) → node (pid 14233)

Source      : pm2

Working Dir : /opt/apps/expense-manager
Git Repo    : expense-manager (main)
Sockets     : 127.0.0.1:5001 (TCP | LISTENING)

字段含义都在名字面上,但有三处细节容易被划过:Started 给的是相对时间加绝对时间两种读数,判断“这个进程活了多久”不用自己换算;Git Repo 带分支,能立刻把进程和一份具体代码对上;Sockets 是 地址:端口 (协议 | 状态) 的形式,多个套接字会逐行列出,超出显示上限时以 ... and N more 收尾。

Restarts 这一行只在托管方确实重启过该单元时才出现,取值来自 systemd 的 NRestarts。输出里没有这行不代表没在重启,只代表 systemd 侧没有这个计数。

第三个动作,名字太短命中了一堆:

witr ng
Multiple matching processes found:

[1] nginx (pid 2311)
    nginx -g daemon off;
[2] nginx (pid 24891)
    nginx -g daemon off;
[3] ngrok (pid 14233)
    ngrok http 5000

Re-run with:
  witr --pid <pid>

子串匹配的代价就在这里。它把候选摊开让你挑,而不是自己猜一个;下一步要么换成 --pid,要么加 -x 收紧匹配。

输出:给人看的字段,给脚本用的退出码

输出模式

除了默认输出,还有七种模式:

模式作用
--short只保留祖先链,快速看“谁拉起来的”
--tree树形展示祖先链,附带最多 10 个子进程并高亮目标
--json机器可读,接脚本、CI(持续集成)或告警系统
--env看环境变量,权限受限时只能部分获取
--warnings只看警告,扫异常用
--verbose扩展信息,容器场景会带挂载、网络与 compose 元数据
--no-color关掉着色,重定向到文件或日志时更干净

这几种模式和多目标输入是正交的:--short、--tree、--json、--env、--warnings、--verbose 都能和重复、混用的目标参数一起用。

默认输出的组织原则在 README 里写得很朴素:尽量单屏、顺序确定、叙事式解释、探测不到就明说是尽力结果。四条都是为了压力下能读——出故障时没人有耐心调窗口。

退出码

退出码按 0 到 5 分,这一段决定了它能不能进巡检脚本:

退出码含义
0找到目标,且没有警告
1找到目标,有一个或多个警告
2没找到匹配的进程或服务
3权限不足
4输入无效,或匹配结果有歧义
5内部错误
witr nginx --short
case $? in
  0) echo "All clear" ;;
  1) echo "Warnings detected" ;;
  2) echo "Process not running" ;;
  3) echo "Need elevated privileges" ;;
  4) echo "Invalid input or ambiguous match" ;;
  5) echo "Internal error" ;;
esac

5 单独占一档是有意的。源码注释解释过,把内部错误和“有警告”区分开,脚本才不会把一次采集失败误读成一次风险命中。

警告阈值

警告规则比 README 列出的更细。按 v0.3.3 的实现,触发条件是这些:

  • 服务重启次数大于 5。
  • 进程处于僵尸态(zombie / defunct)或已停止(T 状态)。
  • 累计 CPU 时间超过 2 小时,或 RSS(常驻内存集)超过 1 GB。
  • 监听在公网地址,例如 0.0.0.0 或 ::。
  • 以 root 运行;非 root 进程持有危险 capability 时单独告警,名单是 CAP_SYS_ADMIN、CAP_SYS_PTRACE、CAP_NET_RAW、CAP_DAC_OVERRIDE、CAP_DAC_READ_SEARCH、CAP_FOWNER、CAP_SYS_MODULE、CAP_SYS_RAWIO。
  • 检测不到任何已知的托管方或服务管理器。Windows 上这条被主动关掉——系统在那里会留下过期的父进程号(PPID)而不把孤儿子进程重新挂到 init,链断掉是常态,留着会误伤绝大多数用户进程。
  • 已运行超过 90 天。读不到启动时间时不触发,避免把受保护进程的空白当成“活得很久”。
  • 工作目录是 /、/tmp 或 /var/tmp。
  • 容器明确没有配置 healthcheck(状态未知时不告警)。
  • 服务名与进程名互不包含,比对时会剥掉 .service、.timer 等后缀和 @实例 部分。
  • 可执行文件已被删除,常见于升级后未重启或库注入。
  • 环境变量里出现 LD_PRELOAD 或 DYLD_*。

TUI 是四个标签页

不带参数或加 -i 进入交互式 TUI。它不是把 witr 的查询换个皮,而是把四类目标做成四个可切换的实时视图:

  • Processes:全进程列表,可排序、过滤、搜索,侧栏显示高亮进程的祖先树。
  • Ports:开放与监听端口及其归属进程,按 a 在仅 LISTEN 和全部之间切换。
  • Containers:跨 Docker、Podman、nerdctl、K8s/crictl、Incus、LXC、LXD 与 FreeBSD jails 的容器统一列表,含名称、镜像、状态、端口、命令,单容器详情里能看挂载、网络和 compose 项目元数据。
  • Locks:系统级文件锁。Linux 读 /proc/locks,macOS 与 FreeBSD 由 lsof / fstat 推得;按 a 切到“全部打开的文件”,用 / 在合并结果里搜索。

详情面板往下还能看子进程、环境变量、工作目录、套接字和文件上下文。界面本身还有几处工程上的用心。配色随终端明暗自动适配;列表按自适应节奏刷新,起始 3 秒、负载高时自动退让;也支持鼠标点选和排序。

边界要说清楚。witr 的看家能力是解释与定位,不是修复。但 TUI 里可以直接发 Kill、Terminate、Pause、Resume 信号,也能改 nice 值(进程调度优先级),所以它不是一个只读查看器。这几个动作仅在 Unix 系可用,Windows 上没有。

平台差异集中在这些地方

四个平台都能完成“名称 / PID / 端口 / 文件 / 容器”五种入口的归因,真正不等价的是外围能力:

能力LinuxmacOSWindowsFreeBSD说明
按文件反查持有者✅✅✅✅Windows 走 Restart Manager,内核把结果拷进本进程缓冲区,不读其他进程的内存
TUI 文件锁视图✅✅❌✅与上一行不是一回事,Windows 缺的是这个面板
环境变量读取✅⚠️⚠️✅macOS 受 SIP(系统完整性保护)限制;Windows 读不到受保护进程
服务管理器识别✅✅✅✅systemd / launchd / Windows Services / rc.d
容器来源识别✅✅✅✅需对应运行时 CLI 在 PATH 上
定时调度识别✅✅❌❌Linux 为 systemd timers;macOS 为 launchd 的 interval / calendar
tmux / screen 会话识别✅✅❌✅命中时在来源里带会话名
Snap / Flatpak 沙箱识别✅❌❌❌
危险 capability 告警✅❌❌❌Linux 专属
TUI 进程操作✅✅❌✅

各平台取信息的路子不一样,这直接影响你能指望它读到什么。Linux 走 /proc,信息最全;macOS 用 ps、lsof、sysctl、pgrep 拼装;FreeBSD 用 procstat、ps、lsof。Windows 是唯一直接调 Win32 API(应用程序接口)的,用的是 ToolHelp32、PSAPI 和服务控制管理器,不经过 PowerShell 或 WMI。少了一层兜底,换来的是启动快,也不会卡在 Get-CimInstance 上。

权限要分平台看:

  • Linux / FreeBSD:部分系统目录和进程信息需要更高权限,看不到时先试 sudo witr。
  • macOS:sudo 能解决一部分,但 SIP 保护的系统进程即使加了 sudo 也读不到细节。
  • Windows:要看其他用户或系统服务的详情,必须以管理员身份运行终端。

安装:按你已有的生态选一条

官方脚本最快,顺带把 man page 一起装好:

curl -fsSL https://raw.githubusercontent.com/pranshuparmar/witr/main/install.sh | bash
irm https://raw.githubusercontent.com/pranshuparmar/witr/main/install.ps1 | iex

Unix 脚本会识别 linux / darwin / freebsd 与 amd64 / arm64,默认装到 /usr/local/bin/witr,man page 放到 /usr/local/share/man/man1/witr.1,可用 INSTALL_PREFIX 覆盖;Windows 脚本校验 checksum 后把 witr.exe 放进 %LocalAppData%\witr\bin 并写入用户 PATH。

进入发行版官方源这件事,对运维场景比 star 数更有实际意义:

sudo apt install witr      # Ubuntu 26.04+、Debian sid 及 Kali / Devuan / Raspbian
brew install witr          # macOS 与 Linux
sudo port install witr     # MacPorts
sudo pkg install witr      # FreeBSD

其余常用入口:

conda install -c conda-forge witr    # 也可用 mamba / pixi
npm install -g @pranshuparmar/witr
go install github.com/pranshuparmar/witr/cmd/witr@latest
winget install -e --id PranshuParmar.witr
choco install witr
scoop install main/witr

不想装也能先试手。nix run github:pranshuparmar/witr -- --help 和 pixi exec witr --help 可以直接跑起来。官方另做了一个浏览器演练场(https://pranshuparmar.github.io/witr/),在一台模拟的 Linux 机器上走引导教程和自由练习。评估阶段用它,比拿生产机试更稳妥。AUR、GNU Guix、Aqua 等渠道 README 里都有,就不一一列了。

装完先做一件事就够:

witr --version

README 明确提示过 apt 源里的版本可能落后于 GitHub release。要追最新版本,用官方脚本或 release 页更可靠。

用起来会碰到的几种情况

名字查到多个结果。 子串匹配的默认行为就是会摊开候选。改用 --pid 精确指定,或加 -x 要求同名匹配。

端口归属 PID 1。 这不是 bug,套接字激活和容器运行时托管都会这样。witr 会尝试走容器侧映射;如果仍然只有 init,配合 --verbose 看服务单元细节。

Source 是 unknown。 先确认权限,再确认平台。Windows 上祖先链断在孤儿进程是常见情况,链断不等于配置有问题。macOS 上则要预期部分字段读不到。

--container 查不到。 十有八九是运行时 CLI 不在当前用户的 PATH 上,尤其是通过 sudo 或 CI 环境调用时。

字段比预期少。 对照上面的平台矩阵和权限说明,再决定要不要加 sudo。

脚本里拿到退出码 5。 那是内部错误,不代表被查的进程有风险,别把它并进告警分支。

什么时候值得装,什么时候不必

这几类场景里,它省的时间是实打实的:

  • 端口突然被占,要判断是手工启动、systemd 托管还是容器拉起。
  • 线上有个跑了很久的进程,需要确认它是否还应该存在。
  • 服务反复重启,先搞清楚是谁在负责拉起。
  • 应急响应,需要在几分钟内建立进程上下文。

反过来,这些期望不要给它:

  • 它不做监控,不采集时间序列指标,也不存历史。
  • 它不做性能剖析,给不出 CPU / 内存趋势和热点函数。
  • 它不替代 systemctl、docker、kubectl 这些管理器本身。
  • 它不做修复编排,TUI 的发信号能力止步于单个进程的手动处置。

采用顺序可以按风险从低到高排。先在日常开发机上装一个,用 witr --port 处理几次端口冲突,看它给的解释链对不对你的胃口。确认可信之后,再把它放进运维脚本,用 --json 配退出码做巡检。--env 和 TUI 的进程操作留到最后再开,前者受平台权限约束最多,后者会真的动进程。共享的跳板机上不建议一上来就用 TUI 发信号——witr 会把因果链讲清楚,但决定权还是你的。

回到判断

witr 的克制体现在它没有把自己做成一个运维平台。它只做一件别人顺手就能做、但没人替你做到底的事:把跨工具的对齐工作前置,让“这个进程为什么在跑”有一个可以直接读完的答案。

这件事在平时只是省几分钟;在故障和应急场景里,人在压力下需要的不是更多字段,而是一条短、清楚、并且坦白自己哪里只是尽力探测的解释链。把这种坦白做成默认输出格式,是这个工具最有辨识度的地方。

参考资料

本文的事实核对基于 v0.3.3 的 README 与源码(internal/source/detect.go、internal/output/standard.go、internal/target/、internal/app/app.go),Star 数与版本信息取自 2026-09-19 的 GitHub API。后续升级时,警告阈值、来源判定顺序和平台矩阵最需要重新对照。

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。