看清一个进程到底在对你的文件和网络做什么。
在 macOS 和 Linux 上跟踪进程的文件与网络 I/O,实时查看路径、地址、字节数和调用耗时。
iotap 是一个面向 macOS 和 Linux 的命令行工具。给它一个进程 ID 或名称,它就会实时报告该进程的每一次读写:是哪个文件或远程地址,请求了多少字节、实际传了多少,这次调用花了多久,是否失败。当你停止跟踪时,它会按文件和端点汇总整段跟踪记录。
从上往下读,这就是一次 curl 的完整一生:
- 向 93.184.216.34 的 443 端口发送一个 517 字节的请求。
- 请求读取 16 KiB,实际收到 4 KiB。
- 再次请求,却发现还没有数据到达:
EAGAIN。 - 把这 4 KiB 写入
page.html。 - 向终端写入 20 字节的进度信息,然后退出。
常用工具各自只能看到一部分
机器上有东西在忙,你想知道到底在忙什么:某个进程正在狂啃磁盘;刚装的应用在跟一个你根本没让它连的地方通信;构建慢得可疑,你怀疑是 I/O;或者某个程序老是出错,你想看看它究竟栽在哪个文件或哪条连接上。
iotap 把这些碎片拼在一起:字节数、这些字节属于哪个进程,以及去往哪个文件或端点。
| 工具 | 能看到 | 看不到 |
|---|---|---|
| lsof, netstat | 现在打开了什么 | 流经它们的字节数 |
| 活动监视器, top | 多少字节 | 去了哪里 |
| 抓包 | 整台机器的流量 | 属于哪个进程 |
| strace, fs_usage | 进程的每一次调用 | 按文件和端点的汇总 |
| iotap | 你所指定进程的每一次读写,以及字节数、延迟和目标,并按文件和端点汇总 | 内存映射 I/O,以及 Linux 上的 io_uring 和 libaio |
选择要监视的对象
目标可以是进程 ID,也可以是名称。给名称时,凡是进程名、可执行文件名或第一个参数与之相同(不区分大小写)的正在运行的进程,都在跟踪之列;之后以该名称启动的进程也会一并跟踪。
加上 -f,被跟踪进程启动的每一个进程也一并跟踪,无论是开始时就已在运行的,还是之后才启动的。
sudo iotap 1234 # 单个进程
sudo iotap Safari # 所有 Safari 进程,包括新启动的
sudo iotap -f make # make 及其启动的每个进程
sudo iotap -i wlan0 firefox # 只看经过 wlan0 的网络 I/O
sudo iotap -q -d 10 Finder # 10 秒后只输出汇总
一次跟踪,四种读法
事件流就是你在上面看到的那样。跟踪结束时,iotap 会给出汇总。对于运行较久的进程,终端界面会实时显示同样的表格;JSON Lines 可以交给 jq 处理,或存成日志;录制下来的跟踪记录则可以稍后以上述任一形式回放,而且不需要 root。
汇总
按下 Ctrl-C 或进程退出时,iotap 会按文件和端点汇总整段跟踪记录。这里的网络流量走的是 en0,也就是带有 192.168.1.20 地址的那个接口。
终端界面
--tui 显示最近一个完整秒和总计的吞吐量,下面是三个标签页:文件、网络端点,以及最新的 10,000 条事件。图为回放一段下载录制后的 Files 标签页。
- 1 2 3 切换标签页
- s 按字节、读、写、调用次数或最近使用排序
- Enter 查看所选行的详情
- p 暂停画面,跟踪仍在继续
- y 复制所选行的路径或地址
- i 查看各网络接口的流量
- q 退出并打印汇总
JSON Lines
--json 把每个事件写成一行,时间是 Unix 纪元以来的纳秒数,目标是一个小对象。可以管道传给 jq,或存成日志。下面是一条事件,展开成了多行。
sudo iotap --json curl | jq -c 'select(.type == "event")'
录制
--record FILE 保存跟踪记录,--replay FILE 让它经过同一套处理流程,因此无论用哪种输出模式回放,得到的都和实时运行时一模一样。录制文件只含路径和地址,不含传输的数据,并且只能在录制它的那个操作系统上回放。
sudo iotap --net-only --record t.iotaprec 1234
iotap --replay t.iotaprec # 同样的输出,无需 root
它只是读取内核本来就在记录的东西
程序的每一次 read、write、open 和 connect 都是系统调用,而内核可以按要求在每一次调用发生时把它记录下来,并附上发起调用的进程、参数和结果。iotap 读的就是这份记录。
它不挂调试器,不注入代码,也不改动所监视的程序。
- 在 macOS 上
- 这份记录来自内核跟踪机制 kdebug,与 Apple 的
fs_usage读取的是同一来源。它内置于内核,无需安装任何组件。 - 在 Linux 上
- iotap 把自己的一个小型 eBPF 程序挂到内核的系统调用跟踪点上。内核在运行前会校验它,并在 iotap 退出时将其卸载。
- 为什么需要 sudo
- 两者都能看到机器上每一个进程的活动,所以内核只把它们交给 root。没有 root 时,iotap 会直接说明,并在触碰任何东西之前就停下。回放录制不需要 root。
- 只记元数据
- iotap 记录调用、描述符、字节数、延迟、路径和套接字端点。它从不读取或保存正在传输的数据:某个进程读取你的 SSH 密钥时,显示的只是对该文件的一次读取及其大小,绝不会是内容。
- 只看你指定的
- iotap 只要求内核记录你所指定进程的调用,加上
-f时还包括它们启动的进程。其他的一概不记。 - 不留痕迹
- 在 macOS 上,同一时间只能有一个程序占用 kdebug,所以无论以哪种方式退出,iotap 都会释放它,包括出错、收到信号和 panic。在 Linux 上,iotap 一退出,内核就会卸载 eBPF 程序。终端界面退出时同样会恢复终端。
- 一个二进制,没有守护进程
- iotap 只在你运行它时运行,自己不会建立任何连接。可选的主机名解析(
--resolve)走系统的解析器。 - 它看不到的
- 内存映射的文件 I/O,以及内核替进程执行的 I/O,例如页缓存回写。在 Linux 上还有通过 io_uring 或 libaio 提交的 I/O。其余的列在 README 的 Limitations 一节。
用 Homebrew 安装,即可开始跟踪
Homebrew 为 Apple Silicon Mac 和 ARM64、x86-64 Linux 安装预编译的二进制文件,无需安装 Rust 工具链。
还没有 Homebrew?请先安装 Homebrew。运行所需的库由 Homebrew 管理,同时也须满足 Homebrew 自身的操作系统要求。
跟踪需要 sudo。Linux 需要 5.8 或更高版本的内核,并支持 BPF 和系统调用跟踪点。回放已保存的记录不需要 root 权限。
安装 iotap:
brew install rath/tap/iotap
确认已安装的版本:
iotap --version
跟踪一个正在运行的进程(将 1234 换成它的 PID):
sudo iotap --tui 1234
也可以直接下载发布的二进制文件,并用 SHA256SUMS 校验。Linux 下载版本需要 glibc 2.28 或更高版本、libelf 和 zlib,详见运行依赖。
从源码构建(包括 Intel Mac)
源码构建支持 macOS,以及 64 位 Arm 或 x86-64 Linux。
构建需要 Rust 1.98.1,由 rust-toolchain.toml 指定。macOS 上还需要 Xcode Command Line Tools;Linux 上还需要 clang、make、pkg-config、一个 C 编译器,以及 libelf 和 zlib 的开发文件。
在 Debian 和 Ubuntu 上,先安装构建依赖:
sudo apt install build-essential clang pkg-config \
libelf-dev zlib1g-dev
然后构建并跟踪一个正在运行的进程:
git clone https://github.com/rath/iotap.git
cd iotap
cargo build --release --locked
sudo ./target/release/iotap 1234