xCrash ANR 非主线程初始化不生效的前世今生

2,070 阅读14分钟

背景

这要从一个 bug 说起, 项目 Demo 工程该功能 work 但是集成到集成工程里该功能不生效 经过排查发现二者的差异如下 :

  1. Demo 工程 xCrash 初始化在 mian 线程。
  2. 集成工程里 xCrash 初始化却在子线程里。

方案

  1. xCrash 模块的初始化切换到主线程运行即可。

至此问题得以解决。但究其根本原因,其实还尚未可知,难道就止于此吗?
答案自然是否定的,于是带着种种疑惑与那颗对技术炽热的心进入下一篇章 「溯源」。

溯源

Android 系统中信号的处理机制

zygote 进程对信号的处理

代码位置

art/runtime/jni/java_vm_ext.cc
art/runtime/runtime.cc
art/runtime/signal_set.h

代码调用栈

JNI_CreateJavaVM // 虚拟机创建函数指针回调

      Runtime::Create 

          Runtime::Init

             BlockSignals(); // zygote 进程对信号的处理 

BlockSignals 函数
// 重点关注该函数
void Runtime::BlockSignals() {
  // SignalSet 实现细节 art/runtime/signal_set.h
  SignalSet signals;
  signals.Add(SIGPIPE);
  // SIGQUIT is used to dump the runtime's state (including stack traces).
  signals.Add(SIGQUIT);
  // SIGUSR1 is used to initiate a GC. 
  signals.Add(SIGUSR1);
  // SIG_BLOCK 阻塞上面的三个信号
  signals.Block();
}

到这里我们只能知道 zygote 进程屏蔽了 SIGPIPE,SIGQUIT,SIGUSR1 这三个信号的接收。
那么问题1来了,为什么 zygote 进程要 Block 住 SIGPIPE,SIGQUIT,SIGUSR1 这三个信号呢?莫急莫急, 带着这个问题来看看从 zygote 进程 fork 出来的子进程对信号的处理方式吧。

fork 子进程对信号的处理

代码位置

art/runtime/native/dalvik_system_ZygoteHooks.cc
art/runtime/runtime.cc
art/runtime/signal_catcher.cc

代码调用栈

dalvik_system_ZygoteHooks

    ZygoteHooks_nativePostForkChild

        Runtime->InitNonZygoteOrPostFork

           StartSignalCatcher

               new SignalCatcher();

SignalCatcher 构造函数
// 重点关注该函数
SignalCatcher::SignalCatcher()
    : lock_("SignalCatcher lock"),
      cond_("SignalCatcher::cond_", lock_),
      thread_(nullptr) {
  // 这个标记先设置成 false, 析构函数会设置成 true。   
  SetHaltFlag(false);

  // Create a raw pthread; its start routine will attach to the runtime.
  // 1. 通过 pthread_create 创建一个 Linux C 的线程 并且让虚拟机管理该线程其实就是跟 C++ 的 Thread 建立映射关系。
  // 2. Java 线程本质其实就是映射了 Linux C 的线程。对这个感兴趣可以看一下其实现,代码位置在 art/runtime/thread.cc
  // 3. 子线程对信号的处理在 Run 方法里。
  CHECK_PTHREAD_CALL(pthread_create, (&pthread_, nullptr, &Run, this), "signal catcher thread");

  Thread* self = Thread::Current();
  MutexLock mu(self, lock_);
  while (thread_ == nullptr) {
    // 内部调用 pthread_cond_wait 阻塞线程 等待 Signal Catcher 子线程初始化完成并且 Runtime::AttachCurrentThread()
    // block current thread
    cond_.Wait(self);
  }
}

SignalCatcher 线程 Run 方法
// 该方法就是 Linux C 线程的 Run 方法,不熟悉的同学可以类比 Java 线程的 run 方法。
void* SignalCatcher::Run(void* arg) {
  // arg 参数本质其实就是 SignalCatcher 的 this 指针(pthread_create最后一个参数)。
  // 这里通过 C++ reinterpret_cast来做类型转换。(不丢失精度)
  SignalCatcher* signal_catcher = reinterpret_cast<SignalCatcher*>(arg);
  CHECK(signal_catcher != nullptr);
  // Runtime 就是虚拟机的化身
  Runtime* runtime = Runtime::Current();
  // start routine will attach to the runtime
  // 该方法内部调用了 Thread::Attach() 让虚拟机管理 Linux C 创建的线程
  // 线程名 Signal Catcher 熟悉吧, 在 trace.txt 文件里见过吧。
  CHECK(runtime->AttachCurrentThread("Signal Catcher", true, runtime->GetSystemThreadGroup(),
                                     !runtime->IsAotCompiler()));
  // attach 之后返回 C++ Thread 对象 所以这里获取的就是前面创建的
  // 这里我理解是类似 Java 的 ThreadLocal 线程私有数据 (linux c pthread_getspecific)
  // 但是事实上不是 是通过汇编指令获取的???
  /**
  * Thread::Init()
  * #ifdef __BIONIC__
  * __get_tls()[TLS_SLOT_ART_THREAD_SELF] = this;
  * #else
  * CHECK_PTHREAD_CALL(pthread_setspecific, (Thread::pthread_key_self_, this), "attach self");
  * Thread::self_tls_ = this;
  * #endif
  * 汇编指令获取当前线程对象(线程私有数据类似pthread_setspecific)
  * # define __get_tls() ({ void** __val; __asm__("mrc p15, 0, %0, c13, c0, 3" : "=r"(__val)); __val; })
  */
  Thread* self = Thread::Current();
  // 设置线程的运行状态为 kRunnable
  DCHECK_NE(self->GetState(), ThreadState::kRunnable);
  {
    MutexLock mu(self, signal_catcher->lock_);
    signal_catcher->thread_ = self;
    // 唤醒等待线程 Signal Catcher 创建完成了 但是哪儿等待呢? 其构造函数 Wait()
    // Attach Thread之后, unblock current thread
    // 内部函数调用了 pthread_cond_broadcast 唤醒前面的 pthread_cond_wait
    signal_catcher->cond_.Broadcast(self);
  }

  // Set up mask with signals we want to handle.
  // 又见到了 前面 zygote 进程对信号的处理也是用这个类。
  SignalSet signals;
  signals.Add(SIGQUIT);
  signals.Add(SIGUSR1);
  // 子进程只关心了俩个信号 SIGQUIT SIGUSR1
  while (true) {
    // 子线程等待上面俩个信号的发生。否则一直阻塞 如果信号发生 则会进入后面的逻辑。
    // signals.Wait();
    // TEMP_FAILURE_RETRY(sigwait64(&set_, &signal_number));
    // #define sigwait64 sigwait
    // 其内部本质上是调用了 sigwait 等待信号的到来。
    int signal_number = signal_catcher->WaitForSignal(self, signals);
    // 这个熟悉吧 构造函数初始化 false 所以这里不调用
    if (signal_catcher->ShouldHalt()) {
      // 脱离虚拟机的管理 把 Thread 从 ThreadList里移除并且释放资源
      runtime->DetachCurrentThread();
      return nullptr;
    }
    
    switch (signal_number) {
    case SIGQUIT:
      // SIGQUIT 信号产生 则进入该逻辑 生成 trace 文件。
      // 其内部会调用 runtime->DumpForSigQuit(os);
      // xCrash 生成 trace 文件也是通过获取 DumpForSigQuit 函数的指针地址来实现的。
      signal_catcher->HandleSigQuit();
      break;
    case SIGUSR1:
      // SIGUSR1 信号发生会触发 GC
      // 内部调用了 Runtime::Current()->GetHeap()->CollectGarbage(/* clear_soft_references= */ false);
      signal_catcher->HandleSigUsr1();
      break;
    default:
      LOG(ERROR) << "Unexpected signal %d" << signal_number;
      break;
    }
  }
}

至此我们知道了 fork 子进程接收并特殊处理了 SIGQUIT,SIGUSR1 信号,但是默认屏蔽了SIGPIPE信号。
总结起来就是 zygote 进程对信号的处理漠不关心; fork 子进程对信号的处理小心翼翼。
然上面提到的问题1还没有得以解决,又有了其它问题,如下 :
问题2 : 信号在多线程环境里如何处理?
问题3 : 如果多个线程都监听的相同的信号,那该那个线程收到并处理呢?
问题4 : 子进程从 zygote 进程 fork 出来继承了 zygote 进程的信号集, 为什么 sigwait 可以监听被 Block 的信号集呢?
越看问题越多,慌了...忧愁...感伤...莫慌。其实仔细一看发现以上这些问题都是对底层 Linux 信号处理不太了解,接下来带着这样疑问进入下一篇章:「基础拾遗之 Linux POSIX 标准信号处理」。

Linux POSIX 标准信号的处理

信号基础

POSIX 信号和多线程应用
  1. 信号处理程序必须在多线程应用的所有线程之间共享,不过,每个线程都必须有自己的挂起信号掩码和阻塞信号掩码。
  2. POSIX 库函数 kill() / sigqueue() 必须向所有的多线程应用而不是某个特殊的线程发送信号。所有由内核产生的信号也是如此(SIGQUIT等)
  3. 每个发送给多线程应用的信号仅传送给一个线程,这个线程是由内核在从不会阻塞该信号的线程中随意选择出来的。
  4. 如果向多线程应用发送一个致命的信号,那么内核将杀死该应用的所有线程,而不仅仅是杀死接收信号的那个线程。

通过第三条我们解决问题2,3 : 在多线程环境下信号只能有一个线程来处理,而且这个处理线程是随机的。

与信号相关的数据结构

task_signal.png

  1. signal 信号描述符的指针
  2. sighand 信号处理程序描述符的指针「每一个进程引用一个信号处理程序描述符」
  3. 信号处理程序描述符就可以由几个进程共享。
  4. 线程组中的所有轻量级进程都应用相同的信号描述符和信号处理程序描述符。
sigaction

int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);

该函数为某一信号设置全局信号处理程序,其它线程的信号处理程序就会被覆盖。「使用完需要恢复之前的信号处理」
具体用法 可以 man sigaction

sigwait

DESCRIPTION

The sigwait() function selects a set of signals, specified by set. If none of the selected signals are pending, sigwait() waits until one or more of the selected signals has been generated.

Then sigwait() atomically clears one of the selected signals from the set of pending signals for the process and sets the location pointed to by sig to the signal number that was cleared.

The signals specified by set should be blocked, but not ignored, at the time of the call to sigwait(). Processes which call sigwait() on ignored signals will wait indefinitely.

Ignored signals are dropped immediately by the system, before delivery to a waiting process.

通过 man 手册,了解到 sigwait 监听到信号需要被 Block
至此解决了问题4 sigwait 监听 block 的信号
回顾 Signal Catcher 捕获信号的时候发现并没有 Block 信号啊?那么子进程在哪儿 Block 信号呢? 答案就是再 zygote 进程 Block, 子进程fork 继承了 zygote 进程,这样问题1得以解惑。

通过基础拾遗篇,我们解决了之前提到的1,2,3,4关于信号的问题。有了信号基础之后,我们就可以进入下一个篇章 「xCrash ANR 原理机制」来探索它为什么只能在主线程初始化?

xCrash ANR 原理机制

知识回顾

  1. zygote 进程 Block 了 SIGPIPE, SIGQUIT, SIGUSR1 三个信号。
  2. fork 子进程继承了父进程的信号集。子进程通过 Signal Catcher 线程(sigwait)监听 Block 的 SIGQUIT,SIGUSR1 信号。
  3. 在多线程的应用里,信号产生只能有一个线程来处理信号。
  4. 信号捕获处理方式:
    4.1 sigaction 设置全局信号处理程序,会保存旧的信号集,通过旧的信号集恢复旧的信号处理。
    4.2 sigwait 用于子线程监听信号的到来 并且只能监听 Block 的信号。
  5. 那么问题来了, 如果自己捕获系统 SIGQUIT 信号该怎么做呢?
  6. 方案:
    6.1 sigwait : 因为无法确定那一个子线程来处理信号,所以该方案不行。
    6.2 sigaction :
    6.2.1 拦截 Signal Catcher 线程对信号的捕获?
    6.2.2 main 线程 sigaction 如何捕获呢?
    6.2.3 non-main 线程 sigaction 如何捕获呢?

接下来看 xCrash 如何捕获 SIGQUIT 信号呢?

信号处理

xCrash 信号拦截处理
// handler 为信号处理函数
int xcc_signal_trace_register(void (*handler)(int, siginfo_t *, void *))
{
    int              r;
    sigset_t         set;
    struct sigaction act;
    // 下面的注释先解除信号的阻塞并且希望是主线程。
    //un-block the SIGQUIT mask for current thread, hope this is the main thread
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);
    // 先解除进程 Block 的信号。解决上面提到的问题6.2.1
    // Signal Catcher 子线程就无法捕获到 SIGQUIT 信号了。
    // 经过测试程序发现下面俩种方式 xCrash 均可以捕获到信号
    // 1. pthread_sigmask & sigaction 均在主线程可运行
    // 2. pthread_sigmask (mian) & sigaction (子线程) 也可以捕获到 SIGQUIT 信号 详见后面实践中的测试程序
    // 虽然上面 2 的方式可以处理信号,但是由于信号处理本身单线程就比较复杂,因为信号打断了进程的控制流,所以信号处理函数只能调用异步安全的函数,而异步信号安全更是一个很苛刻的条件。
    // 多线程条件下,加剧了这种复杂度,因为信号既可以发送给进程,也可以发送个进程的某一个线程,不同线程还可以设置掩码来实现对信号的屏蔽。
    // 在多线程环境下一般建议采用sigwait/ sigwaitinfo等方式同步处理信号,减少异步处理带来的风险和引入 bug 的可能。
    // 综上所述
    // 1. Android 系统处理信号在子线程使用了 sigwait 的方式来捕获 SIGQUIT 信号
    // 2. xCrash 则是采用单线程模式下捕获 SIGQUIT 信号(main 线程初始化信号相关操作)。
    if(0 != (r = pthread_sigmask(SIG_UNBLOCK, &set, &xcc_signal_trace_oldset))) return r;
    // 为 SIGQUIT 注册新的信号处理程序。
    //register new signal handler for SIGQUIT
    memset(&act, 0, sizeof(act));
    sigfillset(&act.sa_mask);
    act.sa_sigaction = handler;
    // 设置 SA_RESTART属性, 那么当信号处理函数返回后, 被该信号中断的系统调用将自动恢复.
    act.sa_flags = SA_RESTART | SA_SIGINFO;
    // 使用 sigaction 捕获,解决问题6.2.2
    // 并且保存旧的信号集。用于恢复之前的信号处理程序
    if(0 != sigaction(SIGQUIT, &act, &xcc_signal_trace_oldact))
    {
        pthread_sigmask(SIG_SETMASK, &xcc_signal_trace_oldset, NULL);
        return XCC_ERRNO_SYS;
    }
    return 0;
}

生成 trace 文件

开启子线程
// create thread for dump trace
// If successful, the pthread_create() function will return zero.
if(0 != (r = pthread_create(&thd, NULL, xc_trace_dumper, NULL))) {
    goto err1;
}
dump trace 文件
// dump trace fun
static void *xc_trace_dumper(void *arg)
{
    // JNIEnv 用于回调 Java 函数。
    JNIEnv         *env = NULL;
    uint64_t        data;
    uint64_t        trace_time;
    int             fd;
    struct timeval  tv;
    char            pathname[1024];
    jstring         j_pathname;
    
    (void)arg;
    
    pthread_detach(pthread_self());

    JavaVMAttachArgs attach_args = {
        .version = XC_JNI_VERSION,
        .name    = "xcrash_trace_dp",
        .group   = NULL
    };
    // 又见到了熟悉的代码, 关联 C++ Thread 对象
    if(JNI_OK != (*xc_common_vm)->AttachCurrentThread(xc_common_vm, &env, &attach_args)) goto exit;

    while(1)
    {
        // 子线程等待 SIGQUIT 信号的到来
        //block here, waiting for sigquit
        // 类似 Android Looper 的实现方案 创建 eventfd 
        // 当 SIGQUIT 信号发生时,write写入信息,这里read读取到信息就往下执行了。
        XCC_UTIL_TEMP_FAILURE_RETRY(read(xc_trace_notifier, &data, sizeof(data)));
        
        //check if process already crashed
        if(xc_common_native_crashed || xc_common_java_crashed) break;

        //trace time
        if(0 != gettimeofday(&tv, NULL)) break;
        trace_time = (uint64_t)(tv.tv_sec) * 1000 * 1000 + (uint64_t)tv.tv_usec;

        //Keep only one current trace.
        if(0 != xc_trace_logs_clean()) continue;

        //create and open log file
        if((fd = xc_common_open_trace_log(pathname, sizeof(pathname), trace_time)) < 0) continue;
        
        //write header info
        if(0 != xc_trace_write_header(fd, trace_time)) goto end;

        //write trace info from ART runtime
        if(0 != xcc_util_write_format(fd, XCC_UTIL_THREAD_SEP"Cmd line: %s\n", xc_common_process_name)) goto end;
        if(0 != xcc_util_write_str(fd, "Mode: ART DumpForSigQuit\n")) goto end;
        if(0 != xc_trace_load_symbols())
        {
            if(0 != xcc_util_write_str(fd, "Failed to load symbols.\n")) goto end;
            goto skip;
        }
        if(0 != xc_trace_check_address_valid())
        {
            if(0 != xcc_util_write_str(fd, "Failed to check runtime address.\n")) goto end;
            goto skip;
        }
        if(dup2(fd, STDERR_FILENO) < 0)
        {
            if(0 != xcc_util_write_str(fd, "Failed to duplicate FD.\n")) goto end;
            goto skip;
        }

        xc_trace_dump_status = XC_TRACE_DUMP_ON_GOING;
        if(sigsetjmp(jmpenv, 1) == 0) 
        {
            if(xc_trace_is_lollipop)
                xc_trace_libart_dbg_suspend();
            // Runtime::DumpForSigQuit(os) 生成 trace 文件。
            // 函数符号 _ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE
            // 可以通过 http://demangler.com/ 反解函数名。
            // 如何获取该函数的符号呢?
            /**
            * 启动模拟器
            * 1. adb shell 
            * 2. su 
            * 3. find | grep libart.so
            * 4. adb pull apex/com.android.art/lib64/libart.so ~/
            * 5. 配置电脑环境变量 用 ojbdump 查找函数符号
            * 6. aarch64-linux-android-objdump -Tw /local_path/libart.so
            * _ZN3art9JavaVMExt14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE
            * _ZN3art2gc4Heap14DumpForSigQuitERNSt3__113basic_ostreamIcNS2_11char_traitsIcEEEE
            * _ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE
            * 搜索函数名会有好多?
            * 可以结合http://demangler.com/来反解函数
            * 小技巧:一般函数名前面是类名
            */
            // 函数指针可以 sym 解析出函数指针地址。
            xc_trace_libart_runtime_dump(*xc_trace_libart_runtime_instance, xc_trace_libcpp_cerr);
            if(xc_trace_is_lollipop)
                xc_trace_libart_dbg_resume();
        } 
        else 
        {
            fflush(NULL);
            XCD_LOG_WARN("longjmp to skip dumping trace\n");
        }

        dup2(xc_common_fd_null, STDERR_FILENO);
                            
    skip:
        if(0 != xcc_util_write_str(fd, "\n"XCC_UTIL_THREAD_END"\n")) goto end;

        //write other info
        if(0 != xcc_util_record_logcat(fd, xc_common_process_id, xc_common_api_level, xc_trace_logcat_system_lines, xc_trace_logcat_events_lines, xc_trace_logcat_main_lines)) goto end;
        if(xc_trace_dump_fds)
            if(0 != xcc_util_record_fds(fd, xc_common_process_id)) goto end;
        if(xc_trace_dump_network_info)
            if(0 != xcc_util_record_network_info(fd, xc_common_process_id, xc_common_api_level)) goto end;
        if(0 != xcc_meminfo_record(fd, xc_common_process_id)) goto end;

    end:
        //close log file
        xc_common_close_trace_log(fd);

        //rethrow SIGQUIT to ART Signal Catcher
        // 再发送 SIGQUIT 信号给 Signal Catcher 线程处理
        if(xc_trace_rethrow && (XC_TRACE_DUMP_ART_CRASH != xc_trace_dump_status)) xc_trace_send_sigquit();
        xc_trace_dump_status = XC_TRACE_DUMP_END;

        //JNI callback
        //Do we need to implement an emergency buffer for disk exhausted?
        if(NULL == xc_trace_cb_method) continue;
        if(NULL == (j_pathname = (*env)->NewStringUTF(env, pathname))) continue;
        // trace 文件生成回调 Java 函数
        (*env)->CallStaticVoidMethod(env, xc_common_cb_class, xc_trace_cb_method, j_pathname, NULL);
        XC_JNI_IGNORE_PENDING_EXCEPTION();
        (*env)->DeleteLocalRef(env, j_pathname);
    }
    // 释放资源
    (*xc_common_vm)->DetachCurrentThread(xc_common_vm);

 exit:
    xc_trace_notifier = -1;
    close(xc_trace_notifier);
    return NULL;
}
SIGQUIT 信号恢复
sigquit rethrow art 
static void xc_trace_send_sigquit()
{
    if(xc_trace_signal_catcher_tid >= 0) {
        // 通过系统调用给 signal catcher 线程发送 SIGQUIT 信号
        syscall(SYS_tgkill, xc_common_process_id, xc_trace_signal_catcher_tid, SIGQUIT);
    }
}

任督二脉

image.png 纸上得来终觉浅,绝知此事要躬行,接下来进入下一篇章「实践」

实践

环境配置「交叉编译」

export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools:$NDK_HOME:/Users/username/project/android_toolchain/tools:/Users/username/Library/Android/sdk/ndk/21.1.6352462/toolchains/llvm/prebuilt/darwin-x86_64/bin
export CC=clang
export CXX=clang++

测试代码

//
// Created by yuqiang on 2022/2/26.
//
#define _GNU_SOURCE
#include<stdio.h>
#include<stdlib.h>
#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <signal.h>
#include <pthread.h>
#include <sys/syscall.h>

struct sigaction xcc_signal_trace_oldact;

void startSignalCatcherOneThread();

void startSignalCatcherTwoThread();

void startSigActionThread();

void SetMainThreadSigMask(int how);

void initSigActionInMain();

/**
 * 模拟系统 Signal Catcher One 线程处理 SIGQUIT 信号处理
 * @param arg  线程方法参数指针
 * @return
 */
void *SignalCatcherOneRun(void *arg) {
    printf("SignalCatcherOneRun begin...tid = %d\n", gettid());
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);
    sigaddset(&set, SIGUSR1);

    while (1) {
        int signal_number;
        printf("SignalCatcherOneRun sig_wait wait...tid = %d\n", gettid());
        int ret = sigwait(&set, &signal_number);
        if (ret != 0) {
            printf("sig_wait error ....\n");
        }
        printf("SignalCatcherOneRun handle sig_number = %d, tid = %d\n", signal_number, gettid());
    }
}

/**
 * 模拟系统 Signal Catcher Two 线程处理 SIGQUIT 信号处理
 * @param arg  线程方法参数指针
 * @return
 */
void *SignalCatcherTwoRun(void *arg) {
    printf("SignalCatcherTwoRun begin....tid = %d\n", gettid());
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);
    sigaddset(&set, SIGUSR1);

    while (1) {
        int signal_number;
        printf("SignalCatcherTwoRun sig_wait wait...tid = %d\n", gettid());
        int ret = sigwait(&set, &signal_number);
        if (ret != 0) {
            printf("SignalCatcherTwoRun sig_wait error ....\n");
        }
        printf("SignalCatcherTwoRun handle sig_number = %d tid = %d\n", signal_number, gettid());
    }
}

void SigActionHandlerInSubThread(int sig, siginfo_t *si, void *uc){
    printf("SigActionHandlerInSubThread recv sig tid = %d \n", gettid());
}

/**
 * 使用子线程用 sigaction 捕获 SIGQUIT 信号
 * @param arg 线程参数
 * @return
 */
void *SigActionThreadRun(void *arg) {
    sigset_t main_set_t;
    sigset_t old_sig_set_t;
    sigemptyset(&main_set_t);
    sigaddset(&main_set_t, SIGQUIT);
    int r = pthread_sigmask(SIG_UNBLOCK, &main_set_t, &old_sig_set_t);
    printf("SigActionThreadRun pthread_sigmask r = %d, tid = %d\n", r, gettid());

    struct sigaction act;

    memset(&act, 0, sizeof(act));
    sigfillset(&act.sa_mask);
    act.sa_sigaction = SigActionHandlerInSubThread;
    act.sa_flags = SA_RESTART | SA_SIGINFO;
    int result = sigaction(SIGQUIT, &act, &xcc_signal_trace_oldact);
    printf("SigActionThreadRun sigaction r = %d tid = %d\n", result, gettid());
    if (result != 0) {
        pthread_sigmask(SIG_SETMASK, &old_sig_set_t, NULL);
    }
}

void SigActionHandlerInMain(int sig, siginfo_t *si, void *uc){
    printf("SigActionHandlerInMain recv sig tid = %d\n", gettid());
}

int main() {
    printf("main thread tid = %d\n", gettid());
    // SIG_UNBLOCK 模拟系统 Signal Catcher 
    // SIG_BLOCK 模拟 xCrash 
    SetMainThreadSigMask(SIG_UNBLOCK);
    startSignalCatcherOneThread();
    startSignalCatcherTwoThread();
    sleep(2);
    startSigActionThread();
    // initSigActionInMain();
    while(1);
    return 0;
}

/**
 * xCrash catch SIGQUIT signal in main thread
 */
void initSigActionInMain() {
    sigset_t main_set_t;
    sigset_t old_sig_set_t;
    sigemptyset(&main_set_t);
    sigaddset(&main_set_t, SIGQUIT);
    int r = pthread_sigmask(SIG_UNBLOCK, &main_set_t, &old_sig_set_t);
    printf("main pthread_sigmask r = %d\n", r);


    struct sigaction act;

    memset(&act, 0, sizeof(act));
    sigfillset(&act.sa_mask);
    act.sa_sigaction = SigActionHandlerInMain;
    act.sa_flags = SA_RESTART | SA_SIGINFO;
    int rc = sigaction(SIGQUIT, &act, &xcc_signal_trace_oldact);
    printf("main sigaction r = %d.....old = %p\n", rc, xcc_signal_trace_oldact);
    if (rc != 0) {
        pthread_sigmask(SIG_SETMASK, &old_sig_set_t, NULL);
    }
}

/**
 * 设置 main 线程信号的 mask
 * @param how
 */
void SetMainThreadSigMask(int how) {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);
    sigaddset(&set, SIGUSR1);
    // zygote 进程 BLOCK 了 SIGQUIT SIGUSR1 信号 这里解除屏蔽
    int result = pthread_sigmask(how, &set, NULL);
    if (result != 0) {
        printf("pthread_sigmask error code = %d\n", result);
    }
}

/**
 * 开启子线程 使用
 */
void startSigActionThread() {
    pthread_t tid;
    int ret2 = pthread_create(&tid, NULL, SigActionThreadRun, NULL);
    if (ret2 != 0) {
        perror("pthread_create 2 error");
    }
}

/**
 * 开启子线程 模拟 Signal Catcher Onw 线程 (类似 Android Signal Catcher 捕获信号)
 */
void startSignalCatcherTwoThread() {
    pthread_t tid;
    int ret1 = pthread_create(&tid, NULL, SignalCatcherTwoRun, NULL);
    if (ret1 != 0) {
        perror("startSignalCatcherTwoThread pthread_create error");
    }
}

/**
 * 开启子线程 模拟 Signal Catcher Two 线程 (类似 Android Signal Catcher 捕获信号)
 */
void startSignalCatcherOneThread() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, SignalCatcherOneRun, NULL);
    if (ret != 0) {
        perror("startSignalCatcherOneThread pthread_create error");
    }
}

编译运行

// 编译源文件
aarch64-linux-android28-clang -o main main.c
// push 到模拟器上
adb push main /data/local/tmp/main
// 运行 main
./main
// 模拟信号产生
// 1. adb shell
// 2. su
// 3. top | grep main
// 4. 观察 main 进程的进程 ID
// 5. 执行 kill -3 pid

结果验证

模拟 Signal Catcher 捕获 SIGQUIT

image.png

// 大前提是 BLOCK SIGQUIT 信号
// 主线程线程 ID = 10127 
main thread tid = 10127
// 模拟 Signal Catcher 子线程 Two 线程 ID = 10129
SignalCatcherTwoRun begin....tid = 10129
SignalCatcherTwoRun sig_wait wait...tid = 10129
// 模拟 Signal Catcher 子线程 One 线程 ID = 10128
SignalCatcherOneRun begin...tid = 10128
SignalCatcherOneRun sig_wait wait...tid = 10128
// 测试 sigaction 捕获信号 主线程 BLOCK SIGQUIT 信号
SigActionThreadRun pthread_sigmask r = 0, tid = 10130
SigActionThreadRun sigaction r = 0 tid = 10130
// kill -3 pid 
// 模拟 Signal Catcher 的其中一个线程响应 SIGQUIT 的处理
// 并且验证信号在多线程环境下只有一个线程响应
SignalCatcherOneRun handle sig_number = 3, tid = 10128
SignalCatcherOneRun sig_wait wait...tid = 10128
模拟 xCrash 主线程初始化

由于 Demo 可验证捕获 SIGQUIT 信号,这里不做赘述。

模拟 xCrash 子线程初始化生效(略有改动)

image.png

// 一个修改点就是 pthread_sigmask(SIG_UNBLOCK) 在主线程运行
// 主线程线程 ID = 10095 
main thread tid = 10095
// 模拟 Signal Catcher 子线程 One 线程 ID = 10096
SignalCatcherOneRun begin...tid = 10096
SignalCatcherOneRun sig_wait wait...tid = 10096
// 模拟 Signal Catcher 子线程 Two 线程 ID = 10097
SignalCatcherTwoRun begin....tid = 10097
SignalCatcherTwoRun sig_wait wait...tid = 10097

// sigaction 设置信号掩码以及信号处理程序在子线程初始化
SigActionThreadRun pthread_sigmask r = 0, tid = 10098
SigActionThreadRun sigaction r = 0 tid = 10098
// kill -3 pid 
// 需要注意的是信号处理函数在 main 线程回调的!!!
SigActionHandlerInSubThread recv sig tid = 10095 

说明

  1. sigaction 的逻辑在子线程初始化。
  2. 信号处理程序响应是在主线程。
其它

可自行通过修改测试程序来验证信号的相关知识。

ANR 展望

Android R 之后系统支持了 ANR trace 的获取方式

获取 ANR Trace 文件新姿势

ApplicationExitInfo.getTraceInputStream()

AppExitInfoTracker

鸣谢

感谢快手 KOOM 交流群里 (薛秋实), xCrash 作者蔡克伦大佬们的指导建议。同时也要感谢高爷 Gracker 收录于微信公众号 Android Performance 中 AndroidWeekly #30 「技术文章之21」

参考

《深入理解Android:Java虚拟机ART》

《UNIX 环境高级编程》

《深入理解 LINUX 内核》

cs.android.com/android

demangler.com/

github.com/iqiyi/xCras…

blog.csdn.net/qq_40732350…