为什么我的信号处理器不收效?

之前写了几篇关于Linux信号的文章,有许多小读者找到我后台留言,说对他们帮助很大。一起也有小伙伴用了我之前写的一个结构,Signal把公司的Crash上报机制给“玩坏”了。

事情是这样的,有小伙伴用了Signal,原意是想产生反常的时分进行了一次兜底重启,成果他把一切的新号都注册进去了,导致他们的反常上报直接变成了 0 native crash。当然,这个是符合Signal的机制的,由于咱们悄悄阻拦了信号的分发进程进行重启,所以信号就不会被传递到Crash SDK了。

下面咱们来看一下详细的经过,已经“始作俑者”的我终究干了什么。

常见的Crash上报

咱们都知道,常见的crash上报结构,比方xCrash,或者由xCrash衍生的各个上报Crash SDK,其实都采纳了同样一种方法上报native crash,便是经过比方sigaction函数,注册了一个信号处理器,当一场信号来暂时,比方SIGSEGV(11 内存错误相关),开端dump堆栈,之后就上报为一次crash。

为什么我的信号处理器不生效

为了不要混杂咱们接下来说的东西,咱们Signal指的是结构,而不是信号。

为什么我的信号处理器不生效
当Signal最后初始化的时分,其实会把old_action给设置为NULL,没错,咱们故意不把信号往下面传递了。

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

关于不了解的读者,咱们看一下sigaction的界说,其中第二个参数便是当时注册的新号处理函数,第三个参数便是上一次注册的新号处理函数,而这个信号处理器是能够被掩盖的,比方咱们注册两个sigaction


struct sigaction sigc;
sigc.sa_sigaction = sig_func;
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;
struct sigaction normal_sig;
normal_sig.sa_sigaction = normal_func;
sigfillset(&normal_sig.sa_mask);
normal_sig.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;
咱们注册两个sigaction
sigaction(SIGSEGV, &sigc, NULL);
sigaction(SIGSEGV, &normal_sig, NULL);

那么终究SIGSEGV来的时分,只会被normal_sig里边的信号处理器处理,由于会互相掩盖(假如后者没有保存上一个信号处理器并主动把信号传递下去的话)

能够看到,信号处理的这条“链”,它是能够被人为破坏掉的,由于旧的信号处理器能够不被保存,就算被保存了,也能够不调用。

再探sigaction

假如我是一个业务方同学,那么我肯定是笑嘻嘻的,由于咱们完全能够打断调用链重启对吧,这样虽然我对用户的体验比较差,但是我目标美观呀!作为APM的同学,就头疼了,有的时分,用户重启也想记载为一次反常,但是比方xCrash等上报结构,是无法避免信号处理链断裂的问题。

作为APM一方,咱们诉求是,最早拿到信号并进行上报处理。下面,咱们就来看,这个终究怎样做到!

咱们的标题也看到了,是再探sigaction。那么咱们再来反思一下,在jni调用的sigaction,有许多小伙伴就被误导进去了,它其实并不是Linux中的信号注册处理函数,而是一个同名的Android 体系的sigaction函数!


extern "C" int sigaction(int signal, const struct sigaction* new_action,
                         struct sigaction* old_action) {
    InitializeSignalChain();
    return __sigaction(signal, new_action, old_action, linked_sigaction);
}

惊喜嘛!它居然是一个壳!终究调用的是经过Android SignalChain机制调用真实的Linux sigaction。

SignalChain机制

SignalChain是Android特有的一种对信号处理的一种机制,咱们来看一下InitializeSignalChain做了什么

__attribute__((constructor)) static void InitializeSignalChain() {
    static std::once_flag once;
    std::call_once(once, []() {
        lookup_libc_symbol(&linked_sigaction, sigaction, "sigaction");
        lookup_libc_symbol(&linked_sigprocmask, sigprocmask, "sigprocmask");
#if defined(__BIONIC__)
        lookup_libc_symbol(&linked_sigaction64, sigaction64, "sigaction64");
        lookup_libc_symbol(&linked_sigprocmask64, sigprocmask64, "sigprocmask64");
#endif
    });
}

它其实是找到了Linux中真实的sigaction函数与sigprocmask,前者作用是为了注册信号处理器,后者是为了增加信号掩码,而咱们调用sigaction,都会在这儿被阻拦,比方Android会在进程创立的时分,统一将SIGSEGV进行了特殊处理,在FaultManager::Init方法中,意图便是为了当信号来暂时,最早回调的是体系的信号处理器art_sigsegv_handler

void FaultManager::Init(bool use_sig_chain) {
    CHECK(!initialized_);
    if (use_sig_chain) {
        sigset_t mask;
        sigfillset(&mask);
        sigdelset(&mask, SIGABRT);
        sigdelset(&mask, SIGBUS);
        sigdelset(&mask, SIGFPE);
        sigdelset(&mask, SIGILL);
        sigdelset(&mask, SIGSEGV);
        SigchainAction sa = {
                .sc_sigaction = art_sigsegv_handler,
                .sc_mask = mask,
                .sc_flags = 0UL,
        };
        AddSpecialSignalHandlerFn(SIGSEGV, &sa);

它的界说如下

static bool art_sigsegv_handler(int sig, siginfo_t* info, void* context) {
    return fault_manager.HandleSigsegvFault(sig, info, context);
}

关键的AddSpecialSignalHandlerFn函数,其实做的事情便是,榜首,保证SignalChain存在,其次便是保护一个列表,保证art_sigsegv_handler在最早处理的方位,方位0,接着便是咱们应用层调用sigaction加进去的其他handler

extern "C" void AddSpecialSignalHandlerFn(int signal, SigchainAction* sa) {
    InitializeSignalChain();
    if (signal <= 0 || signal >= _NSIG) {
        fatal("Invalid signal %d", signal);
    }
    chains其实是一个数组,最早加入的元素,也便是Chain头,便是一开端经过FaultManager Init方法加入的处理器
    chains[signal].AddSpecialHandler(sa);
    chains[signal].Claim(signal);
}

其实进程便是这样

为什么我的信号处理器不生效

到这儿,咱们就理解了,原来最早处理信号的这个需求,并不是APM特有的,咱们Android体系也需求,而这个处理的方法,便是直接调用libc里边的sigaction注册真实的信号处理函数,其他经过jni层调用的sigaction,其实被包装进了SignalChain机制,即自主保护了一条信号处理器调用链,而这个调用链无论中心的元素怎样变(即便中心断裂了),都不会影响首要处理的信号处理函数art_sigsegv_handler。

直接调用libc sigaction

完成SignalChain最重要的一点,便是咱们不直接调用Android体系的sigaction去信号处理器就能够了,而是咱们直接调用libc的sigaction,怎样做到呢?很简单,咱们之间从so找到符号调用就能够了

获取libc.so句柄
void *libc = dlopen("libc.so", RTLD_LOCAL);
if (__predict_true(NULL != libc)) {
    // sigaction64() / sigaction()
    libc_sigaction64 = (libc_sigaction64_t)dlsym(libc, "sigaction64");
    if (NULL == libc_sigaction64)
        libc_sigaction = (libc_sigaction_t)dlsym(libc, "sigaction");
    dlclose(libc);
}
struct sigaction sigc;
sigc.sa_sigaction = sig_func;
// 信号处理时,先堵塞一切的其他信号,避免搅扰正常的信号处理程序
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;
struct sigaction *ac  = calloc(1,sizeof (struct sigaction));
prev_action = ac;
libc_sigaction64(SIGSEGV, &sigc, prev_action);

咱们之间调用libc的sigaction,即便后续再调用Android的sigaction,也是先回调libc的sigaction的,由于咱们掩盖了AndroidSignalChain的信号处理器!完整代码如下

typedef int (*libc_sigaction64_t)(int, const struct sigaction64 *, struct sigaction64 *);
typedef int (*libc_sigaction_t)(int, const struct sigaction *, struct sigaction *);
static libc_sigaction64_t libc_sigaction64 = NULL;
static libc_sigaction_t libc_sigaction = NULL;
struct sigaction * prev_action;
static void sig_func(int sig_num, struct siginfo *info, void *ptr) {
    __android_log_print(ANDROID_LOG_ERROR, "hello", "sig fun ");
    prev_action ->sa_sigaction(sig_num,info,ptr);
}
static void normal_func(int sig_num, struct siginfo *info, void *ptr) {
    __android_log_print(ANDROID_LOG_ERROR, "hello", "normal_func ");
}
JNIEXPORT void JNICALL
Java_com_pika_mooner_MainActivity_test(JNIEnv *env, jobject thiz) {
    void *libc = dlopen("libc.so", RTLD_LOCAL);
    if (__predict_true(NULL != libc)) {
        // sigaction64() / sigaction()
        需求差异64位就调用sigaction64 32就调用sigaction
        libc_sigaction64 = (libc_sigaction64_t)dlsym(libc, "sigaction64");
        libc_sigaction = (libc_sigaction_t)dlsym(libc, "sigaction");
        dlclose(libc);
    }
    struct sigaction sigc;
    sigc.sa_sigaction = sig_func;
    // 信号处理时,先堵塞一切的其他信号,避免搅扰正常的信号处理程序
    sigfillset(&sigc.sa_mask);
    sigc.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;
    struct sigaction *ac  = calloc(1,sizeof (struct sigaction));
    prev_action = ac;
    这儿简单直接调用libc_sigaction64
    libc_sigaction64(SIGSEGV, &sigc, prev_action);
    struct sigaction normal_sig;
    normal_sig.sa_sigaction = normal_func;
    // 信号处理时,先堵塞一切的其他信号,避免搅扰正常的信号处理程序
    sigfillset(&normal_sig.sa_mask);
    normal_sig.sa_flags = SA_SIGINFO | SA_ONSTACK |SA_RESTART;
    sigaction(SIGSEGV, &normal_sig, NULL);
}

此刻当SIGSEGV来的时分,终究输出的是log是

为什么我的信号处理器不生效

经过咱们的改造,就从原本的流程变成了下图

为什么我的信号处理器不生效

之后咱们采纳这种方法进行APM改造,就能最早获取信号并处理了!

总结

信号作为Linux重要的一环,Android并没有完全照搬Linux的信号处理,而是在上层构建了SignalChain机制合理调度信号处理,咱们一定要注意Linux内核处理与Android体系这儿边的差异!看到这儿,还不点个赞!!!