witr:把“这个进程为什么在跑”讲明白的跨平台归因工具
posts posts 2026-05-16T19:45:00+08:00witr 是一个跨平台进程归因工具。给它进程名、PID、端口、文件或容器,它会追出启动链、主来源、上下文与风险警告,直接回答“这个进程为什么在跑?”技术笔记Go, TUI, CLI, 故障排查, 运维端口被占了,你能查到是谁占的;难的是下一步——它是谁拉起来的、现在归谁管、能不能停。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 --shortsystemd (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 ngMultiple 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" ;;
esac5 单独占一档是有意的。源码注释解释过,把内部错误和“有警告”区分开,脚本才不会把一次采集失败误读成一次风险命中。
警告阈值
警告规则比 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 / 端口 / 文件 / 容器”五种入口的归因,真正不等价的是外围能力:
| 能力 | Linux | macOS | Windows | FreeBSD | 说明 |
|---|---|---|---|---|---|
| 按文件反查持有者 | ✅ | ✅ | ✅ | ✅ | 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 | bashirm https://raw.githubusercontent.com/pranshuparmar/witr/main/install.ps1 | iexUnix 脚本会识别 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@latestwinget 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 --versionREADME 明确提示过 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 的克制体现在它没有把自己做成一个运维平台。它只做一件别人顺手就能做、但没人替你做到底的事:把跨工具的对齐工作前置,让“这个进程为什么在跑”有一个可以直接读完的答案。
这件事在平时只是省几分钟;在故障和应急场景里,人在压力下需要的不是更多字段,而是一条短、清楚、并且坦白自己哪里只是尽力探测的解释链。把这种坦白做成默认输出格式,是这个工具最有辨识度的地方。
参考资料
- GitHub 仓库
- README
- Latest Release
- 浏览器版演练场
- Debian 软件包页面
- Repology 各发行版打包状态
- 项目故事:Why is this running?
- Hacker News 讨论
本文的事实核对基于 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 登录。欢迎补充事实、异议与实践。
讨论暂时无法加载。