百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

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

lipiwang 2024-11-15 22:01 15 浏览 0 评论


作者:非台

背景

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所指向的函数(参数为信号类型)。有效的信号包括:

SIGABRT

异常终止,如调用abort()。

SIGFPE

算术运算出错,如除数为0或溢出。

SIGILL

非法函数映象,如非法指令。

SIGINT

交互式信号,如中断。

SIGSEGV

非法访问存储器,如访问不存在的内存单元。

SIGTERM

发送给本程序的终止请求信号。

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  //  V
1558  // Runtime::Start() (zygote):
1559  //   No-op wrt native bridge.
1560  //  |
1561  //  | start app
1562  //  V
1563  // DidForkFromZygote(action)
1564  //   action = kUnload -> dlclose native bridge.
1565  //   action = kInitialize -> initialize library
1566  //
    
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 篇移动技术实践&干货给你思考!

相关推荐

前端入门——css 网格轨道详细介绍

上篇前端入门——cssGrid网格基础知识整体大概介绍了cssgrid的基本概念及使用方法,本文将介绍创建网格容器时会发生什么?以及在网格容器上使用行、列属性如何定位元素。在本文中,将介绍:...

Islands Architecture(孤岛架构)在携程新版首页的实践

一、项目背景2022,携程PC版首页终于迎来了首次改版,完成了用户体验与技术栈的全面升级。作为与用户连接的重要入口,旧版PC首页已经陪伴携程走过了22年,承担着重要使命的同时,也遇到了很多问题:维护/...

HTML中script标签中的那些属性

HTML中的<script>标签详解在HTML中,<script>标签用于包含或引用JavaScript代码,是前端开发中不可或缺的一部分。通过合理使用<scrip...

CSS 中各种居中你真的玩明白了么

页面布局中最常见的需求就是元素或者文字居中了,但是根据场景的不同,居中也有简单到复杂各种不同的实现方式,本篇就带大家一起了解下,各种场景下,该如何使用CSS实现居中前言页面布局中最常见的需求就是元...

CSS样式更改——列表、表格和轮廓

上篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,这篇文章分享列表、表格和轮廓,一起来看看吧。1.列表List1).列表的类型<ulstyle='list-...

一文吃透 CSS Flex 布局

原文链接:一文吃透CSSFlex布局教学游戏这里有两个小游戏,可用来练习flex布局。塔防游戏送小青蛙回家Flexbox概述Flexbox布局也叫Flex布局,弹性盒子布局。它决定了...

css实现多行文本的展开收起

背景在我们写需求时可能会遇到类似于这样的多行文本展开与收起的场景:那么,如何通过纯css实现这样的效果呢?实现的难点(1)位于多行文本右下角的展开收起按钮。(2)展开和收起两种状态的切换。(3)文本...

css 垂直居中的几种实现方式

前言设计是带有主观色彩的,同样网页设计中的css一样让人摸不头脑。网上列举的实现方式一大把,或许在这里你都看到过,但既然来到这里我希望这篇能让你看有所收获,毕竟这也是前端面试的基础。实现方式备注:...

WordPress固定链接设置

WordPress设置里的最后一项就是固定链接设置,固定链接设置是决定WordPress文章及静态页面URL的重要步骤,从站点的SEO角度来讲也是。固定链接设置决定网站URL,当页面数少的时候,可以一...

面试发愁!吃透 20 道 CSS 核心题,大厂 Offer 轻松拿

前端小伙伴们,是不是一想到面试里的CSS布局题就发愁?写代码时布局总是对不齐,面试官追问兼容性就卡壳,想跳槽却总被“多列等高”“响应式布局”这些问题难住——别担心!从今天起,咱们每天拆解一...

3种CSS清除浮动的方法

今天这篇文章给大家介绍3种CSS清除浮动的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。首先,这里就不讲为什么我们要清楚浮动,反正不清除浮动事多多。下面我就讲3种常用清除浮动的...

2025 年 CSS 终于要支持强大的自定义函数了?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是CSS自定义属性CSS自...

css3属性(transform)的一个css3动画小应用

闲言碎语不多讲,咱们说说css3的transform属性:先上效果:效果说明:当鼠标移到a标签的时候,从右上角滑出二维码。实现方法:HTML代码如下:需要说明的一点是,a链接的跳转需要用javasc...

CSS基础知识(七)CSS背景

一、CSS背景属性1.背景颜色(background-color)属性值:transparent(透明的)或color(颜色)2.背景图片(background-image)属性值:none(没有)...

CSS 水平居中方式二

<divid="parent"><!--定义子级元素--><divid="child">居中布局</div>...

取消回复欢迎 发表评论: