Appearance
syscall-trace —— 系统调用追踪实验
混合 3 种 syscall 模式(IO/futex/sleep),用
perf trace实时观察系统调用的类型、频率和耗时,并对比strace的性能开销差异。本文按"原理 → 实验设计 → 代码实现 → 实测数据 → 对比分析 → 结论"递进展开。
1 快速开始
bash
make # 编译
make run # 跑全部模式
make perf-trace # perf trace -s 汇总模式
make perf-trace-strace # 对比 strace -c
make experiment # 一键跑完所有实验(编译+三模式汇总+strace对比),结果写入 experiment-*.log2 原理:perf trace 与 strace 的本质区别
perf trace | strace | |
|---|---|---|
| 底层机制 | perf 事件子系统 / 内核 tracepoint | ptrace 系统调用拦截 |
| 性能开销 | 通常 <5% | 可能 >10x(频繁短 syscall 时) |
| 符号解析 | 自动 DSO/build-id | 需手动匹配 |
| 实时输出 | 需 --no-syscalls 关一些 | 全量实时 |
| 需要 root | 通常不需要(paranoid=1 时) | 同 |
核心原因:strace 每次 syscall 都走 ptrace 暂停-检查-恢复流程,而 perf trace 基于内核 tracepoint 事件,只在 syscall 进入/退出时记录,不暂停进程。下面两张序列图把这条"一字之差"画清楚。
2.1 strace:ptrace 暂停-恢复机制(高开销根源)
strace 通过 ptrace(PTRACE_SYSCALL) 附着进程后,被跟踪进程每一次进入/退出 syscall 都会被内核暂停、发 SIGTRAP 给 tracer,等 strace 读寄存器/返回值后放行。下图的序列清晰呈现:一次 syscall 要被暂停两次(进入一次、退出一次),进程几乎无法连续运行——这正是「性能开销可能 >10x」的微架构层面的直接原因。

2.2 perf trace:tracepoint 零暂停机制(低开销根源)
perf trace 不附着/不暂停进程,而是订阅内核里已有的 syscall tracepoint。进程 syscall 进入/退出时,内核只往每 CPU 环形缓冲区写一条记录,进程立即继续;perf 用户态线程异步读缓冲区做解析。整个过程中进程零等待,所以开销通常 <5%。

2.3 两张图如何对应表格各行
| 表格维度 | strace 图证据 | perf trace 图证据 |
|---|---|---|
| 底层机制 | ptrace 拦截:SIGTRAP + 读寄存器/返回值 | tracepoint 事件:写环形缓冲 |
| 性能开销 | 进出各暂停一次 → >10x | 进程零等待 → <5% |
| 符号解析 | 需 strace 自己匹配寄存器到符号(手动) | 内核经 DSO/build-id 直接解析,自动 |
| 实时输出 | 同步暂停式,天然全量实时 | 异步缓冲,需 --no-syscalls 等开关裁剪噪声 |
| 需要 root | paranoid 限制下二者一致 | 同左 |
一句话:
strace的图里进程被反复"冻住"(ptrace 暂停-恢复),perf trace的图里进程一路畅通只往缓冲区丢记录——这一字之差就是「>10x 开销」与「<5% 开销」的分水岭;其余表格差异(符号解析/实时输出/权限)都是这条主线的延伸。
3 实验设计
本实验要验证第 2 章的核心论断:同样能追踪 syscall,perf trace 比 strace 开销低一个数量级。为此需要先设计"能被两类工具清晰区分"的负载,再用代码把它实现出来。
3.1 实验目标与模式设计
故意混合 3 类 syscall 行为,让一次运行即可覆盖 perf trace 三类典型观测视角:
| 模式 | 命令 | 主要 syscall | 设计意图 / 特点 |
|---|---|---|---|
| IO | ./syscall_trace io | open, read, write, close | 模拟"计算密集 + 频繁短 IO":每次 IO 完成一个读写循环,产生大量 write/read |
| 锁 | ./syscall_trace lock | futex | 4 线程争抢同一 mutex,制造锁争用:产生海量 futex 进出,是 strace 开销放大的重灾区 |
| 睡眠 | ./syscall_trace sleep | nanosleep | 每 10ms 触发一次定时唤醒,观察"低频长间隔"型 syscall 的时间分布 |
| 全部 | ./syscall_trace | 以上所有 | 串联跑上面 3 种模式,每种约 10s,便于横向对比三类 syscall 的频率与耗时画像 |
设计要点
- 为什么混合三种模式:IO 模式考察"高频短 syscall"下追踪工具的开销放大;锁模式把这种放大推到极致(futex 进出极密);睡眠模式作为对照,验证 perf trace 对"低频定时"syscall 同样能抓到耗时。三类合起来,才能同时验证第 2 章讲的"perf trace 低开销"和"strace 高开销"。
- 参数设计:IO 模式循环次数按文件大小固定;锁模式固定 4 线程(多于核数时争用更明显);睡眠模式固定 10ms 间隔。参数都集中在
main.cpp顶部宏,方便复现。 - 可观测目标:用 perf trace 回答三个问题——① 哪类 syscall 调得最多?② 哪种 syscall 总耗时最高?③ 是否有异常长的尾巴延迟(
max)?
3.2 代码设计(main.cpp)
整个程序是一个单文件、三模式、可插拔的结构:顶层 main() 负责解析模式参数并按需调度三个 run_*_mode() 函数,每个模式内部只做"能产生目标 syscall 的最小动作",不掺杂任何与测量无关的逻辑。
整体结构(调用关系)

各模式实现要点与 syscall 映射
| 模式 | 关键代码动作 | 触发的 syscall | 设计取舍 |
|---|---|---|---|
IO (run_io_mode) | 循环 ofstream 写 10 字节 → flush → close;再 ifstream 读回 | open / write / read / close | 每次循环都新建并删除文件(ios::trunc),故意制造短小高频 IO,让 write/read 调用密度最大化——这正是 strace 开销放大的温床 |
锁 (run_lock_mode) | 4 个 std::thread 循环抢同一 std::mutex,临界区只做 counter++ | futex | std::mutex 底层就是 futex;临界区极短 → 线程空转争用 → 海量 futex 进出。用 std::atomic<bool> stop 做无锁退出信号,避免主线程 join 卡死 |
睡眠 (run_sleep_mode) | 循环 std::this_thread::sleep_for(10ms) | nanosleep | 固定 10ms 间隔,产生"低频、定长"型 syscall,作为前两类"高频"的对照,验证 perf trace 对长间隔 syscall 同样能抓到耗时 |
两个值得注意的设计细节
- 退出机制用
atomic<bool>而非条件变量:锁模式里主线程sleep_for(seconds)后把stop置true,worker 循环while(!stop)自然退出。若用condition_variable会引入额外 syscall,污染futex计数;atomic轮询零 syscall,保证测到的futex全部来自mutex争用本身。 - 计时用
steady_clock+seconds参数化:三种模式时长都集中在main()顶部(io_seconds/lock_seconds/sleep_seconds = 10),想拉长观测窗口只改一处即可,便于复现。
编译与运行骨架
bash
make # -O2 -std=c++17 -lpthread
./syscall_trace # 默认 all:三种模式各 10s
./syscall_trace io # 单跑 IO 模式
./syscall_trace lock # 单跑锁模式
./syscall_trace sleep # 单跑睡眠模式一句话:
main.cpp用"一个main+ 三个run_*_mode"的极简骨架,每个模式只保留能触发目标 syscall 的最小动作(IO 用 trunc 刷盘、锁用atomic退出避免污染futex、睡眠用定长sleep_for),让 perf trace 看到的 syscall 画像干净、可解释。
4 实验数据与分析
4.1 采集命令与输出解读
bash
# 汇总:哪个 syscall 调了多少次、总耗时
perf trace -s ./syscall_trace
# 只看 futex(锁争用视角)
perf trace -e futex ./syscall_trace lock
# 只看 write
perf trace -e write ./syscall_trace io
# 排除噪音(忽略 poll/select 等后台 syscall)
perf trace -e '!poll,!select,!nanosleep' ./syscall_trace-s 汇总输出解读
bash
syscall calls errors total avg min max
write 1234 0 123.456 ms 0.100 ms 0.050 ms 5.234 ms
futex 567 0 78.901 ms 0.139 ms 0.002 ms 12.345 mscalls: 调用次数total: 全部调用的总耗时avg: 平均每次耗时max: 最长一次调用的耗时 → 看尾巴延迟
环境说明:以下数据在 x86_64 Linux 实测;
perf trace基于内核 tracepoint,不需要 PMU 硬件计数器,因此在云服务器 KVM 等"全部硬件指标 not supported"的无 vPMU 环境同样能正常出数。
4.2 IO 模式数据
./syscall_trace io,10s,共 265,966 次事件,主导为文件 IO 类 syscall:
| syscall | calls | total (ms) | avg (ms) | max (ms) |
|---|---|---|---|---|
open | 38014 | 8625.669 | 0.227 | 8.026 |
write | 18991 | 157.643 | 0.008 | 0.754 |
close | 37927 | 119.072 | 0.003 | 0.037 |
read | 18993 | 82.372 | 0.004 | 0.024 |
clock_gettime | 18988 | 86.003 | 0.005 | 0.062 |
观察:
open的总耗时占绝对主导(8.6s / 10s),且max达 8.026ms——说明小文件trunc重写时open的系统开销远大于read/write本身。这正是 IO 模式"高频短 syscall"的典型画像:open才是热点,而非直觉中的write。
4.3 锁模式数据
./syscall_trace lock,10s,4 工作线程争用 mutex,futex 洪流 + 环形缓冲溢出:
| 线程 | syscall | calls | total (ms) | avg (ms) | max (ms) |
|---|---|---|---|---|---|
| 主线程 | futex | 4 | 10000.242 | 2500.060 | 10000.183 |
| worker#1 | futex | 1130694 | 5003.609 | 0.004 | 25.014 |
| worker#2 | futex | 1087016 | 5303.501 | 0.005 | 22.118 |
| worker#3 | futex | 1173982 | 5228.016 | 0.004 | 20.560 |
| worker#4 | futex | 1122412 | 4942.769 | 0.004 | 25.033 |
观察:
- 4 线程合计约 451 万次 futex(5.1M 事件量级),但日志中出现大量
LOST events!——perf 每 CPU 环形缓冲被 futex 洪流冲爆,事件被丢弃。- 程序实际完成 328,784,253 次锁保护操作(
counter++),perf 仅采到约 0.14%。这恰好印证第 2 章:这是strace会彻底卡死的重灾区——若用strace,每次 futex 都暂停进程,450 万次暂停足以让程序慢数十倍;而perf trace靠环形缓冲异步记录,虽丢事件但进程几乎不被拖慢。
4.4 睡眠模式数据
./syscall_trace sleep,10s,991 次 nanosleep,极其规律:
| syscall | calls | total (ms) | avg (ms) | max (ms) | stddev |
|---|---|---|---|---|---|
nanosleep | 991 | 9993.568 | 10.084 | 10.134 | 0.00% |
clock_gettime | 993 | 2.421 | 0.002 | 0.012 | 0.76% |
观察:
nanosleep的avg=10.084ms、stddev=0.00%,与代码sleep_for(10ms)完全吻合,说明perf trace对"低频定长"型 syscall 的耗时测量非常精确——低频场景无事件洪流,零丢失。
4.5 与 strace 性能对比
对比命令
bash
# strace 性能开销大
time strace -c ./syscall_trace io # 观察 wall-clock 时间
# perf trace 开销小
time perf trace -s ./syscall_trace io # 对比开销对比表(实测:time 跑 IO 模式,各方式取 real / sys / user)
| 追踪方式 | wall-clock (real) | sys CPU | user CPU | sys 相对基线 |
|---|---|---|---|---|
| 无追踪(基线) | 10.002s | 0.868s | 0.108s | 1x |
perf trace -s | 10.077s | 0.833s | 0.529s | 0.96x |
strace -c | 10.009s | 7.044s | 0.736s | 8.1x |
关键解读——为什么 wall-clock 看起来没变? 本 demo 程序按"墙钟 10s"运行(
while(now < end)),所以无论追踪开销多大,wall-clock 都≈10s,wall-clock 看不出差异。真正拉开差距的是 sys CPU 时间:
strace -c让 sys 从 0.868s 暴涨到 7.044s(≈8.1x),user 从 0.108s → 0.736s(≈6.8x)——单个核几乎被ptrace暂停-恢复流程占满;perf trace -s的 sys 仅 0.833s(≈基线),几乎零额外内核开销(user 略高 0.529s,是用户态解析环形缓冲的代价)。这与第 2 章"strace >10x / perf trace <5%"的结论一致。需说明:若被测程序是"完成固定工作量"(而非固定时长),
strace才会在 wall-clock 上直接放大 10~50x;本程序固定 10s 墙钟,故开销集中体现在 CPU 占用率而非墙钟。这也是一个独立的教学点:用time测定时长型 I/O 程序,要看 sys/user,别只看 real。另:
strace -c也能数出 syscall 次数(open 32598 / close 32566 / write 16283 / read 16285 / clock_gettime 16282,合计 114088),说明它能观测、但代价是上面的 CPU 暴涨;perf trace同样能给出次数与耗时(见 4.2),代价却小一个数量级。
5 结论
把以上原理、设计与实测串起来,得到三条递进结论:
- 机制决定开销:
strace的 ptrace 同步暂停-恢复(每次 syscall 冻结进程两次)与perf trace的 tracepoint 异步写环形缓冲(进程零等待)是数量级差异的根源——图 2.1/2.2 已画清,表 2.3 逐行对应。 - 实测印证原理:
- IO 模式:
perf trace精准定位到open才是 8.6s 热点(非write),证明它能给出可用画像; - 锁模式:451 万次
futex洪流下perf trace仅采 0.14% 却几乎不拖慢进程,而同样负载若交给strace会因 450 万次暂停直接卡死——这是"低开销"最有力的证据; - 睡眠模式:
nanosleep测量 avg=10.084ms、stddev=0.00%,印证低频场景零丢失、极精确。
- IO 模式:
- 定量差距 = 8.1x(sys CPU):同样跑 IO 模式,
strace的 sys CPU 是基线的 8.1 倍,perf trace仅 0.96 倍;且因本程序固定 10s 墙钟,差异体现在 CPU 占用率而非 wall-clock——测定时长型程序要看 sys/user。
适用建议:生产环境、高频 syscall、或任何不想被观测行为拖慢的场景,优先用 perf trace;且它基于 tracepoint、不需 PMU,连云服务器 KVM 无 vPMU 的环境也能直接上手。
一句话:
perf trace是strace的高性能替代品——同样的 syscall 追踪能力,但基于 perf 事件而非 ptrace,sys CPU 开销从 8.1x 降到 0.96x,且无需 PMU 硬件即可使用。
6 关联文档
- tools/code/perf.md——perf 工具主文档,§六.4
perf trace - concepts/tools/perf-demos-architecture.md——perf 12 场景学习架构总纲