写点什么

「ANR」Android SIGQUIT(3) 信号拦截与处理

发布于: 刚刚
「ANR」Android SIGQUIT(3) 信号拦截与处理


作者:非台

背景

Android 的 ANR 频次(Application Not Responding)一直是 Android 用户体验的重要指标,然而在 Android 6.0+的设备上,由于设备 anr 目录权限的收敛,已经不能通过扫描/data/anr/traces.txt 文件来获取 ANR 文件了,因此今天我们来简单聊聊获取 ANR 的另一种方式,Android 环境下,信号 SIGQUIT(3)拦截。

信号量处理

关于信号 SIGQUIT 的拦截,我们需要了解信号量处理的部分相关函数,kill、signal、sigaction、sigwait、pthread_sigmask 等系统信号量处理相关函数是阅读本文的必备知识,因此在这一章节简单介绍下,更多系统函数知识,请阅读《UNIX 环境高级编程》。

kill [1]

头文件:#include<signal.h>


定义函数:int kill(pid_t pid,int signo)


函数说明:kill 函数可以对进程发送 signal,Android AMS 在发生 ANR 的是其实是通过 Process.sendSignal(pid,signal)来通信的,Process.sendSignal 方法在 JNI 层,其实调用的是 kill


想详细了解 ANR 的同学可以看


  • AppErrors.java:http://androidxref.com/9.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/am/AppErrors.java

  • android_util_Process.cpp:http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/jni/android_util_Process.cpp

signal [2]

头文件:#include<signal.h>


定义函数:sig_t signal(int signum,sig_t handler);


函数说明:signal()用于确定以后当信号 sig 出现时的处理方法。如果 handler 的值是 SIG_DFL,那么就采用实现定义的缺省行为;如果 handler 的值是 SIG_IGN,那么就忽略该信号;否则,调用 handler 所指向的函数(参数为信号类型)。有效的信号包括:



signal()返回信号 sig 原来的 handler;如果出错,则返回 SIG_ERR。当随后出现信号 sig 时,就中断正在执行的操作,转而执行信号处理函数(*handler)(sig)。如果从信号处理程序中返回,则从中断的位置继续执行。

sigaction [3]

头文件:#include<signal.h>


定义函数:int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact)


函数说明:sigaction 会依参数 signum 指定的信号编号来设置该信号的处理函数。参数 signum 可以指定 SIGKILL 和 SIGSTOP 以外的所有信号。如参数结构 sigaction 定义如下:


struct sigaction {    void (*sa_handler)(int);    void (*sa_sigaction)(int, siginfo_t *, void *);    sigset_t sa_mask;    int sa_flags;    void (*sa_restorer)(void);};
复制代码


代码 1 sigaction 结构体


信号处理函数可以采用 void (*sa_handler)(int)或 void (*sa_sigaction)(int, siginfo_t *, void *)。到底采用哪个要看 sa_flags 中是否设置了 SA_SIGINFO 位,如果设置了就采用 void (*sa_sigaction)(int, siginfo_t *, void *),此时可以向处理函数发送附加信息;默认情况下采用 void (*sa_handler)(int),此时只能向处理函数发送信号的数值。


  • sa_handler:此参数和 signal()的参数 handler 相同,代表新的信号处理函数,其他意义请参考 signal();

  • sa_mask:用来设置在处理该信号时暂时将 sa_mask 指定的信号集搁置;

  • sa_restorer:此参数没有使用;

  • sa_flags :用来设置信号处理的其他相关操作,下列的数值可用。sa_flags 还可以设置其他标志:

  • SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值 SIG_DFL

  • SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用

  • SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置 SA_NODEFER 标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。

sigwait [4]

头文件:#include<signal.h>


定义函数:int sigwait(const sigset_t *set, int *sig) ;


函数说明:sigwait 提供了一种等待信号的到来,以串行的方式从信号队列中取出信号进行处理的机制。sigwait 只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 sigwait 继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:


  1. 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL;

  2. 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS;

  3. 确保 sigwait 等待的信号集已经被进程中所有的线程阻塞;

  4. 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号;

  5. 因为 sigwait 使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。


注:Android 的“Signal Catcher”线程是通过 sigwait 来等待 SIGQUIT 信号。

pthread_sigmask [5]

头文件:#include<signal.h>


定义函数:int pthread_sigmask (int how,const sigset_t *set,sigset_t *oset);


函数说明:每个线程均有自己的信号屏蔽集(信号掩码),可以使用 pthread_sigmask 函数来屏蔽某个线程对某些信号的响应处理,仅留下需要处理该信号的线程来处理指定的信号。实现方式是:利用线程信号屏蔽集的继承关系(在主线程中对 sigmask 进行设置后,主线程创建出来的线程将继承主线程的掩码)。

signal、sigaction、sigwait 的差异

signal、sigaction、sigwait 这三个方法都可以接收信号,并处理,那么他们的差异有哪些,以及处理顺序如何?


sigaction 和 signal 的区别:内核里有 signal 系统调用函数,它注释里也说是为了向后兼容,功能已被 sigaction 取代了,详见《源码剖析 signal 和 sigaction 的区别》[6],也就是可以理解为 signal 的能力是 sigaction 的子集,signal 和 sigaction 最终都是调了系统调用 rt_sigaction。


sigaction 和 sigwait 的区别: 如果多个线程在 sigwait 调用时,等待的是同一个信号,当信号递送的时候,只有一个线程可以从 sigwait 中返回,具体是那个线程则是未定义的(由系统决定)。如果信号被捕获(进程通过使用 sigaction 建立了一个信号处理程序),而且线程正在 sigwait 调用中等待同一信号,那么这时将由操作系统实现来决定以何种方式递送信号。在这种情况下,操作系统实现可以让 sigwait 返回,也可以激活信号处理程序,但不可能出现两者皆可的情况。


我们做了一个实验,如果信号量发送给目标线程,且目标现在存在 sigwait,则执行 sigwait;如果信号量发送给目标线程,且目标线程设置 SIG_BLOCK(屏蔽信息),其他线程在 sigwait,则执行 sigwait;其他状态执行 sigaction 的行为—— 这个只在笔者的 MAC 电脑上实验的结论。


使用 sigwait 的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后安排专用线程作信号处理。这些专用线程可以进行函数调用,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程环境,而非传统的信号处理程序,传统信号处理程序通常会中断线程的正常执行。详见《Libev 源码分析 06》[7]。

Android 系统 ANR SIGQUIT(3)的处理

有了前面的前置知识点,我们来看,Android ANR 信号机制是怎么做的呢?简单的说:目标进程在创建的时候,会启动一个“Signal Catcher”专项线程来处理信号量,AMS 在弹对话框的同时,会有一个系统调用,发出 SIGQUIT(3)信号量,“Signal Catcher”专门来处理 SIGQUIT(3)信号量,从而 dump 目标进程的线程状态到/data/anr/traces.txt 文件。

SignalCatcher 线程创建

当 Android 运行应用时,如果应用进程还没有创建,ActivityManagerService 会请求 Zygote fork 进程(详见《Android 应用进程的创建过程》[8])最终会通过 Runtime 创建“SignalCatcher”线程。


1550  // Look for a native bridge.1551  //1552  // The intended flow here is, in the case of a running system:1553  //1554  // Runtime::Init() (zygote):1555  //   LoadNativeBridge -> dlopen from cmd line parameter.1556  //  |1557  //  V1558  // Runtime::Start() (zygote):1559  //   No-op wrt native bridge.1560  //  |1561  //  | start app1562  //  V1563  // DidForkFromZygote(action)1564  //   action = kUnload -> dlclose native bridge.1565  //   action = kInitialize -> initialize library1566  //    bool Runtime::Init(RuntimeArgumentMap&& runtime_options_in) {1110  // (b/30160149): protect subprocesses from modifications to LD_LIBRARY_PATH, etc.1111  // Take a snapshot of the environment at the time the runtime was created, for use by Exec, etc.1112  env_snapshot_.TakeSnapshot();    ......      // 这行代码非常重要,这里后面会解释为什么SIGQUIT信号量,我们拿不到的原因。1359  BlockSignals();1360  InitPlatformSignalHandlers();1361  ......    1408  std::string error_msg;1409  java_vm_ = JavaVMExt::Create(this, runtime_options, &error_msg);1410  if (java_vm_.get() == nullptr) {1411    LOG(ERROR) << "Could not initialize JavaVMExt: " << error_msg;1412    return false;1413  }    ......1623  return true;1624}
复制代码


代码 2 Runtime::Init 方法


1905 void Runtime::BlockSignals() {1906  SignalSet signals;1907  signals.Add(SIGPIPE);1908  // SIGQUIT is used to dump the runtime's state (including stack traces).1909  signals.Add(SIGQUIT);1910  // SIGUSR1 is used to initiate a GC.1911  signals.Add(SIGUSR1);      //将SIGPIPE、SIGQUIT、SIGUSER1加入目标信号集,底层还是调用了 pthread_sigmask 函数,详见附件1912  signals.Block();1913}
复制代码


代码 3 Runtime::BlockSignals 方法


代码 2、代码 3 是 Runtime 初始化信号量的方法,这里通过 pthread_sigmask(SignalSet 内部实现,有兴趣的小伙伴可以看 signal_set.h)屏蔽了 SIGPIPE、SIGQUIT、SIGUSER1 信号量,使得当前线程(主线程)不会去处理系统发送的 SIGPIPE、SIGQUIT、SIGUSER1 信号量、由其他线程去处理。


由于 Zogyte 在 fork 子进程时,子进程会继承父进程的信号集,因此子进程创建的主线程,以及主线程创建的子线程都会继承这信号集,导致 fork 的子进程的主线程以及其子线程都不会处理 SIGPIPE、SIGQUIT、SIGUSER1 信号,只能通过 sigwait 来处理。

SignalCatcher 原理

[->signal_catcher.cc]void* SignalCatcher::Run(void* arg) {  ...

// Set up mask with signals we want to handle. // part1 拦截SIGQUIT和SIGUSR1 信号 // 这里创建了一个SignalSet对象 SignalSet signals; //将SIGQUIT、SIGUSER1加入目标信号集,底层还是调用了 sigaddset 函数,详见附件 signals.Add(SIGQUIT); signals.Add(SIGUSR1);

while (true) { //等待目标信号SIGQUIT、SIGUSR1发生,底层还是调用了 sigwait 函数,详见附件 int signal_number = signal_catcher->WaitForSignal(self, signals); if (signal_catcher->ShouldHalt()) { runtime->DetachCurrentThread(); return nullptr; }

switch (signal_number) { // part2 处理 SIGQUIT 信号 case SIGQUIT: signal_catcher->HandleSigQuit(); break; case SIGUSR1: signal_catcher->HandleSigUsr1(); break; default: LOG(ERROR) << "Unexpected signal %d" << signal_number; break; } }}
复制代码


代码 4 SignalCatcher::Run 方法


代码 4 SignalCatcher::Run 方法,正如前面所说,Android 系统确实通过了“SignalCatcher”线程通过 pthread_sigmask 和 sigwait 来专项处理 SIGQUIT、SIGUSR1 的信号量。Android 系统如此设计,我的理解是为了保障 SIGQUIT、SIGUSR1 的处理一定由“SignalCatcher”线程来完成,我认为这里由两个优点:


  1. 防止信号量被其他线程处理,确保 ANR 文件内容的正确性;

  2. 由单独线程处理,可以防止对其他线程的堆栈破坏,保障线程堆栈的完整性。

Android 自定义 SIG_QUIT 拦截的实现

// 去监听 SIGQUIT 信号量void RunSigQuitMonitor() {    sigset_t set, old_set;    sigemptyset(&set);    sigaddset(&set, SIGQUIT);

/* * 这里需要调用SIG_UNBLOCK,因为目标进程被Zogyte fork出来的时候,主线程继承了 * Zogyte的主线程的信号屏蔽关系,Zogyte主线程在初始化的时候,通过 * pthread_sigmask SIG_BLOCK把SIGQUIT的信号给屏蔽了,因此我们需要在自己进程的主线程, * 设置pthread_sigmask SIG_UNBLOCK ,这会导致原来的SignalCatcher sigwait将失效, * 原因是SignalCatcher 线程会对SIGQUIT 信号处理 */ int r = pthread_sigmask(SIG_UNBLOCK, &set, &old_set); if (0 != r) return;

... ... struct sigaction sa; memset(&sa, 0, sizeof(sa)); sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGQUIT);

sa.sa_sigaction = SignalHandler; sa.sa_flags = SA_ONSTACK | SA_SIGINFO;

if (sigaction(SIGQUIT, &sa, &old_handler) != 0) { goto Failed; }

return;

Failed: //如果失败需要恢复原先的状态,保障原来的工作能继续完成(主要是SignalCatcher) pthread_sigmask(SIG_SETMASK, &old_set, NULL);}
复制代码


代码 5 SIG_QUIT 拦截自定义 SigPad::RunSigQuitMonitor 方法


代码 5 SigPad::RunSigQuitMonitor 方法,有了 Linux 信号的前置知识以及对 Android “SignalCatcher”初始化的介绍,我们可以通过 pthread_sigmask 设置 SIG_UNBLOCK 来解除当前进程主线程对 SIGQUIT 的屏蔽。再通过 sa_sigaction 对 SIGQUIT 信号量处理方法重定向,从而实现自己的 ANR 监控方法。

小结

以上,有了对 Android SIGQUIT 信号处理的了解,我们就可以快速实现 Android 自定义的 SIGQUIT 信号拦截器,也欢迎广大读者朋友留言交流。

引用

[1] https://baike.baidu.com/item/kill()/2680256


[2] https://baike.baidu.com/item/signal.h/7316160?fr=aladdin


[3] https://baike.baidu.com/item/sigaction


[4] https://baike.baidu.com/item/sigwait


[5] https://baike.baidu.com/item/pthread_sigmask


[6] 源码剖析 signal 和 sigaction 的区别:https://blog.csdn.net/wangzuxi/article/details/44814825


[7] Libev 源码分析 06:https://www.cnblogs.com/gqtcgq/p/7247097.html


[8] Android 应用进程的创建过程:https://www.jianshu.com/p/b4cb8608d7fb


关注我们,每周 3 篇移动技术实践 &干货给你思考!

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

还未添加个人签名 2018.07.07 加入

阿里巴巴移动&终端技术官方账号。

评论

发布
暂无评论
「ANR」Android SIGQUIT(3) 信号拦截与处理