Perfetto 系统调用跟踪(System Calls):从 ftrace raw_syscalls 到 Trace Processor 全链路解析
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
在 Linux 和 Android(仅 userdebug 构建)上,Perfetto 通过 ftrace 的raw_syscalls事件记录每次系统调用的进入与退出:在sys_enter时记录系统调用号与原始参数值,在sys_exit时记录返回值;导入阶段 Trace Processor 借助内部系统调用映射表将编号翻译为sys_xxx名称,最终以普通 slice 的形式内联在每线程轨道中。读完本文,你将掌握如何在 TraceConfig 中启用系统调用跟踪、如何在 UI 中查看、如何用 SQL 查询过滤,以及 syscall 编号表是如何通过 extract_linux_syscall_tables 脚本从 Linux 内核源码生成并驱动导入期的(syscall_table.cc)。
能力边界:哪些系统上可以跟踪系统调用
系统调用跟踪不是通用的用户态能力,而是依赖内核 ftrace 的raw_syscalls/sys_enter与raw_syscalls/sys_exit事件,因此有严格的平台前提:
- Linux:需要内核开启 ftrace 且具备相应权限(通常需要 root 或合适的 tracefs 访问能力);
- Android:仅userdebug 构建(或更高权限的构建)可用,普通 user 构建不允许访问 ftrace 原始系统调用事件;
- 这也是 Perfetto 官方将系统调用跟踪定位为"内核侧数据源"的原因——它不是用户态插桩,而是内核事件流的一部分。
从实现上看,Perfetto 对系统调用的处理分为两条链路:
- 采集侧(traced_probes):启用
linux.ftrace数据源,订阅raw_syscalls/sys_enter与raw_syscalls/sys_exit事件,将系统调用号与参数、返回值写入 trace; - 分析侧(Trace Processor):导入时通过系统调用映射表把编号解析为
sys_xxx名称,与普通用户态 slice 一样进入每线程的 slice 栈。
数据模型:sys_enter / sys_exit 事件格式
系统调用事件在内核侧由raw_syscalls追踪点产生,Perfetto 将其定义为两个 proto 消息,见 protos/perfetto/trace/ftrace/raw_syscalls.proto:
message SysEnterFtraceEvent { optional int64 id = 1; // 系统调用号(架构相关) repeated uint64 args = 2; // 系统调用的原始参数值 } message SysExitFtraceEvent { optional int64 id = 1; // 系统调用号 optional int64 ret = 2; // 系统调用返回值(负数通常表示 errno) }关键点在于args是"原始参数值":Perfetto 不会像 strace 那样把每个参数格式化为具名含义(如把 fd 翻译成路径),而是原样记录寄存器中的数值,因此适合做时序分析(什么时候调用了、持续多久、是否返回错误),而不是完整的参数语义分析。
在 Trace Processor 导入期,FtraceParser::ParseSysEnterEvent 与 FtraceParser::ParseSysExitEvent 分别解码这两类事件:
- 进入事件:读取
id得到系统调用号,遍历args生成args[0]、args[1]…… 参数名并绑定为参数,再调用SyscallTracker::Enter; - 退出事件:读取
id与ret,把返回值绑定为参数,再调用SyscallTracker::Exit。
随后 SyscallTracker 通过SyscallNumberToStringId将系统调用号映射为sys_xxx名称,并在该线程的轨道(InternThreadTrack(utid))上执行 slice 的Begin/End,从而让系统调用成为普通 slice 栈中的一员。两个值得注意的细节:
sys_rt_sigreturn不返回,因此被插入为 instant 事件而非一对 Begin/End slice;sys_write在 Android atrace slice 场景下存在嵌套修复逻辑(MaybeTruncateOngoingWriteSlice,参见 ftrace_parser.cc),并会统计truncated_sys_write_duration以处理"trace 开始时写系统调用尚未结束"的边界情况。
系统调用映射表:编号 → 名称的翻译机制
Trace Processor 使用一张内部系统调用映射表把架构相关的系统调用号翻译成名称,目前支持四种架构(文档中写作 x86、x86_64、ArmEabi、aarch32、aarch64,与源码中的命名对应):
| 架构枚举(源码) | 内核 uname machine | 来源表 |
|---|---|---|
kX86_64 | x86_64 | arch/x86/entry/syscalls/syscall_64.tbl |
kX86 | i686 | arch/x86/entry/syscalls/syscall_32.tbl |
kArm32 | armv7l/armv8l | arch/arm/tools/syscall.tbl |
kArm64 | aarch64 | include/uapi/asm-generic/unistd.h |
映射关系定义在 src/kernel_utils/syscall_table.h 与 src/kernel_utils/syscall_table.cc 中:
ArchFromString根据uname的 machine 字符串选择架构(注意armv8l被识别为 32 位用户态的 Arm32);- 在 Linux/Android 上,
FromCurrentArch通过uname()自动选择当前机器对应的表; GetById(id)由系统调用号反查名称,GetByName(name)支持名称反查编号,返回std::nullopt/nullptr表示未知调用;- 每张表的条目上限为
kMaxSyscalls = 550,名称字符串统一内联存放,偏移量用uint16_t索引(超过 0xffff 时会显式报错)。
映射表是自动生成的
这些表不是手写的,而是由 tools/extract_linux_syscall_tables 脚本从 Linux 内核源码(当前指向 v6.7)拉取生成:
- 对 x86_64、x86、arm32 解析
.tbl新格式(parse_tlb); - 对 arm64 解析
asm-generic/unistd.h中的#define __NR_xxx N旧式头文件(parse_def); - 生成结果写入 src/kernel_utils/syscall_table_generated.h,文件头注明"DO NOT EDIT. Auto-generated"。
因此如果内核版本新增了系统调用号,需要重新运行该脚本更新表。SyscallTracker 在导入时会把"架构相关的系统调用号"直接预计算为StringId数组(arch_syscall_to_string_id_),避免每条事件都做两次查表转换,这是典型的空间换时间优化。
在 UI 中查看系统调用
在 Perfetto UI 中,系统调用以sys_xxx命名的 slice内联显示在每线程的 slice 轨道上,与用户态 slice(如Chrome_*、binder等)交错堆叠:
图中可以看到sys_epoll_pwait、sys_recvfrom、sys_ioctl等 slice 与普通用户态 slice 处于同一线程轨道,通过sys_前缀即可肉眼区分。这样无需跳转单独的"系统调用视图",直接沿用 Perfetto 熟悉的轨道时间线交互(缩放、悬停查看参数、点击查看详情)。
用 SQL 查询系统调用 slice
在 SQL 层面,系统调用与任何其他用户态 slice 事件没有任何结构差异——它们同样落在每线程 slice 栈(thread_track对应的轨道)中,只是名称以sys_前缀开头。因此可以直接用slices表查询并按名称过滤:
select ts, dur, t.name as thread, s.name, depth from slices as s left join thread_track as tt on s.track_id = tt.id left join thread as t on tt.utid = t.utid where s.name like 'sys_%'查询结果示例(来自官方文档):
| ts | dur | thread | name |
|---|---|---|---|
| 856325324372751 | 439867648 | s.nexuslauncher | sys_epoll_pwait |
| 856325324376970 | 990 | FpsThrottlerThr | sys_recvfrom |
| 856325324378376 | 2657 | surfaceflinger | sys_ioctl |
| 856325324419574 | 1250 | android.anim.lf | sys_recvfrom |
| 856325324428168 | 27344 | android.anim.lf | sys_ioctl |
| 856325324451345 | 573 | FpsThrottlerThr | sys_getuid |
围绕该查询可以延伸出多种实用分析:
- 按线程聚合系统调用耗时:按
utid/线程名分组,用sum(dur)或avg(dur)定位哪些线程频繁陷入内核、哪些调用耗时最长; - 观察阻塞性调用:
sys_epoll_pwait这类长耗时 slice 往往对应线程的休眠等待,可结合thread_state表判断线程状态(如S/D)与系统调用的对应关系; - 统计调用频率:
count(*)配合ts分桶,观察某段时间内系统调用风暴; - 查看参数与返回值:系统调用 slice 携带
args[0]、args[1]… 以及ret参数(见上文 Parse 逻辑),在 UI 的 slice 详情或args表中可以看到;ret为负时通常对应-errno,可用于发现反复失败的调用(如sys_openat返回-ENOENT)。
由于系统调用 slice 与用户态 slice 共享同一线程轨道,还能利用 span join 把用户态函数(如 binder 调用)与内核态系统调用重叠起来分析"用户态等待内核返回"的耗时构成。
TraceConfig:如何开启系统调用跟踪
开启系统调用跟踪需要启用linux.ftrace数据源,并订阅raw_syscalls/sys_enter与raw_syscalls/sys_exit两个 ftrace 事件:
data_sources: { config { name: "linux.ftrace" ftrace_config { ftrace_events: "raw_syscalls/sys_enter" ftrace_events: "raw_syscalls/sys_exit" } } }进阶:按需裁剪系统调用子集
全量记录系统调用会产生大量事件(高频率的sys_read/sys_write/sys_epoll_wait可能占满 ftrace 环形缓冲区)。如果只想关注特定调用,可在ftrace_config中设置syscall_events子集字段(见 protos/perfetto/config/ftrace/ftrace_config.proto):
data_sources: { config { name: "linux.ftrace" ftrace_config { syscall_events: "sys_read" syscall_events: "sys_open" } } }关于该字段有两个版本行为需要注意:
- Perfetto v43 之前:只设置
syscall_events还不够,配置中必须同时显式包含raw_syscalls/sys_{enter,exit}; - Perfetto v43 及之后:只需设置
syscall_events字段即可,工具会自动处理(该字段在 Android U 引入)。
此外,ftrace_config中还有buffer_size_kb(默认值及 lower-bound 语义见 ftrace_config.proto)等字段可用于调整 ftrace 缓冲区大小,缓解高频率系统调用带来的丢事件风险;若事件被丢弃,Trace Processor 会通过stats表中的相关计数器反映出来。
命令行快速采集示例
使用 Perfetto 命令行工具(Android 上的perfetto或桌面的tracebox/trace_processor配套的采集端)时,可以编写上述 TraceConfig 的 pbtxt 文件,然后执行:
# Android (userdebug 构建) adb shell perfetto -c /data/local/tmp/syscall_trace.pbtxt -o /data/local/tmp/syscall_trace.perfetto-trace adb pull /data/local/tmp/syscall_trace.perfetto-trace # 本地分析 trace_processor --query "select ts, dur, name from slices where name like 'sys_%' limit 10" syscall_trace.perfetto-trace采集端会通过 ftrace 事件订阅把raw_syscalls事件写入 trace 文件,随后即可用 Trace Processor、UI 或本文的 SQL 查询做分析。
深入实现:从事件到 slice 的关键调用链
将上述环节串起来,一条系统调用从内核到 UI/SQL 的完整链路为:
- 内核:
raw_syscalls/sys_enter、raw_syscalls/sys_exit追踪点产生事件(携带id、args、ret); - 采集:
traced_probes的 ftrace 数据源(linux.ftrace)读取并序列化为 SysEnterFtraceEvent / SysExitFtraceEvent 消息; - 导入:FtraceParser 解码事件,提取系统调用号并绑定参数/返回值;
- 翻译:SyscallTracker::Enter/Exit 借助
SyscallTable(syscall_table.cc)把编号翻译为sys_xxx名称,在每线程轨道上执行 slice Begin/End; - 查询:系统调用以
sys_前缀 slice 落入slices表,与用户态 slice 一样可通过 Perfetto SQL 查询,或在 Perfetto UI 中直接查看。
单元测试对这套机制提供了覆盖,例如 src/trace_processor/importers/syscalls/syscall_tracker_unittest.cc 验证了编号→名称翻译、Begin/End 配对等核心行为,可作为理解语义的参考。
常见问题与注意事项
- 为什么 UI 里看不到系统调用?最常见原因是配置中未启用
raw_syscalls/sys_{enter,exit};其次是设备为 Android user 构建(非 userdebug),无权访问该系统调用追踪点; args为什么只是数字?Perfetto 记录的是原始寄存器参数值,不做 strace 式的参数语义化,这是设计取舍——关注时序与频率而非参数含义时开销更低;- 系统调用号是架构相关的:同一数字在 x86_64 与 aarch64 上对应不同调用,Trace Processor 依据导入时的架构信息选择映射表,因此不要跨架构混用对编号的假设;
- 高频率调用可能丢事件:全量
sys_事件带宽较大,建议用syscall_events子集过滤,并按需调大buffer_size_kb; - trace 边界问题:trace 开始时已进入但未退出的系统调用没有对应
sys_enter,导入器通过MaybeTruncateOngoingWriteSlice等逻辑处理,涉及truncated_sys_write_duration统计项。
通过系统调用跟踪,你可以把"线程在等什么"这个经典性能问题落地:结合sys_slice、thread_state与用户态 slice,快速定位线程被阻塞在内核调用的位置与时长,是分析 IO 等待、锁竞争、Binder 调用与网络阻塞的高价值数据源。
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考