Perfetto工具集之traced_perf
1.Perf工具概览
linux中包含了众多性能分析工具,perf(特指linux-tools perf)工具是2009年在linux内核2.6.31中引入的一个工具。它的主要功能是可以跟踪hardware performance counter(PMU)、tracepoints、software performance counter(hrtimer)、dynamic probes等信息。linux内核将这些信息进行封装,通过syscall(perf_event_open等)的形式提供,使之抽象为events的概念,可以供userspace的进程使用。perf作为一个linux下的命令行工具,可以读取这些events,并结合性能分析的场景,提供了诸如stats、top、record、report等子工具命令,适配更细化的分析需求。
Android中一般使用的并不是老牌的linux-tools perf工具,而是使用经过Android客制化的perf工具,用于支持Android中拓展的一些feature。
simpleperf:
Android最早于Android 6.0(2015年)中引入,距今(2022年)已经有7年的历史。其主要作用就是实现Linux中perf工具的基本功能。
traced_perf:
Google于2019[在2019年开始开发][修改了一下]年开始开发,其作为perfetto的一个consumer而不是单独的一个项目去开发的。其开发目的是能够:
a.利用perfetto的成熟平台,提供profiling、unwinding、UI等各方面的能力
b.伴随着Android权限管控的愈发严格和MAC的要求,原Simpleperf的独立selinux domain完成所有功能的方式已经无法满足sandbox的需求,需要进行严格的domain隔离
本文着重讲一下traced_perf。
2.traced_perf的结构
2.1.代码结构
traced_perf的代码位于AOSP的external/perfetto/src/profiling/perf/ 目录下,可以看出,traced_perf的代码实际上是perfetto项目的一个子目录。
此目录下的代码如下图:

可以看出代码分为三类:
编译脚本相关: BUILD.gn
单元测试相关:X_unittest.cc
主要代码逻辑
除了上述的代码目录外,在perfetto的主目录下还存在文件:external/perfetto/traced_perf.rc
此文件是traced_perf可执行文件的启动脚本。
2.2运行时结构
根据external/perfetto/Android.bp的编译脚本可以看出,traced_perf最终会被编译为一个可执行文件,并且被install到/system/bin/traced_perf。此可执行文件以daemon的形式存在,其启动和结束受它对应的rc启动脚本的控制。


其运行时的生命周期可以通过traced_perf.rc文件去分析:


traced_perf的权限配置
▫traced_perf的用户设置为了nobody,可以确保权限不会影响到其他用户,避免被恶意破解后获得提权
▫traced_perf的组包含nobody、readproc、readtracefs三个,readproc是为了使之被赋予可以读取/proc/PID目录的权限,readtracefs是为了赋予其读取tracefs mount的目录的权限,这两个权限是traced_perf能够正常运行所必须的权限。
▫traced_perf赋予了相应的capability,分别为KILL,DAC_READ_SEARCH。KILL是为了使traced_perf能够给其他进程发送信号。DAC_READ_SEARCH是为了使之能够至少能够获取一些文件的权限,而不至于甚至不能够探测某些文件的存在。这两个权限都是traced_perf正常运行所必需的。
▫task_profiles是为了给traced_perf设置为高capacity(unwinding)的一类cgroup,从而使得调度器可以给予其更合理的资源分配。
traced_perf的资源
traced_perf申请了一个名为traced_perf的unix_socket,此unix_socket是traced_perf与待profiling的进程间通信的通道,后续章节有涉及。
traced_perf的生命周期管控
traced_perf的生命周期管控通过property trigger来完成。当设置persist.traced_perf.enable 为true的时候,会自动启动traced_perf。同时,它还会受到sys.init.perf_lsm_hooks和traced.lazy.traced_perf的管控。
3.traced_perf的架构
3.1perfetto的框架
traced_perf作为perfetto工具集的一个组成部分,其遵循perfetto的service model的。perfetto的service model如下图所示:

3.1.1producer
traced_perf作为Tracing service的producer,其和tracing service的交互由两条通道,分别是IPC channel和shared_memory,其中IPC channel为unix socket,后面有详细描述。
shared_memory是指与tracing service之间建立的共享内存通道,此共享内存通道有两个作用:
1.进行高效的进程间数据传递,这里传递的主要是结构化的采样点数据。
2.与控制流进行隔离,避免被恶意破解后造成安全隐私风险。
traced_perf本身作为producer端,提供了Data Source,每个producer可以提供多种Data Source。traced_perf本身对外提供的Data Source包含linux.perf 和一个metadata的Data Source。后续我们对此Data Souce展开详细的描述。
3.1.2Tracing Service
Tracing Service作为perfetto在手机端的核心服务,其承担了主控的作用。Tracing Service在手机端主要表现为traced进程,它一方面接收consumer的配置文件的控制,另外一方面将配置文件转化为对Producer的控制;同时还承担了producer端与consumer端的桥梁。producer端与consumer的数据通道采用了trace buffer内存,这部分内存是没有进行进程间共享的,从而可以保持数据的隔离。

3.1.3Consumer
consumer端是指对perfetto trace类数据的消耗端,比如perfetto ui、shell command、traceur等,consumer端还可以进行自定义,在Android中添加客制化的consumer,从而对Data Source进行客制化的处理。
consumer端和Tracing Service的IPC通道主要也是通过unix socket进行连接的。
3.2traced_perf与perfetto的交互
3.2.1整体流程
整体流程示意图如下,读者可以根据此流程理解。交互流程涉及到Perfetto内部的众多类的实现,建议读者优先理解涉及到的C++类的声明与函数实现,而不要刚开始就陷入到调用流程的跟踪中,避免陷入到多层嵌套的复杂逻辑中。当把几个关键类的功能和对外关系理清楚之后,再通过调用关系依次跟踪调用流程。

PerfProducer:
调用ConnectService建立连接流程
实现OnConnect流程
实现OnTracingSetup、StartDataSource等函数
ProducerEndPoint
创建建立进程间通信的必要对象
实现OnConnect
ClientImpl
建立Socket连接
实现onConnect、onDataAvaliable等
通过上述流程拆分可以看出,每个类的职责都是非常清晰的。
3.2.2IPC通道建立
对于traced_perf来讲,建立IPC通道由下面几个重要流程:
1.实例化task_runner和AndroidRemoteDescriptorGetter。task_runner是traced_perf中使用的一个Looper工具类实例,AndroidRemoteDescriptorGetter是traced_perf为了获取想要trace的应用的私有进程数据而建立的一个类。后续章节有相关描述。
2.与Tracing Service建立连接
3.启动消息循环

3.2.3IPC通道框架
IPC通道的框架相对来说比较复杂,本小节进行一个原理剖析。
TaskRunner: 是一个Looper interface,PerfProducer使用的实例是基于unix domain socket实例化的TaskRunner。此task_runner_在各个结构间传递,承担了各类消息的派发和处理。
ProducerEndPoint: 是Tracing service的producer端的接口类,通过ProducerIPCClientImpl得以实例化。
为了能够将PerfProducer类注册为Tracing Service的producer,需要执行如下操作:

其中,ProducerIPCClient::Connect是一个静态方法,其实例化了ProducerIPCClientImpl并将其以unique_ptr的形式返回。


上述流程走完之后,实际上就建立了PerfProducer的事件处理流程。
关注到ConnectService中,ProducerIPCClient::Connect中的第二个参数是this指针,实际上是把PerfProducer的对象指针传递给了ProducerEndpoint对象,它是通过ProducerIPCClientImpl构造函数中的第三个参数producer传递过去的。
3.2.3.1相关概念
DataSource
顾名思义,这个是数据源的意思。根据Perfetto的框架图,consumer端需要指明从哪个“数据源”收集数据,而Producer可以提供数据源。数据源在perfetto中的定义以proto的形式进行了规定,在PerfProducer中,它对数据源的定义进行了抽象,通过DataSourceState进行描述。





与DataSource相对应的一个数据结构是traced_perf里面的DataSourceState的结构,可以看到DataSourceState中维护了一个TraceWriter指针,此TraceWriter提供了写入Trace数据的相关方法。

3.2.3.2TraceWriter
TraceWriter类是为了让使用者可以在perfetto的共享内存中,以零拷贝的形式写入Trace数据,方便使用者高效写入Trace数据。
NewTracePacket
创建一个TracePacket并返回一个handle
FinishTracePacket
完成之前创建的TracePacket
Flush
将TracePacket刷入到service端
3.2.3.3IPC消息的接收
ProducerEndPoint对象会通过PerfProducer对象提供的service_sock_name与PerfProducer进行通信,当连接建立之后,就进入了IPC流程,服务端会将消息按照perfetto定义的协议格式发送对应的指令。消息协议如下:


上述消息会被ProducerEndPoint解析,并最终转化为Producer接口类的虚函数调用(注意到ProducerEndPoint维护了一个Producer(PerfProducer)实例的指针)。
Producer的实例需要实现如下接口:
OnConnect
当与Tracing Service建立Socket连接后会被调用
OnDisconnect
当与Tracing Service断开Socket连接后会被调用。此时可以销毁PerfProducer对象了。
OnStartupTracingSetup
当第一个DataSource被创建之前被调用,可以做一些初始化的工作。
SetupDataSource
设置DataSource时被调用,其传递的参数包含DataSourceInstanceId以及DataSourceConfig
StartDataSource
启动DataSource
StopDataSource
停止DataSource
Flush
Tracing Service要求Producer将数据写入到共享内存中。
ClearIncrementalState
Producer端应该在此调用后,停止引用之前写入到共享内存的数据。
3.2.3.4IPC消息的发送
ProducerEndPoint提供如下接口:
Disconnect:
用于与ProducerEndPoint断开连接,此时不再能收到来自于Service端的回调消息。
RegisterDataSource
注册DataSource
UpdateDataSource
更新DataSource
RegisterTraceWriter
注册TraceWriter
CommitData
通知Tracing Service shared memory中的数据已经更新。
CreateTraceWriter
创建TraceWriter
其他同步方法
4.traced_perf的事件处理
上一章节中,我们讨论了traced_perf与perfetto的框架的关系,本章节中着重阐述traced_perf在perfetto producer框架下,如何实现其作为perfetto的一个producer,达到profiling进线程counter信息、获取调用栈、分析性能问题的目的的。
上一章节中,描述了trace_perf 通过IPC通道从tracing service进行事件接收,这些事件最终转化为了Producer的重写函数,那么traced_perf作为tracing service的producer,需要实现这函数从而完成整个流程。
图中方框里面的是PerfProducer的事件处理状态,连接线上的字是traced_perf中发生的事件或者通过IPC接收到的命令。
4.1onConnect的实现
onConnect的实现非常简单,首先设置连接状态的状态机为“kConnected”状态,其次实例化了两个名字分别为“linux.perf”与“perfetto.metatrace”的DataSourceDescriptor,然后通过endpoint_指针的RegisterDataSource方法进行DataSource注册,其中endpoint_即为上一章节中提到的ProducerEndPoint对象的指针。
4.2StartDataSource的实现
StartDataSource的参数有两个,分别是DataSourceInstanceID和DataSourceConfig,其中DataSourceInstanceID是一个唯一的无符号64位的id,用来标识DataSource的实例;DataSourceConfig是data_source_config.proto生成的protobuf类,其原型可以参考:https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/config/data_source_config.proto;l=1;bpv=1;bpt=0
4.3启动MetaTraceSource
通过endpoint_智能指针,调用CreateTraceWriter方法,创建一个TraceWriter对象。同时将此metatrace进行使能,并保存到metatrace_writers_维护的一个map结构中。
4.4tracepoint与id的mapping的lookup操作
tracepoint一般是以名字的方式提供给配置文件的,但是linux kernel中一般使用其对应的id进行API访问控制,因此这里需要一个映射的提取。一般来说,此id可以通过位于tracefs的events/GROUP/NAME/id文件中可以提取出来。
4.5打开perf event对应的eventfd
首先将pb格式封装的配置文件转化为perf_event_attr数据结构,之后调用linux kernel提供的系统调用向操作系统注册。
打开perf event所必须的linux syscall为perf_event_open,此API的参数比较复杂,详细介绍可以参考官方文档:https://man7.org/linux/man-pages/man2/perf_event_open.2.html
这里着重讲解一下关键的配置信息:
▫perf_event_attr
perf_event_attr是一个比较大的结构体,包含了对perf_event配置的各种属性信息,以比较简单的tracepoint事件为例,一般来说需要设置以下必须的字段:
type: 设置为PERF_TYPE_TRACEPOINT类型
size: 设置为sizeof(perf_event_attr)
config:设置为上一步中获取到的mapping的id信息
sample_type: 设置sample中包含的数据类型
read_format:设置read返回的数值中包含的数据类型
开关bitmask配置:包含是否包含mmap的数据,是否包含comm等近30个配置项
pid
获取哪个pid的perf event事件
cpu
获取哪个cpu的perf event事件
groud_fd
可以将多个events通过同一个event fd进行返回,可以将其中一个事件传入-1作为group leader,后续事件可以将返回的fd传入此参数中。
4.6创建TraceWriter并使能perf event
4.7通知Unwinder启动了DataSource
4.8启动周期性读取任务
周期性的读取任务,主要是从内核的共享内存中,获取perf event的数据。在后续的章节中我们会着重讲述获取的数据。
在TickDataSourceRead函数中的ReadAndParsePerCpuBuffer中,会将从内核的共享内存中读取的sample数据,推送到unwinding_worker的queue中。当调用PostProcessQueue时,会唤醒unwinding_worker对应的线程,并执行unwind操作,直到所有sample都unwind完毕。
若DataSource的状态未停止,则需要继续抓取更多的samples,因此在这个task中,又继续调用了延迟任务,继续让task_runner调度本任务。
5.Sample的获取
Sample事件的获取是从Linux内核中提供的ring buffer共享内存中获取的,这部分操作位于PerfProducer::ReadAndParsePerCpuBuffer中进行的,这部分操作相对来说比较繁琐,下图中截取了一部分。其基本的流程是:
循环通过EventReader的ReadUntilSample获取解析好的Sample,如果DataSource的config中有配置一些filter项,则筛选掉不感兴趣的Sample,直到没有Sample产生了或者已经获取到足够的Sample了。

5.1PerfRingBuffer之环形缓冲区数据的获取
回顾一下perf_event_open函数的原型:
int syscall(SYS_perf_event_open, struct perf_event_attr *attr, pid_t pid, int cpu, int group_fd, unsigned long flags);
其中perf_event_attr结构中包含了众多的配置参数。跟通过ring buffer获取sample相关的参数有以下几个配置:
sample_period/sample_freq: 指明多久获取一次sample。
sample_type: 指明什么类型的数据会包含在sample中,比如Instruction pointer、TID、Sample的时间、地址信息等
通过perf_event_open返回的文件描述符,可以进而通过mmap系统调用,返回一个Kernel与Userspace共享的内存地址空间,此内存地址中的数据一般由Kernel写入,Userspace的程序负责对其进行解析。mmap的共享内存地址的分布如下:

metadata页对应的数据结构如下:


data_head: 指向数据区的首地址,这个地址是持续自增加的,在使用它的时候,需要将其地址与mmap buffer的大小进行一个wrap操作。
data_tail: 此数据是需要由userspace写入,指明userspace最后读取的数据的位置,从而使得内核不会降未读取的数据覆盖。
data_offset: perf_sample的起始位置由此述职来确定。
data_size: 包含了perf_sample区域大小信息
由Linux内核提供的perf sample也包含固定的格式,每个perf sample的数据原型如下:


注意到上述结构右边的if注释,假如对应的选项没有在sample_type中配置,则不包含对应的字段,在解析sample的时候,值的注意。
perf_event_header是每个sample的头信息,它的定义如下:

size: 本perf sample的大小
misc:包含本sample的一些额外的信息
type: 不同的sample类型,只有类型为PERF_RECORD_SAMPLE时,才有上面的perf sample的数据结构。比如当其类型为PERF_RECORD_LOST时,对应的perf event的数据结构为

5.2EventReader之Sample的解析
5.2.1perf sample的读取

perf sample的获取实际上是在对环形缓冲区的读取,环形缓冲区的包含一个读取偏移量以及一个写入偏移量。其中写入偏移量是由内核负责写入的,只要读取偏移量小于写入偏移量,则说明环形缓冲区中仍有数据未读取。
这里注意到环形缓冲区实际上是可以出现回卷操作的,假如出现了回卷操作,需要将数据进行重组。
5.2.2Perf sample的解析
perf sample本身的解析工作是通过EventReader::ParseSampleRecord进行的。解析后的数据结构为ParsedSample,其定义为:


可以看出,traced_perf关注的信息包含:
CommonSampleData: cpu_mode, cpu, pid, tid, timestamp, timebase_count等信息
regs: 用作unwinding的userspace寄存器信息
stack: userspace栈信息
kernel_ips: 内核instruction pointer信息
下面是Sample解析的具体流程

上述函数会返回解析好的Perf sample,即ParsedSample。进行一系列的筛选逻辑后,此sample会被发送到unwindwing_worker提供的queue中,以便于进行后续的unwinding操作。

至此,所有从内核中所需要的perf event信息已经收集并解析完毕,下一步是将之转化为可读的callstack信息的流程,这离不开unwinding 操作。
6.Unwinding操作
unwinding操作发生在解析完perf event sample之后,其发起动作的调用为:

其主要处理逻辑位于Unwinder::ConsumeAndUnwindReadySamples函数中。


当unwind成功后,调用到PerfProducer中的PostEmitSample中,将unwinding之后的数据写入TraceWriter。
6.1内核栈的解析
内核栈的解析相对简单,其主要操作函数再Unwinder::SynbolizeKernelCallchain中。其主要原理是解析"/proc/kallsyms" 中的内核地址与符号之间的对应关系。根据对应关系,将sample中的kernel态的instruction pointer翻译成地址信息。kernel 态的地址信息介绍见之前章节。

6.2用户栈的解析
用户栈的解析相对复杂,用户栈的解析首先要获取几个必要的信息:
Userspace寄存器信息
Userspace栈信息
/proc//mem信息
/proc//maps信息
其中前两个信息已经通过之前的Sample解析操作成功获取了,那么第3、4个信息怎么获取呢?
在之前的Android版本中,特权进程是可以直接访问到对应pid的这两个信息的,随着Android对安全隐私的越来越重视,对不同进程的敏感信息进行了比较强的隔离。因此traced_perf为了获取此信息,必须按照符合Android安全设计的机制,以相对复杂的方式进行实现。
6.2.1traced_perf如何请求目标进程的maps和mem信息
在AndroidRemoteDescriptorGetter类中,实现了获取/proc//mem以及/proc//maps操作的动作,获取操作是通过发起signal操作来完成的,signal的目标是目标进程:

而信息的接收是通过socket来完成的,即traced_perf进程刚启动时创建的socket:

在此socket的数据收取操作中,获取到上述两个文件的文件描述符(文件描述符已经经过内核态转换,可以在traced_perf进程中正常使用)。

上述代码中的delegate实际上指向的是PerfProducer对象,因此delegate_->onProcDescriptors会将两个文件描述符发送给PerfProducer对象。而PerfProducer进而将此文件描述符发送给了UnwinderHandle对象。
6.2.6目标进程如何将maps和mem信息发送给traced_perf
如前面所述,traced perf通过signal通知的目标进程,让目标进程将文件描述符进行了发送,那么目标进程为什么都会响应这类信息呢?(目标进程可能非常多样,包括daemon、系统apk、三方apk等),答案在C库中。
当目标进程接收到信号后,通过unix socket与traced_perf进程建立连接,然后打开两个文件maps和mem,通过unix socket的sendmsg进行发送。关于通过unix socket发送文件描述符,可以参考文档:https://man7.org/linux/man-pages/man7/unix.7.html,这里不做详细描述。
上述代码在android_profiling_dynamic.cpp中,会编译成C库的一部分,并被大多数进程所加载。
到此为止,用户栈的所有所需信息均已准备完毕。
6.2.3用户栈的解析
将所有信息准备好后,真正解析用户栈可以直接通过libunwindstack提供的方法Unwinder::Unwind即可了,反而流程显得很直接。
7.Sample的写入
Sample数据写入到trace中的操作也是比较直接的,将前面流程中获取到的信息,通过TraceWriter返回的TrackPacket protobuf结构进行写入即可。
至此,整个traced_perf的获取sample的执行流程大概完成了。
8.总结
traced_perf的工作流程主要部分包括:
perfetto流程的嵌入:traced_perf是perfetto的producer
Sample的获取
Unwinding操作
这三项主要内容看似复杂,实际上整体结构也是比较清晰的。Perfetto已经将tracing、profiling的框架打通,Tracing producer要接入perfetto,也是一件按部就班的事情而已。
9.参考链接
TracedPerf源码: https://cs.android.com/
traced_perf相关文档: https://perfetto.dev/docs
perf历史: https://en.wikipedia.org/wiki/Perf_(Linux)
simpleperf相关文档: https://android.googlesource.com/platform/prebuilts/simpleperf/+/782cdf2ea6e33f2414b53884742d59fe11f01ebe/README.md
perf_event_open: https://man7.org/linux/man-pages/man2