记⼀次Android概率性定屏问题分析解决路程
记⼀次Android概率性定屏问题分析解决路程
前⾔
最经⼀个同事遇到了概率性定屏的问题,询问我有没有解决过类似的问题,这不得让我想起了前同事解决概率⾏定屏问题留下来的宝贵⽂档,下⾯我就把前同事的⽂档关键的内容整理奉上,希望对概率性定屏问题有帮助。
⼀.问题描述
客户的机器在交易过程中出现定屏,出现过如下三种现象:
定屏1:按虚拟键有背景⾊,⽆法响应任何触摸事件,⾳量键⽆作⽤,power键有作⽤,ADB能连;
定屏2:按虚拟键⽆背景⾊,power键有作⽤,ADB能连;
定屏3:power键都⽆作⽤,ADB能连;
根据描述可见,如上三个现象,严重程度递增;
⼆.原因分析
1.复现并初步总结现场log,缩⼩可疑范围
浙江定海根据现场抓取到的logcat信息可以判断,客户的launcher应⽤经常性出现native crash,这说明客户launcher应⽤是有问题的;由表⾯现象显⽰,根据严重程度看有多种情况,不排除仅仅只是应⽤界⾯卡死,故需要加⼤测试⼒度,增加复现概率,抓取log,分类总结各根因;拿了多台机器跑monkey测试,忽略native crash:
adb shell monkey -p -s 500 --ignore-crashes --ignore-timeouts --ignore-native-crashes -v 500000000 &
⾄少5台机器⼀起测试,基本⼀两天能复现出⼀台机器;
其中⼀种情况,杀掉master应⽤可以恢复,通过am命令可以跳转到别的界⾯,进⼀步说明客户应⽤存在缺陷,由于不是系统问题,此问题不重点关注;但是,另外那种情况,应⽤不应该引起系统定住,出现这种现象只能拔电池,这个⽤户体验⾮常差,再说第三⽅应⽤也⽆法保证质量,crash⼏次也难免,所以这是个⾮常严重的问题,需要进⼀步查找系统缺陷的根因
在monkey测试复现的机器上,做如下实验,进⼀步缩⼩问题范围:
getevent 触摸事件有值,说明触摸上报⽆问题
开发者选项中打开触摸调试,现象分为两种:触摸屏幕能出现背景圆球,以及不出现背景圆球
通过adb shell,敲命令stop,start,只重启zygote⽽不重启kernel,问题消失;
通过adb shell,执⾏/system/bin/bootanimation开机动画动作,虽然⽆法正常动画,但界⾯可变化
执⾏ am start -n com.android.launcher/com.android.launcher2.Launcher
跳转,命令卡死,AMS等关键服务、InputReader、InputDispatcher都是 system_rver 启动的核⼼⼦线程,重点关注
system_rver,需要进⼀步根据log判断情况
精彩的自我介绍
从log看,system_rver出现ANR,WindowManager: Input event dispatching timed out;
起初怀疑 从 InputReader到InputDispatcher再到响应显⽰链路出现问题,但从常规logcat中得不到任何线索,看来input timeout只是问题的结果和现象,⽽不是根因;
怀疑system_rver各⼦线程出现死锁,关注anr的关键log:;搜索held by/ waiting/locked等
关键字,现场log有限,并⽆明显死锁的异常;
但其中⼀份 现场bugreport,引起了注意,system_rver有明显死锁,ActivityManagerService的锁在执⾏ ndDeathNotice 时被拿住了,很多线程都在等它
⽽其余情况下有些bugreport抓取log失效;log现象都是Input event dispatching timed out,monkey是否跟客户遇到的问题⼀致?还不得⽽知
仍然需要⾃⼒更⽣,加⼤⾃测复现概率和抓取更多有⽤信息;⽽⼤量的看起来有点疑问实际却⽆效的log,扰乱了分析思路,遮蔽了真实根因
由于复现时常常出现在输⼊法界⾯,怀疑第三⽅输⼊法调⽤系统api引起了⼀些兼容性问题,删掉输⼊法,确实概率变低,但是即使不是客户环境,monkey测试仍然出现过此问题,看来还需进⼀步分析
由于情况复杂,总结搜集了如下⼀些重要log及调试⼿段,需要根据出现问题的时间点,仔细分析⼤量的log细节:
1adb shell "ps | grep ' Z '" > 201706211748/
2
3adb shell "ps | grep ' t '" > 201706211748/
4
5adb shell "ps -t | grep ' D '" > 201706211748/ps_
6
7adb pull /sdcard/HiLogcat 201706211748/HiLogcat
8
9adb shell logcat -v threadtime -d *:v >
10
11adb shell logcat -b events -v threadtime -d *:v > 201706211748/
12
13adb shell dmesg >
14
15adb pull /data/anr ./201706211748/anr
16
17adb pull /data/tombstones ./201706211748/tombstones
18
19adb pull /data/system/dropbox ./201706211748/dropbox
20
21adb shell ls -lsa /data/anr/" > ./
22
23adb shell ls -lsa /data/tombstones/ >> ./
24明间
25adb shell ls -lsa /data/system/dropbox/ >> ./
26
27adb shell "debuggerd -b 829" > ./201706211748/
28
29adb shell "kill -3 829"
30
31adb pull /data/anr ./201706211748/anr2
32
33adb shell "echo t > /proc/sysrq-trigger"
34
35adb shell "cat /proc/kmsg" > ./201706211748/
36
37adb shell dumpstate > ./
38
360无线路由器
39adb shell dumpsys > ./
40
41adb pull /system/build.prop ./201706211748/build.prop
42
43adb bugreport > ./
其中 echo t > /proc/sysrq-trigger 能看到kernel的线程函数栈,针对死锁问题很有帮助;kill -3 pid可以得到关注应⽤空间调⽤栈,⽽debuggerd 也可以分析关注的进程栈
2.定位到debuggerd机制出了问题
分析monkey测试复现机器的log结果,重点关注分析 、bugreport;⽽system_rver、watchdog、ActivityManager等线程的锁的持有情况也是重点关注对象,出现了⼀些转机,发现 ActivityManagerService锁都被持有,执⾏am命令都⽆反应,只要执⾏am命令,再把导出来,之前没有死锁的都会出现明显死锁;
⽽通过其中的debuggerd -b pid调试⼿段去看函数调⽤栈时出现了最⼤的疑点,debuggerd ⽆反应!这时候怀疑android的debug机制有bug,于是想办法证明是否如此,总结规律,发现问题都是出现在native crash的时候,且NativeCrash线程出现异
常,NativeCrashListener: Exception dealing with report;⽽ps | grep D发现,有⼀台机器debuggerd⼀直D状态,⽽有台机器debuggerd也⼀直睡眠,睡在connect连接上,直觉认为,要解决此问题,需要详细了解debuggerd调试机制,⼭路⼗⼋弯,终于要峰回路转了,实验也确实有⽅向了
拿了两台出问题的机器,由于debuggerd只是调试使⽤,删掉system/bin/debuggerd进程,也不会影响正常⾏为,测试了三四天,都不再出现问题;只不过tombstone不再有了,影响debug crash,但这也不会影响实际出货的机器,这也不失为⼀种解决⽅案;
那debuggerd到底哪⾥出现了问题呢?删掉debuggerd总觉得不那么完美,毕竟还得使⽤debug⼿段看⼀些现场,根因还得继续查找
ps -t | grep " t "发现,客户应⽤,即“master”关键字的两个线程处于被调试状态,⽽出问题后,/data/tombstone/ ⽬录的内容不再变化,关注最后时间点的tombstone,它⽐前⾯正常的内容要少!最后时间节点的⼀份tombstone,master很多⼦线程tombstone信息都没得到输出;都是卡在最后⼀个t调试状态的master⼦线程上!
这充分说明了,debug机制确实出现了问题,在输出native crash的dump内容时,系统卡死在⼀个进程调试状态了,那么就只能分析理顺⼀下debug的流程吧;
先理⼀下框架层的crash机制:
1、system_rver启动AMS,AMS会创建⼀个⼦线程如“Thread-XX”,专门侦听crash,然后保存tombstone,让系统(AMS)做些善后处理,进⽽杀掉此进程的所有线程,调⽤流程是:
NativeCrashListener.run-->consumeNativeCrashData-->NativeCrashReporter-->handleApplicationCrashInner--
>crashApplication-->killProcess
2、“Thread-XX”是通过什么⽅式知道发⽣crash的呢?原来它创建了⼀个 "/data/system/ndebugsocket" sock,它做为服务
端,accept等待侦听 debuggerd进程连接与消息;
3、debuggerd是init进程启动的本地守护进程,它是“Thread-XX”的客户端,那它怎么知道发⽣了native crash呢?这个得从linux 信号通信机制和android的调试机制说起,由于这⾥⾮常复杂,只简要说⼀下:android修改了boinic c/c++库函数的linker,在加载so的过程中,即设置了进程的android信号处理函数,拦截了SIGSEGV等信号进⾏处理,这个信号处理函数,通过⼀个名
叫"android:debuggerd"的sock连接到debuggerd进程;原来,信号处理函数做为此sock的客户端,⽽debuggerd作为服务端;
4、综上所述,源头都是程序的信号SIGSEGV,它的出现,使进程执⾏信号处理函数,连接debuggerd,⽽debuggerd⼀⽅⾯做为"android:debuggerd" sock即信号处理函数的服务端,另⼀⽅⾯,它完成⼀次跟信号处理函数的连接后,⼜进⼀步作为“Thread-XX”的客户端,读取crash现场信息,传给“Thread-XX”做tombstone保存;debuggerd是把SIGSEGV信号通知给system_rver的桥梁;
5、上述的过程涉及的主要源码在如下⼏个⽂件:
NativeCrashListener.java (frameworks\ba\rvices\core\java\com\android\rver\am)
Linker.cpp (bionic\linker)
Debuggerd.cpp (system\core\debuggerd)
Tombstone.cpp (system\core\debuggerd)
知道了上述的⼤致流程,再去总结到底哪⾥出了问题?
同时上⽹搜索debuggerd,发现也有⼈遇到过类似的问题:
框架的这个流程中,会出现⼀些异常情况是android没考虑到的:“Thread-XX”和debuggerd的sock连接,如果服务端异常情况,那么客户端有可能阻塞在connect上;那么在debuggerd的处理流程中,加上超时机制,使之出现问题时不要⼀直阻塞;⽽NativeCrashListener在异常时,删掉sock,debuggerd⾃然也不会阻塞;
3.继续查debuggerd阻塞的另⼀种情况:D状态
这样修改代码后,以为不会再出现,测试了好⼏台机器,三天下来,都没出现,但没想到持续测试,虽然降低了概率,却⼜出现了定屏,这时候,发现debuggerd 就⼀直D状态,本来就⼀直疑惑为什么debuggerd 时常是D状态,看来还得继续往下查这个D状态;
这只能深⼊到linux内核,详细摸清debuggerd 跟信号处理函数的sock连接,对照代码,总结出如下流程:
1、客户应⽤master 进程发⽣native crash,也就是段错误,这个会让内核产⽣ SIGSEGV 信号;
2、被 bionic 的 linker 设置的 信号处理函数拦截 SIGSEGV,信号处理函数会通过sock 连接 debuggerd 守护进程;让debuggerd 做crash动作;
3、debuggerd初始化时,先保证⾃⾝忽略所有信号,accept 等候 master 发来的 crash消息,这个当然等到了,之后通过系统调⽤ptrace 的 PTRACE_ATTACH 操作进⼊内核,先建⽴与 master 进程的调试关系,(ptrace系统调⽤返回之后会关闭与信号处理函数之间的sock,之后进⼊后续处理循环);
4、在内核 PTRACE_ATTACH 期间,debuggerd 把⾃⼰设置为 master 的⽗进程;并调⽤ nd_signal 发送强制终⽌信号给master,这个过程中,会调⽤ signal_wake_up 唤醒 master 进程,在ptrace系统调⽤返回时,可能进⾏调度;
5、debuggerd继续运⾏,进⼊D状态,调⽤ wait_on_bit 等待bit19/21标志位清零来唤醒⾃⼰;此时,整个过程,⼀个master进程的SIGSEGV信号,引发了 debuggerd 进⼊D状态;等待 JOBCTL_TRAPPING 清零唤醒
6、在进⾏调度时,会进⼊到master的信号处理,在内核信号分发时使master进程修改为 stop 状态,以⽅便被跟踪调试;
7、在内核中master线程的上下⽂中,处理master的信号时,get_signal_to_deliver会依次调⽤ do_signal_stop do_jobctl_trap ptrace_signal ,都会调⽤到 ptrace_stop ,之后再退出函数循环;
如果不是被调试,则原本准备好信号处理函数的环境后,进⼊到⽤户空间信号处理函数,处理完再返回内核善后再进⼊⽤户空间,
但由于是被调试状态,ptrace_stop会让master暂停,并唤醒调试进程 debuggerd ,然后schedule出去;
8、debuggerd 从D唤醒得到运⾏,得到⼀些master进程的状态后,从ptrace系统调⽤返回到⽤户空间,通过 sock 写个ack给master 的 boinic 信号处理函数
9、debuggerd继续运⾏,也通过sock连接到AMS,保存master的tombstone信息,之后再次调⽤ptrace系统调⽤,通过家访活动记录表
PTRACE_DETACH 解除与master的调试关系,使之继续运⾏;
10、master 进程的boinic信号处理函数从sock读取到ack回复消息;之后设置信号的处理⽅式为默认处理⽅式;杀死⾃⼰;并且发送SIGSTOP 信号给全组线程;然后退出了信号处理函数;
11、由于master的⼦线程都会收到信号,继续上⾯的处理循环,把⼦线程的tombstone信息都保存起来
这个流程⾮常复杂,需要深⼊到内核的信号机制、跟踪调试机制,详细了解kill & ptrace两个系统调⽤的处理流程,同时涉及⼀些CPU体系结构的细节,有兴趣的可以通过“LINUX内核源代码情景分析.PDF”详细了解,涉及的主要⽂件有:
Signal.c (kernel)
Ptrace.c (kernel)婚礼新娘
Signal.c (arch\arm\kernel)
Ptrace.c (arch\arm\kernel)
为了便于分析问题,引⼊了⼿动crash内核,保存内存镜像,通过crash分析内存来调试内核的⽅法,这是分析内核的利器;
顿开茅塞的意思
4.使⽤crash⼯具分析内核ptrace系统调⽤的阻塞情况
先介绍⼀下crash⼯具的常见命令:
启动crash⼯具:./crash DDRCS0.BIN@0x80000000 vmlinux
crash> help //帮助命令,显⽰出crash⽀持的命令
crash> log //打印dmesg
还有 bt whatis rd task struct list kmem dis p mach ps foreach 等命令,不⼀⼀介绍了,分析时都会有⽤到
上述第3节介绍了debuggerd睡在⼀个标志位上,即wait_on_bit(&task->jobctl, JOBCTL_TRAPPING_BIT
,ptrace_trapping_sleep_fn, TASK_UNINTERRUPTIBLE);
睡在这个JOBCTL_TRAPPING_BIT标志位上的线程,会通过如下代码唤醒:
void task_clear_jobctl_trapping(struct task_struct *task)
{undefined
if (unlikely(task->jobctl & JOBCTL_TRAPPING)) {undefined
task->jobctl &= ~JOBCTL_TRAPPING;
wake_up_bit(&task->jobctl, JOBCTL_TRAPPING_BIT);
}
}
它对应的ptrace系统调⽤睡眠的代码:
ptrace调⽤过程:
debuggerd调⽤ptrace,ptrace会通过信号机制,发送信号给要调试的线程master,master的信号处理过程,⼜会调⽤ ptrace_stop--> task_clear_jobctl_trapping,再唤醒debuggerd,⼤致流程就是这样;
通过crash也可以看出:
先找出处于调试态的线程,和处于D状态的debuggerd 线程号
打印debuggerd/30403的函数调⽤栈群禁言在哪里设置