引言
在Android开发中,理解Linux信号机制是处理Native Crash、ANR监控以及系统级异常的关键。本文结合信号处理的核心概念与Android系统的特殊实现,深入解析信号在进程间的传递逻辑、应用层与虚拟机的交互机制,以及实际开发中的应用场景。
相关android系统源码:
sigchain.cc
fault_handler.cc
一、信号基础:从事件通知到进程控制
信号是Linux内核向进程发送的异步事件通知,用于指示特定事件的发生,如硬件异常、进程间通信等。每个信号都有默认行为(如终止进程、忽略信号等),但通过信号处理器(Signal Handler)可自定义处理逻辑。
1. 信号的默认行为与自定义处理
-
常见信号示例:
-
SIGSEGV(无效内存访问):默认终止进程,常见于Native Crash。 -
SIGQUIT:Linux中用于终端退出,Android中用于触发ANR(应用无响应)。 -
SIGABRT:由abort()函数触发,默认终止进程并生成核心转储。
-
-
信号处理器接口:
通过sigaction系统调用注册自定义处理函数,支持两种模式:struct sigaction { union { sighandler_t sa_handler; // 简单处理函数(void (*)(int)) void (*sa_sigaction)(int, siginfo_t*, void*); // 带详细信息的处理函数 }; sigset_t sa_mask; // 处理信号时阻塞的信号集 int sa_flags; // 标志位(如SA_SIGINFO启用sa_sigaction) };示例:注册
SIGSEGV处理函数捕获内存访问异常:void segv_handler(int sig, siginfo_t* info, void* ucontext) { // 记录崩溃堆栈、内存地址等信息 __android_log_print(ANDROID_LOG_ERROR, "SignalHandler", "Caught SIGSEGV at address: %p", info->si_addr); } // 初始化信号处理 struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = segv_handler; sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, nullptr);
2. 信号的接收策略:忽略、终止与恢复
进程对信号的响应可分为三种:
-
忽略信号:通过设置
sa_handler = SIG_IGN丢弃信号(如忽略SIGPIPE避免管道断开时崩溃)。 -
使用默认行为:通过
sa_handler = SIG_DFL恢复系统默认处理。 - 自定义处理:通过注册处理函数实现业务逻辑(如APM监控中捕获信号并生成日志)。
二、信号处理的进阶机制
1. 信号阻塞与掩码(Signal Mask)
Linux内核为每个进程维护一个信号掩码,阻塞掩码内的信号直至显式解除。通过sigprocmask(进程级)或pthread_sigmask(线程级)操作掩码:
// 阻塞SIGBUS信号
sigset_t blockset;
sigemptyset(&blockset);
sigaddset(&blockset, SIGBUS);
sigprocmask(SIG_BLOCK, &blockset, nullptr); // 添加到当前掩码
// 解除SIGQUIT阻塞(如ANR监控线程需要监听该信号)
sigset_t unblockset;
sigemptyset(&unblockset);
sigaddset(&unblockset, SIGQUIT);
pthread_sigmask(SIG_UNBLOCK, &unblockset, &old_mask); // 线程级解阻塞
函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
-
参数
how:-
SIG_BLOCK:将set中的信号加入当前掩码。 -
SIG_UNBLOCK:从当前掩码中移除set中的信号。 -
SIG_SETMASK:直接设置当前掩码为set。
-
示例:阻塞SIGINT
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, NULL); // 阻塞SIGINT
线程级信号控制
在多线程环境中,pthread_sigmask可控制单个线程的信号掩码:
// 线程函数中解除对SIGQUIT的阻塞
void* thread_func(void* arg) {
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);
pthread_sigmask(SIG_UNBLOCK, &mask, NULL);
// 线程可以接收SIGQUIT
return NULL;
}
2. 信号发送的多样化方式
-
进程内发送:
raise(signal)向自身发送信号(如模拟异常测试)。 -
跨进程/线程发送:
-
kill(pid, signal):向指定进程或进程组发送信号。 -
tgkill(tgid, tid, signal):向线程组内特定线程发送信号(如Android中向SignalCatcher线程发送SIGQUIT获取Trace)。 -
pthread_kill(thread_id, signal): pthread接口,直接向线程发送信号。
-
3. 同步信号监听:sigwait的应用
区别于异步处理(sigaction),sigwait用于同步等待信号,适用于专门的信号处理线程(如Android的SignalCatcher):
sigset_t wait_set;
sigemptyset(&wait_set);
sigaddset(&wait_set, SIGQUIT);
int signo;
sigwait(&wait_set, &signo); // 阻塞直至收到SIGQUIT
// 处理ANR相关逻辑
三、Android系统的信号处理机制:从内核到虚拟机的拦截
Android运行在Linux内核之上,其虚拟机(如ART)通过Hook机制优先处理关键信号,形成“内核→虚拟机→应用”的三级传递链。
1. 虚拟机对信号处理的拦截
在Android系统源码的FaultManager初始化代码中,通过注册特殊信号处理器(如art_fault_handler)拦截SIGSEGV、SIGABRT等信号:
void FaultManager::Init() {
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_fault_handler,
.sc_mask = mask,
};
AddSpecialSignalHandlerFn(SIGSEGV, &sa); // 注册虚拟机级处理函数
}
2. Hook机制:改写sigaction实现信号流转
Android通过Hooksigaction系统调用,将应用层注册的信号处理器重定向到虚拟机的处理链。当信号到达时,虚拟机会先执行自身逻辑(如Java层异常处理),再决定是否传递给应用处理器:
// 自定义sigaction实现,拦截信号注册
int sigaction(int signal, const struct sigaction* new_action, struct sigaction* old_action) {
if (chains[signal].IsClaimed()) { // 虚拟机已声明处理该信号
chains[signal].SetAction(new_action); // 记录应用处理器,而非直接传递给内核
return 0;
}
return linked_sigaction(signal, new_action, old_action); // 未拦截信号交给内核处理
}
3. 典型场景:ANR与SIGQUIT的处理
当应用发生ANR时,系统会向主线程发送SIGQUIT。由于Android线程创建时默认阻塞该信号,监控线程需先通过pthread_sigmask解阻塞,再通过sigwait同步监听,最终触发堆栈收集逻辑。
四、Hook机制:动态劫持系统调用
4.1 动态符号替换
在art/sigchainlib/sigchain.cc中,ART通过动态链接库(libc)获取原生sigaction的地址,并替换为自定义实现。
关键代码解析:
// 定义原生sigaction的函数指针
static decltype(sigaction)* linked_sigaction = nullptr;
// 动态加载原生sigaction
static void InitializeSignalChain() {
void* libc = dlopen("libc.so", RTLD_LAZY);
linked_sigaction = reinterpret_cast<decltype(sigaction)*>(dlsym(libc, "sigaction"));
}
// 替换sigaction实现
extern "C" int sigaction(int signo, const struct sigaction* new_act, struct sigaction* old_act) {
InitializeSignalChain(); // 确保初始化
// 若信号被虚拟机声明,记录应用处理器
if (chains[signo].IsClaimed()) {
if (old_act != nullptr) *old_act = chains[signo].GetAction();
if (new_act != nullptr) chains[signo].SetAction(new_act);
return 0;
}
// 否则调用原生sigaction
return linked_sigaction(signo, new_act, old_act);
}
原理:
- dlopen(动态库加载器)和dlsym(符号解析器):动态获取原生sigaction的函数地址。
- 劫持逻辑:当应用调用linux内核的sigaction函数时,由于虚拟机hook内核的sigction函数,优先由虚拟机处理,未处理的信号转发给原生函数。
4.2 信号链(Signal Chain)的构建
ART通过SignalChain类管理每个信号的处理链:
class SignalChain {
public:
void AddSpecialHandler(SigchainAction* sa) {
// 将虚拟机处理器插入链表头部
for (auto& slot : special_handlers_) {
if (slot.sc_sigaction == nullptr) {
slot = *sa;
return;
}
}
fatal("Too many handlers");
}
bool IsClaimed() { return claimed_; }
private:
bool claimed_;
struct sigaction action_; // 应用层处理器
SigchainAction special_handlers_[2]; // 虚拟机处理器(最多支持2个)
};
处理流程:
- 虚拟机优先处理:遍历special_handlers_,调用注册的处理器。
- 应用层处理:若未处理,调用应用注册的sigaction。
五、实战:在APM中捕获Native Crash
-
注册信号处理器:对
SIGSEGV、SIGABRT等信号设置自定义处理函数,记录崩溃现场(内存地址、寄存器状态等)。 -
处理阻塞信号:确保监控线程不阻塞目标信号(如通过
pthread_sigmask解除拦截)。 -
与虚拟机交互:利用Android提供的接口(如
backtrace_symbols)解析堆栈,避免与虚拟机处理链冲突。
六、总结与注意事项
-
信号的双刃剑:合理使用信号处理器可增强异常监控能力,但错误忽略关键信号(如
SIGKILL)可能导致不可恢复的问题。 - Android的特殊性:虚拟机对信号的优先处理意味着应用层处理器可能滞后,需注意处理逻辑的兼容性。
- 性能与安全:信号处理函数应保持轻量,避免复杂操作导致重入问题或性能瓶颈。
理解Linux信号在Android中的底层机制,不仅能帮助开发者更好地处理Crash和ANR,还能为系统级优化提供理论支撑。通过结合sigaction、信号掩码与虚拟机Hook机制,可构建健壮的异常监控体系,提升应用的稳定性与可观测性。










网友评论