由Ctrl+Z引发的系统卡死重启问题分析

时间:2024-05-19 08:52:09

起因

JT同学拿着手机来找我说用着用着自动重启了,让我看一下,我抓出bugreport一看是system server发生watchdog重启了,然后接着看了一下traces,发现traces比较诡异。

初步分析

为什么说traces诡异?暂且一看

由Ctrl+Z引发的系统卡死重启问题分析

都被thread 31持有的锁给block了,那就看看thread 31在搞什么飞机?

"InputDispatcher" prio=10 tid=31 Native
  | group="main" sCount=1 dsCount=0 obj=0x133d90a0 self=0x7f73f8c800
  | sysTid=1662 nice=-8 cgrp=default sched=0/0 handle=0x7f5f80b440
  | state=S schedstat=( 1388341757 1177043722 12975 ) utm=81 stm=57 core=2 HZ=100
  | stack=0x7f5f70f000-0x7f5f711000 stackSize=1013KB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/1662/stack)
  native: #00 pc 00000000000684e0  /system/lib64/libc.so (__ioctl+4)
  native: #01 pc 0000000000072508  /system/lib64/libc.so (ioctl+100)
  native: #02 pc 000000000002d584  /system/lib64/libbinder.so (_ZN7android14IPCThreadState14talkWithDriverEb+164)
  native: #03 pc 000000000002e050  /system/lib64/libbinder.so (_ZN7android14IPCThreadState15waitForResponseEPNS_6ParcelEPi+104)
  native: #04 pc 000000000002e2c4  /system/lib64/libbinder.so (_ZN7android14IPCThreadState8transactEijRKNS_6ParcelEPS1_j+176)
  native: #05 pc 0000000000025654  /system/lib64/libbinder.so (_ZN7android8BpBinder8transactEjRKNS_6ParcelEPS1_j+64)
  native: #06 pc 00000000000e02cc  /system/lib64/libandroid_runtime.so (???)
  native: #07 pc 0000000000505e64  /data/dalvik-cache/arm64/[email protected]@boot.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+200)
  at android.os.BinderProxy.transactNative(Native method)
  at android.os.BinderProxy.transact(Binder.java:503)
  at android.app.IActivityController$Stub$Proxy.activityStarting(IActivityController.java:172)
  at com.android.server.am.ExtraActivityManagerService.checkStartActivityLocked(ExtraActivityManagerService.java:259)
  at com.android.server.am.ExtraActivityManagerService.checkStartActivityPermission(ExtraActivityManagerService.java:207)
  - locked <0x025805bb> (a com.android.server.am.ActivityManagerService)
  at com.android.server.am.ActivityStackSupervisor.startActivityMayWait(ActivityStackSupervisor.java:983)
  at com.android.server.am.ActivityManagerService.startActivityAsUser(ActivityManagerService.java:3962)
  at android.app.ContextImpl.startActivityAsUser(ContextImpl.java:696)
  at android.app.ContextImpl.startActivityAsUser(ContextImpl.java:675)
  at com.android.server.policy.PhoneWindowManager.startActivityAsUser(PhoneWindowManager.java:3250)
  at com.android.server.policy.PhoneWindowManager.startDockOrHome(PhoneWindowManager.java:6545)
  at com.android.server.policy.PhoneWindowManager.launchHomeFromHotKey(PhoneWindowManager.java:3391)
  at com.android.server.policy.PhoneWindowManager.launchHomeFromHotKey(PhoneWindowManager.java:3342)
  at com.android.server.policy.BaseMiuiPhoneWindowManager.interceptKeyBeforeDispatching(BaseMiuiPhoneWindowManager.java:643)
  at com.android.server.wm.InputMonitor.interceptKeyBeforeDispatching(InputMonitor.java:375)
  at com.android.server.input.InputManagerService.interceptKeyBeforeDispatching(InputManagerService.java:1751)

调用ActivityController的activityStarting接口时阻塞了?

这里说明一下,通过名字我们大概能猜到ActivityController的作用,简单来说就是一个监控器,提供的接口如下:

/**
 * Testing interface to monitor what is happening in the activity manager
 * while tests are running.  Not for normal application development.
 * {@hide}
 */
interface IActivityController
{
    /**
     * The system is trying to start an activity.  Return true to allow
     * it to be started as normal, or false to cancel/reject this activity.
     */
    boolean activityStarting(in Intent intent, String pkg);

    /**
     * The system is trying to return to an activity.  Return true to allow
     * it to be resumed as normal, or false to cancel/reject this activity.
     */
    boolean activityResuming(String pkg);

    /**
     * An application process has crashed (in Java).  Return true for the
     * normal error recovery (app crash dialog) to occur, false to kill
     * it immediately.
     */
    boolean appCrashed(String processName, int pid,
            String shortMsg, String longMsg,
            long timeMillis, String stackTrace);

    /**
     * Early call as soon as an ANR is detected.
     */
    int appEarlyNotResponding(String processName, int pid, String annotation);

    /**
     * An application process is not responding.  Return 0 to show the "app
     * not responding" dialog, 1 to continue waiting, or -1 to kill it
     * immediately.
     */
    int appNotResponding(String processName, int pid, String processStats);

    /**
     * The system process watchdog has detected that the system seems to be
     * hung.  Return 1 to continue waiting, or -1 to let it continue with its
     * normal kill.
     */
    int systemNotResponding(String msg);
}

为什么调用一下这个ActivityController的activityStarting就被block了呢?这个ActivityController是谁?在那个进程中?

继续往下分析,看看都有谁实现了ActivityController
由Ctrl+Z引发的系统卡死重启问题分析

比较可疑的有两个,一个是monkey的,一个是Am的,然后就问了一下JT同学问题发生的场景,是不是在做monkey测试或者执行adb shell am相关的命令操作?他说是的,问题发生当时在执行am命令。

好,问题有了一点眉目,但是为什么执行am命令会导致阻塞并重启呢?继续看代码

        @Override
        public boolean activityStarting(Intent intent, String pkg) {
            synchronized (this) {
                System.out.println("** Activity starting: " + pkg);
            }
            return true;
        }

执行activityStarting需要ActivityController自己本身的锁,如果这个时候拿不到锁就会block system server,那就继续看看这个锁还会被谁用,看了一圈代码,发现锁的粒度都非常小,不像会block的样子,这就是上面为什么说traces比较诡异的原因,block在了不应该block的地方,那接下来怎么办呢?

深入分析

通过初步分析我们觉得调用ActivityController的activityStarting不应该被block,而这次调用是一个binder call,跨进程的,有没有可能没有到达am进程那边?或者am进程出了什么问题?或者另有隐情?赶紧再问问JT同学当时的操作步骤,看能不能重现问题,然后dump一下相关信息,JT同学不愧是专业的测试,操作现场还保留着,执行的一堆命令也还在终端上,赶紧再操作一遍

ps | grep ^u
am monitor
ctrl+z

问题奇迹复现了,点击一个icon去startactivity之后系统就卡死了,赶紧在一分钟内看看am进程的信息

ps | grep am

没有am的信息?那就看看app_process

ps | grep app_process

原来am小伙通过app_process起来之后连名字都没改。。。。

等等,我好像擦觉到了一丝异常?上面为什么要用ctrl+z?赶紧问JT同学,他说这是HH同学敲的命令,难道HH同学是想停止am monitor继续在终端里面输命令吗?如果同样的需求我都是用ctrl+c的啊?他为啥要用ctrl+z,有特别的含义吗?赶紧去问一下HH同学,他说他只是想停止am monitor继续在终端里面输命令,没有特殊含义,而且他都是这么用的?这下问题就看到曙光了,这两个组合键操作虽然都能满足停止当前任务继续输入其他命令的需求,但是ctrl+z和ctrl+c是有本质区别的,在Linux下区别是什么呢?

ctrl-c: ( kill foreground process ) 发送 SIGINT 信号给前台进程组中的所有进程,强制终止程序的执行
ctrl-z: ( suspend foreground process ) 发送 SIGTSTP 信号给前台进程组中的所有进程,常用于挂起一个进程,而并
            非结束进程,用户可以使用使用fg/bg操作恢复执行前台或后台的进程。fg命令在前台恢复执行被挂起的进
            程,此时可以使用ctrl-z再次挂起该进程,bg命令在后台恢复执行被挂起的进程,而此时将无法使用ctrl-z
            再次挂起该进程;
            一个比较常用的功能:
                   正在使用vi编辑一个文件时,需要执行shell命令查询一些需要的信息,可以使用ctrl-z挂起vi,等执行                   完shell命令后再使用fg恢复vi继续编辑你的文件(当然,也可以在vi中使用!command方式执行shell命令,
            但是没有该方法方便)。
ctrl-d: ( Terminate input, or exit shell ) 一个特殊的二进制值,表示 EOF,作用相当于在终端中输入exit后回车;

类似的组合键操作还有以下几个:

ctrl-/    发送 SIGQUIT 信号给前台进程组中的所有进程,终止前台进程并生成 core 文件
ctrl-s   中断控制台输出
ctrl-q   恢复控制台输出
ctrl-l    清屏

看到这里我想有些同学可能已经知道问题的原因了,am monitor执行之后,am进程会向AMS注册一个ActivityController,启动一个activity的时候会回调ActivityController的activityStarting接口,但是HH同学用Ctrl+Z挂起am进程之后,AMS在通过binder call activityStarting的时候就被block了,因为activityStarting是同步调用,虽然binder driver把这次调用的transaction放到了am进程的todo list里面,但是am进程因为是STOPED状态所以不会拿到时间片去执行todo list里面的transaction,进而也不会有reply,就这样system server就被卡住了,瞬间又加重了Android系统的脆弱感。。。

到这就结束了吗?当然没有,我们细心的JT同学又发现说好的一分钟watchdog重启,都等了好几分钟了怎么还没有重启?好问题,继续看代码
由Ctrl+Z引发的系统卡死重启问题分析

代码是最清晰、最好的解释,原来watchdog线程也依赖ActivityController,会调用ActivityController的systemNotResponding,从而导致连watchdog线程也被卡住了,本来说好的一分钟重启就做不到了,近在咫尺的重启代码执行不了,连GOODBYE也没出来就死了,哎,生活就是这样,这就是生活。。。。

                Slog.w(TAG, "*** GOODBYE!");
                Process.killProcess(Process.myPid());
                System.exit(10);

下一步动作

虽然生活总让人无奈,但我们还是要想办法做的更好,接下来会尝试改善watchdog线程依赖ActivityController的机制,不能因为ActivityController出了问题就永远卡住系统,之前就分析过一个monkey导致的系统永远卡住的问题,也是因为在调用ActivityController的systemNotResponding时候卡住:由MONKEY测试引发的跨多个进程的ANDROID系统死锁问题分析

通过下面的步骤可以在所有Android手机上复现这个问题:

am monitor
ctrl+z
随便点击一个桌面图标或者按下home键