iotap

看清一个进程到底在对你的文件和网络做什么。

在 macOS 和 Linux 上跟踪进程的文件与网络 I/O,实时查看路径、地址、字节数和调用耗时。

brew install rath/tap/iotap

macOS(Apple Silicon)· Linux(ARM64 / x86_64)

sudo iotap curl
iotap: tracing 4242 (curl); press Ctrl-C to stop
TIME PID OP FD REQUESTED RESULT LATENCY TARGET
14:13:20.004541 4242 sendto 5 517 517 0.041 ms tcp 192.168.1.20:61000 -> 93.184.216.34:443
14:13:20.104625 4242 recvfrom 5 16384 4096 0.041 ms tcp 192.168.1.20:61000 -> 93.184.216.34:443
14:13:20.104708 4242 recvfrom 5 16384 EAGAIN 0.041 ms tcp 192.168.1.20:61000 -> 93.184.216.34:443
14:13:20.104791 4242 write 4 4096 4096 0.041 ms /Users/me/page.html
14:13:20.104875 4242 write 1 20 20 0.041 ms /dev/ttys004
iotap: 4242 (curl) exited

iotap 是一个面向 macOS 和 Linux 的命令行工具。给它一个进程 ID 或名称,它就会实时报告该进程的每一次读写:是哪个文件或远程地址,请求了多少字节、实际传了多少,这次调用花了多久,是否失败。当你停止跟踪时,它会按文件和端点汇总整段跟踪记录。

从上往下读,这就是一次 curl 的完整一生:

  1. 向 93.184.216.34 的 443 端口发送一个 517 字节的请求。
  2. 请求读取 16 KiB,实际收到 4 KiB。
  3. 再次请求,却发现还没有数据到达:EAGAIN。
  4. 把这 4 KiB 写入 page.html。
  5. 向终端写入 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 地址的那个接口。

iotap summary: 4242 (curl) traced for 0.1 s
Files (2 targets)
READ CALLS WRITTEN CALLS FAILED TARGET
0 B 0 4.0 KiB 1 0 /Users/me/page.html
0 B 0 20 B 1 0 /dev/ttys004
Network (1 target)
RECEIVED CALLS SENT CALLS FAILED TARGET
4.0 KiB 2 517 B 1 1 tcp 93.184.216.34:443
Totals
files read 0 B (0 calls), written 4.0 KiB (2 calls)
network received 4.0 KiB (2 calls), sent 517 B (1 call)
en0 received 4.0 KiB (2 calls), sent 517 B (1 call)
5 calls, 1 failed

终端界面

--tui 显示最近一个完整秒和总计的吞吐量,下面是三个标签页:文件、网络端点,以及最新的 10,000 条事件。图为回放一段下载录制后的 Files 标签页。

iotap 4242 curl (exited) 0:00:12
FILE READ FILE WRITTEN NET RECEIVED NET SENT
per second 0 B 1.2 MiB 1.2 MiB 0 B
total 0 B 14.1 MiB 14.1 MiB 517 B
End of the recording. Press q for the summary.
1 Files (2) 2 Network (1) 3 Events (7,205) sort: bytes
READ CALLS WRITTEN CALLS FAILED IDLE TARGET
0 B 0 14.1 MiB 3601 0 <1s /Users/me/big.iso
0 B 0 20 B 1 0 <1s /dev/ttys004
4242 (curl) exited q quit s sort p pause i interfaces r reset ↑↓ select enter details
  • 1 2 3 切换标签页
  • s 按字节、读、写、调用次数或最近使用排序
  • Enter 查看所选行的详情
  • p 暂停画面,跟踪仍在继续
  • y 复制所选行的路径或地址
  • i 查看各网络接口的流量
  • q 退出并打印汇总

JSON Lines

--json 把每个事件写成一行,时间是 Unix 纪元以来的纳秒数,目标是一个小对象。可以管道传给 jq,或存成日志。下面是一条事件,展开成了多行。

{
"type": "event",
"time_ns": 1790259200104708333,
"pid": 4242,
"tid": 2,
"op": "recvfrom",
"dir": "read",
"syscall": "recvfrom",
"fd": 5,
"requested": 16384,
"bytes": null,
"messages": null,
"errno": 35,
"error": "EAGAIN",
"latency_ns": 41666,
"target": {
"kind": "socket",
"proto": "tcp",
"local": "192.168.1.20:61000",
"remote": "93.184.216.34:443"
},
"interface": "en0",
"resolved": "traced"
}
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