写点什么

Android ANR 分析(trace 文件的产生流程)

作者:北洋
  • 2022 年 4 月 21 日
  • 本文字数:3900 字

    阅读完需:约 13 分钟

首先收集需要 dump trace 的进程并给对应进程发送 dump trace 的信号

1.当一些带有超时机制的系统消息(如:Service 的创建)判定超时后,会调用系统服务 AMS 接口,收集 ANR 相关信息并存档(data/anr/trace, data/system/dropbox)



2.进入到 AMS 中,AppError 会先进行筛选(1.当前进程正在进行 dump 流程 2.已经发生 crash 3. 已经被系统 kill 4.系统是否正在关机等情况),如果都不符合,则认为当前进程发生了 anr。



3.接下来系统在判断当前 ANR 进程对用户是否可感知,然后开始统计与该进程由关联的进程,或者一些系统核心服务进程的信息(例如与应用交互的 SurfaceFligner,System Server 等系统进程),如果这些系统服务进程在响应时被阻塞,那么将导致应用进程 IPC 通信过程被卡死。接着获取其他系统核心进程,因为这些服务进程是 init 进程直接创建的,并不在 SystemServer 或 Zygote 进程管理范围。


firstPids 队列:第一个是 ANR 进程,第二个是 system_server,剩余是所有 persistent 进程;Native 队列:是指/system/bin/目录的 mediaserver,sdcard 以及 surfaceflinger 进程;lastPids 队列: 是指 mLruProcesses 中的不属于 firstPids 的所有进程。



4.在收集完第一步信息后,接下来便开始统计各进程本地的更多信息,如虚拟机信息,java 线程状态及堆栈。首先会弹出一个 ANR 的对话框,然后向 UI 线程发送 SHOW_NOT_RESPONDING_MSG 消息



5.当 UI 线程收到该消息后,会调用 dumpStackTraces 函数:



最重要的一点:向目标进程发送 SINAL_QUIT(进程中的 Signal Catcher 会进行阻塞检测收集信息后面讲),firstPids 列表中的进程, 两个进程之间会休眠 200ms, 可见 persistent 进程越多,则时间越长. top 5 进程的 traces 过程中, 同样是间隔 200ms, 另外进程使用情况的收集也是比较耗时.


==总结==;


将 am_anr 信息输出到 EventLog(分析 anr 问题时先看该 log)获取重要进程的信息,java 进程的,和 native 的进程将 ANR 的 Reason 和 CPU 使用的情况输出到 main_log 在将 CPU 使用情况和进程的 trace 文件信息,在保存到 drpobox 文件下向收集到的进程发送 SINAL_QUIT 信号。

接着分析最后一步向收集到的进程发送信号

==(Android5.0 之前是 dump 用的 SuspendAll 线程,收集信息之后用 ResumeAll 恢复。在 5.0 之后采用的是 checkPoint 进行 dump 信息)==


发生 ANR 时,systemServer 进程会执行 dumpStackTraces 函数,在该函数中发 SIGQUIT 信号给对应的进程(上面有分析到)


处于安全考虑,进程之间是相互隔离的,即使系统进程也无法获取其他进程的信息,所以要借助于 IPC 通信,将指令发送到目标进程,目标进程接收到消息后,协助完成自身进程 Dump 信息并发送给系统进程。Android P 流程:



1.一个进程接收到了 SIGQIUT 信号的时候,SingaCatcher 线程的 WaitForSignal 函数会返回接着会调用到 HandlerSigQuit()函数。



2.hindleSigQuit()函数为:



3.DumpForSigQuit()函数:



这是读取的信息,但是什么时候去读取呢(什么时候才能保证获取到的却是是需要的东西,例如 GC 信息,当前分配了多少对象,这些打印一般都需要在 suspend 当前进程里面的所有的线程),接下来先分析这个 suspend 过程:



这个挂起 SupendAll 实在 Thread_list.cc 中实现的,他的作用就是用来 suspend 当前进程里面所有其他的线程(一般发生在 GC,DumpForSigQuit 等过程中)。SuspendAll 过程实现最重要的就是 ModifySupendCount(self,+1,false)这段语句他会修改对应 Thread 对象的 suspend 引用计数:



因为传入的 delta 值是+1 所以会先执行 AtmoicSetFlag()利用原子操作设置了 KSuspendRequest 标志位,代表当前这个线程有挂起请求。什么时候会进行检测这个标志位呢?==这里涉及到了 checkPoint 的知识点最后讲解==(在线程运行中进行上下文切换(例如 java 线程转换为 Native 线程)时就会运行 CheckSuspend 函数,这个函数才是真正的把当前线程 suspend:



可以看到检测到了 KSuspendRequest 标记就会执行 FullSuspend 函数,KSuspendRequest 标志位是用来 dump 线程的堆栈的,分析完了 SuspendAll 之后,再继续分析 FullSuspendCheck 函数:



调用 TransitionFromRunnableToSuspend()这个函数后,线程就进入了 KSuspended 状态,然后在调用 TransitionFromSuspendedToRunnablecpm 函数从 Suspend 状态切换到 Runnable 状态的时候会阻塞在一个条件变量上,除非调用 SuspendAll 的线程接着又调用了 ResumeAll()函数,要不然这些线程就会一直被阻塞住。


4.现在就把 SuspendAll 的流程分析完了,但是 dump 线程堆栈的时候并不是在设置了挂起标志位(KSuspendRequest)后执行的,与他相关的是另外一个标志位 KCheckpointRequest,接下来看一下 Thread_list 的 Dump 函数,这个函数会在 Thread_list 的 DumpForSigQuit 中会被调用到,也就是在 Signal Cathcer 线程处理 SIGQUIT 信号的过程中。



这个函数先创建了一个叫 DumpCheckPoint 对象 checkpoint,然后调用了 RunCheckpoint 将这个对象传入,这个函数会返回现在处于 Runnable 状态的线程个数,接着调用了 WaitForThreadsToRunThroughCheckpoint()等待这些处于 Runnable 的线程都执行完 DumpCheckpoint 的 Run 函数,如果等待超时就会报错。



接着分析 RunCheckPoint 函数,先看前一部分:



对于处于 Runnable 状态的线程执行它的 RequestCheckpoint 函数会返回 true,其他状态的线程则会返回 false。对于这些非 Runnable 状态的线程就会像 SuspendAll 一样会设置 KSuspendRequest 标志位,后面状态切换的时候就会检查这个标志位挂起。同事 RunCheckPoint 函数会把这些线程统计到 suspend_count_modified_threads 这个 Vector 变量中,在这个变量中的线程,Singal Catcher 线程会主动触发他们的 dump 堆栈过程。接下来再看看这个 RequestCheckpoint 函数



最后一行设置 kCheckpointRequest 标志位,在刚才线程切换运行状态时会执行 CheckSuspend 函数在检测 kCheckpointRequest 标志位的时候会执行 RunCheckpointFunction 函数,接着会执行这个 checkpoints 里面元素的 run 函数:



(这个存储的其实就是 Thread 中的 RequestCheckpoint 在这里不仅设置了标志位还把参数设置为元素的值,这个参数就是 Dump 里面调用 RunCheckpoint 传过来的,其实就是 DumpCheckpoint)。,所以也就是执行 DumpCheckpoint 的 run 函数:



其实就是调用了 Thread 的 Dump 函数,线程的 java 堆栈,Native 堆栈和 Kernel 堆栈就是在这里打印的,刚才说对于处于 Runnable 状态的线程是通过调用他们的 RequestCkeckPoint 函数,然后它们自己去 dump 当前堆栈的,而那些不处于 Runnable 状态的线程则是添加到了一个 Vector 的变量中,接着就分析 RunCheckPoint 函数的第二部分:



对于这些不是 Runnable 状态的线程,他们可能不会主动去调用 Run 函数,所以只能有 Signal Catcher 线程去帮他们 Dump,至于 DumpCheckpoint 的 Run 函数的功能和 Runnable 状态的线程是一样的,都是打印线程堆栈,并且最后修改引用计数让这些线程在切换状态时继续运行。


==总结==:


1.SingalCatcher 线程接收到信号后,首先 Dump 当前虚拟机有关信息(内存状态。对象,加载 class,GC 等相关信息)2.接下来会设置每个线程的标记为(check_point),和请求线程状态(suspend)。当线程运行过程中进行上下文切换时,会检查该标记。如果发现有挂起请求,会将自己主动挂起。等到所有线程都挂起之后,SingalCatcher 线程开始遍历 Dump 各个线程的堆栈和线程数据后再唤醒线程。如果某个线程一直无法挂起导致超时,那么本次 Dump 流程失败抛出异常.


==大致流程(Android5.0 之前)==:



==checkPoint:==先讲解 safePoint,对于 ART 编译的代码,可以定期轮询当前 Runtime 来确认是否需要执行某些特定代码;可以认为这些轮询时的点,就是 safepoint;safepoint 可以用来实现暂定一个 java 线程,也可以用来实现 Checkpoint 机制;例:当正在执行 java 代码的线程 A 执行到 safepoint 时,会执行 CheckSuspend 函数,在发现当前线程有 checkpoint request 时,


会在这个点执行线程的 CheckPoint 函数;如果发现当前线程有 suspend request 时,会进行 SuspendCheck,使得线程进入 Suspend 状态(暂停);


所以说,ART CheckPoint 应该是 safepoint 的一个功能实现;


==safePoint 讲解链接:==


作者:RednaxelaFX 链接:https://www.zhihu.com/question/48996839/answer/113801448来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


从编译器和解释器的角度看,ART 的 safepoint 有两种:


主动 safepoint:编译生成的代码里或者解释代码里有主动检查 safepoint 的动作,并在发现需要进入 safepoint 时跳转到相应的处理程序里。ART 的解释器安插主动 safepoint 的位置在循环的回跳处(backedge,具体来说是在跳转前的源头处)以及方法返回处(return / throw exception)。ART Optimizing Compiler 安插主动 safepoint 的位置在循环回跳处(backedge,具体来说是在跳转前的源头处)以及方法入口处(entry)。被动 safepoint:所有未内联的方法调用点(call site)都是被动 safepoint。这里并没有任何需要主动执行的代码,而就是个普通的方法调用。之所以要作为 safepoint,是因为执行到方法调用点之后,控制就交给了被调用的方法,而被调用的方法可能会进入 safepoint,safepoint 中可能需要遍历栈帧,因此 caller 也必须处于 safepoint。


安插 safepoint 的位置的思路是:程序要能够在 runtime 发出需要 safepoint 的请求后,及时地执行到最近的 safepoint 然后把控制权交给 runtime。怎样算“及时”?只要执行时间是有上限(bounded)就可以了,实时性要求并不是很高。于是进一步假设,向前执行(直线型、带条件分支都算)的代码都会在有限时间内执行完,所以可以不用管;而可能导致长时间执行的代码,要么是循环,要么是方法调用,所以只要在这两种地方插入 safepoint 就可以保证及时性了。至于具体在方法入口还是出口、循环回边的源头还是目标处插入 safepoint,这是个具体实现的细节,只要选择一边插入就可以了。

发布于: 刚刚阅读数: 4
用户头像

北洋

关注

Android开发 2021.05.25 加入

记录Android学习之路 分享读书心得体会~

评论

发布
暂无评论
Android ANR分析(trace文件的产生流程)_4月月更_北洋_InfoQ写作社区