前言

本期也是挖坑的系列之一,为什么会选信号当作本期的主题呢?其实是跟笔者的一些学习经验相关的,无论是自己写的一些尝试,还是行业内的一些常见的crash监控方案或者其他一些优秀开源,或多或少都会涉及到对linux信号的处理,因此,作为依赖于Linux内核的android体系,也离不开对Linux信号的一些处理。了解并运用一些信号相关的手法,能更好的帮助咱们进行日常的疑难杂症的处理。尽管信号自身涉及到c语言相关的知识,可是也不阻碍咱们进行学习!

信号概念与信号处理器的效果

信号自身,其实是对事件发生时对进程的一种通知机制,当然,这种机制并不是只限于Linux体系,其他许多类Unix体系也有,信号的初衷仅仅通知,比方当硬件反常时,可由硬件自身的过错检测通知到内核,再由内核告知相关的进程,又或者是,进程间可经过信号自身,去传递一些特有的消息。可是咱们在日常开发中常常能够看到,比方nativecrash是由某个信号导致的,比方常见的SIGABRT/SIGSEGV等。可是这个表述,其实是不太精确的,大部分信号(这儿特指除了SIGKILL/SIGSTOP)自身其实跟进程crash/exit,自身其实是没有必定关系的,这点需求留意,也是十分容易尝试误会的点。比方咱们说,收到了SIGSEGV就会导致进程退出,其实,这仅仅一个默许的行为,记住,是默许的行为!下面咱们列举一下常见的信号默许行为

信号 默许行为
SIGKILL 确保杀死进程,无法更改默许行为
SIGPIPE 管道相关断开,默许杀死进程
SIGSTOP 确保进程中止,无法更改默许行为,常用于debugger
SIGSEGV 无效内存引证,默许杀死进程
SIGABRT 中止进程,android上行为默许也是杀死
SIGQUIT 默许杀死进程,linux上是终端退出,而android用来发生anr

看到这儿,咱们或许会有疑问,如同各个信号其实都差不多都是杀死进程。还有便是SIGQUIT,分明android发生了anr会抛出SIGQUIT,可是也不是说进程也会被杀死呀!其实呢,上面列举了这些,都是信号自身的默许行为算了,咱们是能够改动默许行为的,比方经过体系调用signal(),或者sigaction()注册一个信号处理器,其实就会把一个信号从默许行为变成了自界说行为,假设说自界说行为里边没有进程退出的调用,比方exit,那么,及时当时进程收到了信号,也是不会溃散的。当然这儿指的是可更改默许行为的信号,除了SIGKILL与SIGSTOP,咱们都能够经过上述的信号处理调用更改,咱们举个例子

Java_com_example_signal_MainActivity_throwNativeCrash(JNIEnv *env, jobject thiz) {
    // 向自身发送一个信号
    raise(SIGABRT);
    __android_log_print(ANDROID_LOG_INFO, "hello", "%s", "qwe");
}

当咱们经过一个jni调用,调用到throwNativeCrash办法时,里边经过一个raise调用(意思是向当时进程自身发送一个信号),假设默许咱们什么也不处理,那么就会导致进程中止退出,体现的方法便是app闪退。可是,假设咱们加入了一个信号处理函数(增加信号处理函数一般有signal与sigaction)这儿咱们介绍一下sigaction

int sigaction(int __signal, const struct sigaction* __new_action, struct sigaction* __old_action);

sigaction承受3个参数,第一个是需求增加信号处理器的信号,第二个是一个sigaction的结构体的指针,用于设置当时信号的新信号处理器,第三个也是一个sigaction的结构体的指针,用于返回之前的信号处理器,假设有的话。

接着咱们介绍sigaction结构体

struct sigaction {
  union {
    sighandler_t sa_handler;
    void (*sa_sigaction)(int, struct siginfo*, void*);
  };
  sigset_t sa_mask;
  int sa_flags;
  void (*sa_restorer)(void);
};

union是c语言的一个概念,意味着sa_handler与函数sa_sigaction只取其中一个,当sa_flags包括SA_SIGINFO时,就调用sa_sigaction函数,默许便是调用sa_handler函数。

当然,本篇会尽量讲一些涉及到的体系调用,可是就不细致讲下去了,这些都或许google拿到,不理解的也能够直接谈论!

好了,回到咱们上文,假设咱们对SIGABRT设置了一个信号处理函数,即便SigFunc(收到信号要调用的函数)什么也做,进程也不会退出!

struct sigaction sigc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;
sigaction(SIGABRT, &sigc, nullptr);

这儿十分关键啦!只要咱们设置了信号处理函数,那么允许默许行为改变的信号的默许行为就会被改动,即便你的信号处理器什么也没干!

到这儿,咱们应该就能理解了,信号处理器其实便是更改信号默许行为的一种手法,也是对信号自界说处理的一些手法,这也是为什么咱们一些apm结构,会经过监听更改信号处理器后,然后经过old_action重新再把信号抛出给默许处理器的原因,因为信号处理器只能设置一个,假设不重新调用old_action,那么app默许行为就会被改动(即发生了过错也不crash)。

这儿或许给咱们埋下了一个漆黑的主意,假设咱们都把一切信号都注册一遍,就算咱们信号处理器啥都不干,是不是就实现了native crash == 0 ? 好家伙,一般情况下,还真是,可是正常的过错已经是发生了,即便咱们经过信号处理函数绕过了crash行为,可是当过错实在不可控了,体系就会发送SIGKILL/SIGSTOP的信号,而两个信号是无法经过信号处理器去进行修正的,这样形成的问题是,以后app crash了,也就无法定位出根本的堆栈,说不定会形成一个无法处理的BUG!这点是十分需求留意的!

这儿再拓宽一下,信号处理函数是支持setjump的,也便是咱们可回溯到一个安全的阶段避免一次crash,这儿不详述,能够看看笔者的mooner

信号接纳进程的行为

上面咱们说了一大堆,其实仅仅信号自身的默许行为,可是关于接纳信号的进程来说,却有着多种情况,信号你能够发,接不承受,那是进程可控的。

当信号到达后,进程可体现以下几种方法

  • 疏忽信号,内核将丢掉该信号,信号对进程不发生任何影响
  • 停止进程,也便是履行信号自身的一些,默许行为
  • 进程中止,比方debug经过信号控制
  • 恢复履行,比方经过信号恢复之前中止的进程

进程也能够经过有无设置信号处理器,将信号处理器界说为以下情况

  • 设置信号默许行为
  • 疏忽信号
  • 自界说信号处理器

下面咱们来讲一下,怎么设置信号处理器的上诉行为

自界说信号处理器

咱们上面也说过,咱们能够经过,sigaction,设置自界说的信号处理器,比方

struct sigaction sigc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;
sigaction(SIGABRT, &sigc, nullptr);

这儿SigFunc是一个函数,函数界说为

void (*sa_sigaction)(int, struct siginfo*, void*);

即可,里边就能够处理自界说的一些逻辑

void SigFunc(int sig_num, siginfo *info, void *ptr) {
 比方打log
}

默许行为

咱们能够经过SIG_DFL这个宏界说,设置信号处理器的默许行为,比方设置SIGABRT默许行为,这个宏需求放在sa_handler处理,意味着咱们就不能在sa_flags加上SA_SIGINFO标记,而是采纳默许的处理方法

struct sigaction sigc;
sigc.sa_handler = SIG_DFL;
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_RESTART;
... 
sigaction(SIGABRT, &sigc, nullptr);

疏忽信号

与默许行为类似,咱们也有一个疏忽信号的宏界说,则是SIG_IGN,咱们也能够一样设置在sa_handler中

struct sigaction sigc;
sigc.sa_handler = SIG_IGN;
sigc.sa_flags = SA_RESTART;
sigfillset(&sigc.sa_mask);
...
sigaction(SIGABRT, &sigc, nullptr);

这儿还不忘多提一句,当设置SIG_IGN,那么内核就会把该信号丢掉,这也意味值,进程丝毫不感知该信号,这也就意味着,假设发生反常了,尽管进程不会crash,可是过错足够大时同样会发生SIGKILL去停止进程,尽管疏忽信号在一些情况能够避免一些潜在的信号过错的默许处理让进程不crash,可是也给bug排查带来困难

小结

关于信号的上篇,咱们先到这儿完毕一个阶段,在android中,会对部分信号处理选用hook方法,从而不一定履行咱们用户自界说的信号处理函数,这个咱们就放在下篇讲,同时还有一些信号的阻塞行为与其他发送信号的体系调用,以及信号处理过程中的重入与坑,咱们也会在下篇进一步讲解,感谢观看!!

最终,还别忘对我的github 点上你的小星星!!
github.com/TestPlanB/m…