为什么我的信号处理器不生效?
之前写了几篇关于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系统这里面的区别!看到这里,还不点个赞!!!
作者:Pika
链接:https://juejin.cn/post/7248513044917682231
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。