ANR系列(二)——ANR监听方案之SIGQUIT信号的监控

前言

这种方案才是真正的监控ANR,matrix、xCrash都在使用这种方案,已经在国民应用微信等app上检验过,稳定性和可靠性都能得到保证。

SignalCatcher线程

在明白SIGQUIT信号监控之前,要理解一下系统的SignalCatcher线程的职责

  • 每一个应用进程都会有一个SignalCatcher线程,专门处理SIGQUIT等信号
  • 在接受到SIGQUIT信号后,开始遍历Dump每个线程的堆栈和线程数据

1、SignalCatcher的初始化

在初始化中,通过CHECK_PTHREAD_CALLpthread_create创建一个SignalCatcher线程

SignalCatcher::SignalCatcher(const std::string& stack_trace_file)
    : stack_trace_file_(stack_trace_file),
      lock_("SignalCatcher lock"),
      cond_("SignalCatcher::cond_", lock_),
      thread_(nullptr) {
  SetHaltFlag(false);

  // Create a raw pthread; its start routine will attach to the runtime.
  CHECK_PTHREAD_CALL(pthread_create, (&pthread_, nullptr, &Run, this), "signal catcher thread");

  Thread* self = Thread::Current();
  MutexLock mu(self, lock_);
  while (thread_ == nullptr) {
    cond_.Wait(self);
  }
}

2、SignalCatcher线程的执行

在C++层同样的,创建线程之后,就会执行线程的Run方法

void* SignalCatcher::Run(void* arg) {

  ......
  
  // Set up mask with signals we want to handle.
  SignalSet signals;
  signals.Add(SIGQUIT);
  signals.Add(SIGUSR1);

  while (true) {
    // 1、监控信号
    int signal_number = signal_catcher->WaitForSignal(self, signals);
    if (signal_catcher->ShouldHalt()) {
      runtime->DetachCurrentThread();
      return nullptr;
    }

    switch (signal_number) {
    case SIGQUIT:
      signal_catcher->HandleSigQuit();
      break;
    case SIGUSR1:
      signal_catcher->HandleSigUsr1();
      break;
    default:
      LOG(ERROR) << "Unexpected signal %d" << signal_number;
      break;
    }
  }
}

int SignalCatcher::WaitForSignal(Thread* self, SignalSet& signals) {
  ScopedThreadStateChange tsc(self, kWaitingInMainSignalCatcherLoop);

  2、 阻塞等待线程的信号
  int signal_number = signals.Wait();
  if (!ShouldHalt()) {
    LOG(INFO) << *self << ": reacting to signal " << signal_number;
    Runtime::Current()->DumpLockHolders(LOG(INFO));
  }

  return signal_number;
}

在Run方法中可以看到线程监控SIGQUITSIGUSR1,并且通过signal_catcher->WaitForSignal(self, signals)阻塞等待线程的信号。如果遇到SIGQUIT信号后,则通过signal_catcher->HandleSigQuit()处理当前的信号,并dump出当前的stacktraces

SIGQUIT信号的监控

信号的处理在Linux端是比较常见的方式,在这里只要利用Linux特性监听到信号就能满足信号的监控

1、方案1 (sigwait)

模仿SignalCatcher线程使用的sigwait方法进行同步、阻塞地监听,跟SignalCatcher线程做一样的事情,创建一个子线程,通过一个死循环sigwait,一直监听SIGQUIT

#include <jni.h>
#include <string>
#include <pthread.h>
#include <syslog.h>
#include <android/log.h>
#include <dirent.h>
#include <unistd.h>

#define TAG "Hensen"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)

static void *mySigQuitCatcher(void* args) {
    while (true) {
        int sig;
        sigset_t sigSet;
        sigemptyset(&sigSet);
        sigaddset(&sigSet, SIGQUIT);
        sigwait(&sigSet, &sig);
        if (sig == SIGQUIT) {
            // 2、ANR信号监听
            
        }
    }
}

// 1、Android端调用此JNI
extern "C" JNIEXPORT void JNICALL
Java_com_example_syncbarriermonitor_NativeLib_monitor(
        JNIEnv *env,
        jobject /* this */) {

    LOGD("monitor start");

    pthread_t pid;
    pthread_create(&pid, nullptr, mySigQuitCatcher, nullptr);
    pthread_detach(pid);

    LOGD("monitor end");
}

通过案例学知识:

  • pthread_create:创建线程,在线程创建以后,就开始运行相关的线程函数。
  • pthread_detach:是一个不阻塞函数,即主线程与子线程分离,子线程结束后,资源自动回收。
  • sigwait:阻塞本进程,去等待指定的信号。等到了就会解除阻塞。

当前方案会产生第二个线程通过sigwait方法监听同一个信号,那么和系统SignalCatcher线程两个互相抢占,当发生信号的时候,具体是哪一个线程收到信号时不能确定的。因此,当前方案不满足我们的需求

2、方案2 (Signal Handler)

通过Linux提供的sigaction方法注册signal handler来监听SIGQUIT信号,微信的Matrix库就是运用的这个原理

#include <jni.h>
#include <string>
#include <pthread.h>
#include <syslog.h>
#include <android/log.h>
#include <dirent.h>
#include <unistd.h>

#define TAG "Hensen" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型

void signalHandler(int sig, siginfo_t *info, void *uc) {
    if (sig == SIGQUIT) {
        // 4、ANR信号监听
        LOGD("sig == SIGQUIT");
    }
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_syncbarriermonitor_NativeLib_monitor(
        JNIEnv *env,
        jobject /* this */) {

    LOGD("monitor start");

    // 1、第一步:我们通过pthread_sigmask把SIGQUIT设置为UNBLOCK
    sigset_t sigSet;
    sigemptyset(&sigSet);
    sigaddset(&sigSet, SIGQUIT);
    pthread_sigmask(SIG_UNBLOCK, &sigSet, nullptr);

    // 2、第二步:建立了SignalHandler监听ANR
    struct sigaction sa;
    sa.sa_sigaction = signalHandler;
    sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTART;
    sigaction(SIGQUIT, &sa, nullptr);

    // 3、第三步:重新向Signal Catcher线程发送一个SIGQUIT
    int tid = getSignalCatcherThreadId(); //遍历/proc/[pid]目录,找到SignalCatcher线程的tid
    tgkill(getpid(), tid, SIGQUIT);

    LOGD("monitor end");
}
  • 第一步:由于Android默认把SIGQUIT设置成了BLOCKED,系统默认只会响应sigwait而不会进入到我们设置的handler方法中
  • 第二步:建立了Signal Handler,系统sigwaitsignal handler的都能捕获到SIGQUIT信号
  • 第三步:由于通过Signal Handler抢到了SIGQUIT,原本的Signal Catcher线程中的sigwait就不再能收到SIGQUIT,原本的dump堆栈的逻辑就无法完成了,为了保持和系统的ANR流程保持一致,需要在Signal Handler里面重新向Signal Catcher线程发送一个SIGQUIT

通过案例学知识:

  • pthread_sigmask:用来确定如何更改信号组
  • sigaction:用来查询和设置信号处理方式
  • tgkill:内核提供的系统调用,向线程发送信号

3、方案2的优化 (Signal Handler)

当我们的signalHandler收到ANR的信号的时候,我们需要告知Java层调用onANRDumped(),具体的上报等逻辑由客户端业务处理

void signalHandler(int sig, siginfo_t *info, void *uc) {
    if (sig == SIGQUIT) {
       anrDumpCallback();
    }
}

bool anrDumpCallback() {
    JNIEnv *env = JniInvocation::getEnv();
    if (!env) return false;
    env->CallStaticVoidMethod(gJ.AnrDetective, gJ.AnrDetector_onANRDumped);
    return true;
}

最终在我们的Java层onANRDumped()方法上

private synchronized static void onANRDumped() {
    ......
}

优化方向

在收到SIGQUIT信号后,会产生误报的现象,针对两个点做了优化

  1. 在收到SIGQUIT信号后,为了防止SIGQUIT信号是不是由本应用ANR导致的,需要判断当前进程是否被置了ProcessErrorStateInfo.NOT_RESPONDING状态,如果没有,则说明这个信号可能是由其他应用ANR导致的。这里采取的方案是创建Check线程在20s内每隔500ms就轮询查一次
  2. 还有一种情况,当后台ANR后,并不会设置NOT_RESPONDING状态。这里采取的方案是检查主线程queue#mMessageswhen字段,判断当前卡顿的时长,如果发现已卡顿前台2s,后台10s,则认为这是一个anr,立即上报,防止漏报
private synchronized static void onANRDumped() {
    ......
    confirmRealAnr(true);
}

private static void confirmRealAnr(final boolean isSigQuit) {
    //1、第二点优化点:优先确认when字段的卡顿时长
    boolean needReport = isMainThreadBlocked();
    if (needReport) {
        report(false, isSigQuit);
    } else {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //2、第一点优化点:创建线程轮询ProcessErrorStateInfo.NOT_RESPONDING
                checkErrorStateCycle(isSigQuit);
            }
        }, CHECK_ANR_STATE_THREAD_NAME).start();
    }
}

isMainThreadBlocked的关键代码

  • 通过反射拿到对应的when字段,并判断当前卡顿的时长
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isMainThreadBlocked() {
    try {
        MessageQueue mainQueue = Looper.getMainLooper().getQueue();
        Field field = mainQueue.getClass().getDeclaredField("mMessages");
        field.setAccessible(true);
        final Message mMessage = (Message) field.get(mainQueue);
        if (mMessage != null) {
            anrMessageString = mMessage.toString();
            MatrixLog.i(TAG, "anrMessageString = " + anrMessageString);
            long when = mMessage.getWhen();
            if (when == 0) {
                return false;
            }
            long time = when - SystemClock.uptimeMillis();
            anrMessageWhen = time;
            long timeThreshold = BACKGROUND_MSG_THRESHOLD;
            if (currentForeground) {
                timeThreshold = FOREGROUND_MSG_THRESHOLD;
            }
            return time < timeThreshold;
        } else {
            MatrixLog.i(TAG, "mMessage is null");
        }
    } catch (Exception e) {
        return false;
    }
    return false;
}

checkErrorStateCycle的关键代码

  • 通过遍历ActivityManager,获取当前的进程ProcessErrorStateInfo,再获取它的condition属性做对比
private static boolean checkErrorState() {
    try {
        MatrixLog.i(TAG, "[checkErrorState] start");
        Application application =
                sApplication == null ? Matrix.with().getApplication() : sApplication;
        ActivityManager am = (ActivityManager) application
                .getSystemService(Context.ACTIVITY_SERVICE);

        List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
        if (procs == null) {
            MatrixLog.i(TAG, "[checkErrorState] procs == null");
            return false;
        }

        for (ActivityManager.ProcessErrorStateInfo proc : procs) {
            MatrixLog.i(TAG, "[checkErrorState] found Error State proccessName = %s, proc.condition = %d", proc.processName, proc.condition);

            if (proc.uid != android.os.Process.myUid()
                    && proc.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                MatrixLog.i(TAG, "maybe received other apps ANR signal");
                return false;
            }

            if (proc.pid != android.os.Process.myPid()) continue;

            if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                continue;
            }

            MatrixLog.i(TAG, "error sate longMsg = %s", proc.longMsg);

            return true;
        }
        return false;
    } catch (Throwable t) {
        MatrixLog.e(TAG, "[checkErrorState] error : %s", t.getMessage());
    }
    return false;
}

SIGQUIT信号监控后的处理

我们都知道监控到信号之后,进程dump堆栈会将文件保存在data/anr/目录下,这个文件可以获取到Trace文件分析ANR,可我们没有权限读取,这个时候就要Hook掉系统Trace的write方法,替换成我们的方法,其原理采用native hook的方式,用plt_hook框架来实现的,根据不同的版本,找到不同的hook点,其关键代码如下

void hookAnrTraceWrite() {
    int apiLevel = getApiLevel();
    if (apiLevel < 19) {
        return;
    }
    if (apiLevel >= 27) {
        plt_hook("libcutils.so", "connect", (void *) my_connect, (void **) (&original_connect));
    } else {
        plt_hook("libart.so", "open", (void *) my_open, (void **) (&original_open));
    }

    if (apiLevel >= 30 || apiLevel == 25 || apiLevel ==24) {
        plt_hook("libc.so", "write", (void *) my_write, (void **) (&original_write));
    } else if (apiLevel == 29) {
        plt_hook("libbase.so", "write", (void *) my_write, (void **) (&original_write));
    } else {
        plt_hook("libart.so", "write", (void *) my_write, (void **) (&original_write));
    }
}

参考资料

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许英俊潇洒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值