Android 线程挂起超时崩溃与修复

2,628 阅读4分钟

背景

本年度一直在做线程相关的性能优化,例如线程收敛、线程栈优化,以及一些由线程导致的OOM问题。最近在检索崩溃大盘时,发现了一些由于线程挂起导致的Native Crash,发现此问题存在已久只不过量不是很大,属于长尾问题,就花精力研究一下,得出一些方案,就此探讨与分享一下。

堆栈分析

  • Case 1:
// Crash thread
signal:6 (SIGABRT),code:-1 (SI_QUEUE),fault addr:--------
Abort message:
Thread suspension timed out: 0x6f2e45d888:OkHttp https://dummy.global.com/...
backtrace:
// ignore more data

java stacktrace:
at dalvik.system.VMStack.getThreadStackTrace(VMStack.java)
at java.lang.Thread.getStackTrace(Thread.java:1841)
at java.lang.Thread.getAllStackTraces(Thread.java:1909)
at com.appsflyer.internal.AFa1xSDK$23740.AFInAppEventType(AFa1xSDK.java:113)
at com.appsflyer.internal.AFa1xSDK$23740.values(AFa1xSDK.java:168)
at com.appsflyer.internal.AFa1xSDK$23740.AFInAppEventParameterName(AFa1xSDK.java:73)
at com.appsflyer.internal.AFa1tSDK$28986.AFKeystoreWrapper(AFa1tSDK.java:38)
at java.lang.reflect.Method.invoke(Method.java)
at com.appsflyer.internal.AFc1oSDK.AFKeystoreWrapper(AFc1oSDK.java:159)
at com.appsflyer.internal.AFd1hSDK.values(AFd1hSDK.java:88)
at com.appsflyer.internal.AFd1oSDK.valueOf(AFd1oSDK.java:144)
at com.appsflyer.internal.AFd1zSDK.afErrorLog(AFd1zSDK.java:207)
at com.appsflyer.internal.AFc1bSDK.run(AFc1bSDK.java:4184)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487)
at java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
  • Case 2:
// Crash thread
signal:6 (SIGABRT),code:-1 (SI_QUEUE),fault addr:--------
Abort message:
Thread suspension timed out: 0x70a383f4d8:DefaultDispatcher-worker-3
backtrace:
#00 pc 00000000000896fc  /apex/com.android.runtime/lib64/bionic/libc.so (abort+180)
#01 pc 000000000076fc20  /apex/com.android.art/lib64/libart.so (art::Runtime::Abort(char const*)+904)
#02 pc 00000000000357d0  /apex/com.android.art/lib64/libbase.so (android::base::SetAborter(std::__1::function<void (char const*)>&&)::$_0::__invoke(char const*)+80)
#03 pc 0000000000034d58  /apex/com.android.art/lib64/libbase.so (android::base::LogMessage::~LogMessage()+352)
#04 pc 000000000079bac0  /apex/com.android.art/lib64/libart.so (art::ThreadSuspendByPeerWarning(art::ScopedObjectAccess&, android::base::LogSeverity, char const*, _jobject*).__uniq.215660552210357940630679712151551015321+288)
#05 pc 000000000024c838  /apex/com.android.art/lib64/libart.so (art::ThreadList::SuspendThreadByPeer(_jobject*, art::SuspendReason, bool*)+3236)
#06 pc 00000000005949e8  /apex/com.android.art/lib64/libart.so (art::Thread_setNativeName(_JNIEnv*, _jobject*, _jstring*).__uniq.300150332875289415499171563183413458937+744)
#07 pc 0000000000439460  /data/misc/apexdata/com.android.art/dalvik-cache/arm64/boot.oat (art_jni_trampoline+128)

// ingore more data

java stacktrace:
at java.lang.Thread.setNativeName(Thread.java)
at java.lang.Thread.setName(Thread.java:1383)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.setIndexInArray(CoroutineScheduler.java:588)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.tryTerminateWorker(CoroutineScheduler.java:842)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.park(CoroutineScheduler.java:800)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.tryPark(CoroutineScheduler.java:740)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.java:711)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.java:664)

上述是崩溃时 dump 出的日志,其中能看到 Java 的日志,所以相对来说触发崩溃的时机比较好分析。 总结了所有因线程挂起所导致的问题一共有两类。

  • Appsflyer VMStack.getThreadStackTrace()
  • Coroutine Thread.setName()

上述两个方法的调用分别触发了一个abort()的 Linux终止信号,所导致了 App 的崩溃,接下来我们依次分析一下触发此次abort()信号的流程。

Thread.setName()

根据上述堆栈日志我们发现,修改线程名称是由协程所触发的,我们来看一下。 首先我们先追踪一下协程在执行任务的时候在切换调度器的过程中都做了哪些事情。

备注:我们使用的是 coroutinesVersion = '1.6.4'

协程执行流程

在 Kotlin 中,协程与线程是两个不同的概念。协程在 JVM 上通过线程来执行,但它们不直接绑定到任何特定的线程上。多个协程可以在单个线程上运行,也可以灵活地在不同的线程间切换。这种设计允许协程在等待如 I/O 操作的完成时挂起,而不会阻塞其所在的线程,从而其他协程可以继续在该线程上执行。

Kotlin 中协程和线程的关系更多的是抽象层面的关联而非直接的依附关系。协程通过调度器(Dispatchers)来控制其在何种线程或线程池上执行。例如,Dispatchers.Default 是为 CPU 密集型任务准备的,默认使用共享的线程池;而 Dispatchers.IO 则优化用于 I/O 操作,同样操作共享的线程池。 在 Koltin 协程中,线程这个概念可以叫做 Worker

Worker的创建

当我们使用如下的协程代码时,我们就会创建一个 IO 调度器,用于做网络请求等等事件,此时就会触发 Worker的创建流程。

fun doSomething(){
    viewmodelScope.launch(Dispatchers.IO){
        //  do something...
    }
}

针对Dispatcher.IO调度器 & Dispatcher.Default调度器内部都使用了 CoroutineScheduler作为线程池的实现。

在协程 CoroutineScheduler中,内置对 Worker(线程)的创建。

private fun createNewWorker(): Int {
    synchronized(workers) {
        // Make sure we're not trying to resurrect terminated scheduler
        if (isTerminated) return -1
        val state = controlState.value
        val created = createdWorkers(state)
        val blocking = blockingTasks(state)
        val cpuWorkers = (created - blocking).coerceAtLeast(0)
        // Double check for overprovision
        if (cpuWorkers >= corePoolSize) return 0
        if (created >= maxPoolSize) return 0
        // start & register new worker, commit index only after successful creation
        val newIndex = createdWorkers + 1
        require(newIndex > 0 && workers[newIndex] == null)
        /*
         * 1) Claim the slot (under a lock) by the newly created worker
         * 2) Make it observable by increment created workers count
         * 3) Only then start the worker, otherwise it may miss its own creation
         */
        val worker = Worker(newIndex)
        workers.setSynchronized(newIndex, worker)
        require(newIndex == incrementCreatedWorkers())
        worker.start()
        return cpuWorkers + 1
    }
}

上述代码是计算一系列的数量判断,最终如果需要创建一个 Worker时,会初始化Worker对象,然后会调用Thread.start()

internal inner class Worker private constructor() : Thread() {
    init {
        isDaemon = true
    }

    // guarded by scheduler lock, index in workers array, 0 when not in array (terminated)
    @Volatile // volatile for push/pop operation into parkedWorkersStack
    var indexInArray = 0
        set(index) {
            name = "$schedulerName-worker-${if (index == 0) "TERMINATED" else index.toString()}"
            field = index
        }

    constructor(index: Int) : this() {
        indexInArray = index
    }

    // ignore more code...
}

ok,到了这里我们可以看到 Worker实际上就是一个线程。其中存在一个 indexInArray的成员变量,set()方法用于修改线程名称,至于这里为什么要修改线程名称请查看协程 Github 仓库 issue.

我们什么场景会频繁调用 Thread.setName()?

我们已经知道协程调度器实际上是自己做了一个线程池的逻辑,内部如何创建的线程都封装在 CoroutineScheduler类中。 此时我们再回头看一下崩溃日志,这个Thread.setName()的执行顺序是什么。

graph TD
run --> runWorker --> 
tryPark --> park --> tryTerminateWorker -->
setIndexInArray --> Thread.setName

上述流程图可以理解为当我们创建了多个 task 协程挂起事件,这个 task就会去线程池上找一个 worker,如果没有就会 创建一个 worker, 当线程池内部的自旋检查 task & worker 数量状态时,如果当前没有 task 并且 woker 数量超过了 核心工作线程的数量,那么就会回收线程,因此存在了 tryPark 的方法,去终止线程,之后当终止了线程,要同步去修改对应的 worker 的 名称,因为 worker 整体的数据结构以AtomicReferenceArray数组存在的,然后会将index的值依次减1。

修改线程名或者获取堆栈,为什么会挂起 Thread?

了解了问题的发生场景,我们看下挂起线程的原因。

art/runtime/native/java_lang_Thread.cc
static void Thread_setNativeName(JNIEnv* env, jobject peer, jstring java_name) {
  ScopedUtfChars name(env, java_name);
  {
    ScopedObjectAccess soa(env);
    if (soa.Decode<mirror::Object>(peer) == soa.Self()->GetPeer()) {
      // 1.
      soa.Self()->SetThreadName(name.c_str());
      return;
    }
  }
  // Suspend thread to avoid it from killing itself while we set its name. We don't just hold the
  // thread list lock to avoid this, as setting the thread name causes mutator to lock/unlock
  // in the DDMS send code.
  ThreadList* thread_list = Runtime::Current()->GetThreadList();
  // Take suspend thread lock to avoid races with threads trying to suspend this one.
  // 2.
  Thread* thread = thread_list->SuspendThreadByPeer(peer, SuspendReason::kInternal);
  if (thread != nullptr) {
    {
      ScopedObjectAccess soa(env);
      thread->SetThreadName(name.c_str());
    }
    bool resumed = thread_list->Resume(thread, SuspendReason::kInternal);
    DCHECK(resumed);
  }
}

上述代码是在java中线程调用Thread.setNativeName()通过JNI最终调用的native侧的代码。

  • 代码 1 处是用来判断是否是线程改自己的名称,如果是,直接修改名称即可,无需挂起。
  • 代码 2 处是如果是A线程去修改B线程的名称,则需要挂起B线程,再修改线程名。

关于为何要先挂起,源码中也存在一个注释,在多线程环境中修改线程名称涉及到线程状态的同步和管理,直接修改活动线程的名称可能会引起线程自身的状态问题或与其他线程的交互问题。因此,先暂停线程,安全地修改名称后再恢复运行,是一种保证线程安全性的必要措施。

ART虚拟机是如何挂起线程的?

线程挂起检查

接下来我们进一步看下挂起的细节,去看一下SuspendThreadByPeer()函数如何实现的。

art/runtime/thread_list.cc
static constexpr useconds_t kThreadSuspendInitialSleepUs = 0;
static constexpr useconds_t kThreadSuspendMaxYieldUs = 3000;
static constexpr useconds_t kThreadSuspendMaxSleepUs = 5000;

Thread* ThreadList::SuspendThreadByPeer(jobject peer,
                                        SuspendReason reason,
                                        bool* timed_out) {
  bool request_suspension = true; // 标志是否需要请求暂停 
  const uint64_t start_time = NanoTime(); // 记录开始时间 
  int self_suspend_count = 0; // 自暂停计数 
  useconds_t sleep_us = kThreadSuspendInitialSleepUs; // 设置初次循环的休眠时间 这里是 0
  *timed_out = false; // 超时标志 
  Thread* const self = Thread::Current(); // 获取当前线程 
  Thread* suspended_thread = nullptr; // 初始化指向将要被暂停的线程的指针
  VLOG(threads) << "SuspendThreadByPeer starting";
  while (true) {
    Thread* thread; // 用于指向找到的线程
    {
      ScopedObjectAccess soa(self); // 保证对Java对象的访问是安全的
      MutexLock thread_list_mu(self, *Locks::thread_list_lock_); // 锁定线程列表,防止并发修改
      thread = Thread::FromManagedThread(soa, peer); // 通过Java对象找到对应的本地线程
      if (thread == nullptr) {
      // 如果没有找到线程,则检查是否已经有被挂起的线程需要恢复挂起计数
        if (suspended_thread != nullptr) {
          MutexLock suspend_count_mu(self, *Locks::thread_suspend_count_lock_);
          // 重点...
          // 逆向调整挂起计数,避免死锁
          bool updated = suspended_thread->ModifySuspendCount(soa.Self(),
                                                              -1,
                                                              nullptr,
                                                              reason);
          DCHECK(updated);
        }
        // 打印警告信息,并返回空,表示没有找到对应的线程
        ThreadSuspendByPeerWarning(soa,
                                   ::android::base::WARNING,
                                    "No such thread for suspend",
                                    peer);
        return nullptr;
      }
      // 检查找到的线程是否属于当前的线程列表
      if (!Contains(thread)) {
        CHECK(suspended_thread == nullptr);
        // 如果不属于,则打印日志并返回空
        VLOG(threads) << "SuspendThreadByPeer failed for unattached thread: "
            << reinterpret_cast<void*>(thread);
        return nullptr;
      }
      VLOG(threads) << "SuspendThreadByPeer found thread: " << *thread;
      {
        MutexLock suspend_count_mu(self, *Locks::thread_suspend_count_lock_);
        if (request_suspension) {
        // 如果需要请求挂起
          if (self->GetSuspendCount() > 0) {
            // 如果当前线程已经被标记挂起状态,增加自暂停计数并跳过当前循环
            ++self_suspend_count;
            continue;
          }
          CHECK(suspended_thread == nullptr);
          // 设置被暂停的线程
          suspended_thread = thread;
          // 重点...
          // 增加该线程的挂起计数
          bool updated = suspended_thread->ModifySuspendCount(self, +1, nullptr, reason);
          DCHECK(updated);
          request_suspension = false; // 设置不再请求挂起
        } else {
          // 如果不是请求挂起,检查已经有挂起计数
          CHECK_GT(thread->GetSuspendCount(), 0);
        }
        CHECK_NE(thread, self) << "Attempt to suspend the current thread for the debugger";
        if (thread->IsSuspended()) {
          // 如果目标线程已经是挂起状态,记录日志并返回该线程
          VLOG(threads) << "SuspendThreadByPeer thread suspended: " << *thread;
          if (ATraceEnabled()) {
            std::string name;
            thread->GetThreadName(name);
            ATraceBegin(StringPrintf("SuspendThreadByPeer suspended %s for peer=%p", name.c_str(),
                                      peer).c_str());
          }
          return thread;
        }
        // 计算从开始到现在的总延迟时间
        const uint64_t total_delay = NanoTime() - start_time;
        if (total_delay >= thread_suspend_timeout_ns_) 
          // 如果超时,则根据是否已经有挂起的线程分别处理
          if (suspended_thread == nullptr) {
            ThreadSuspendByPeerWarning(soa,
                                       ::android::base::FATAL,
                                       "Failed to issue suspend request",
                                       peer);
          } else {
            CHECK_EQ(suspended_thread, thread);
            LOG(WARNING) << "Suspended thread state_and_flags: "
                         << suspended_thread->StateAndFlagsAsHexString()
                         << ", self_suspend_count = " << self_suspend_count;
            // 记录超时警告 并发送 abort()信号终止进程。
            Locks::thread_suspend_count_lock_->Unlock(self);
            ThreadSuspendByPeerWarning(soa,
                                       ::android::base::FATAL,
                                       "Thread suspension timed out",
                                       peer);
          }
          // 标记代码不可达
          UNREACHABLE();
        } else if (sleep_us == 0 &&
            total_delay > static_cast<uint64_t>(kThreadSuspendMaxYieldUs) * 1000) 
            // 如果未设置休眠时间且延迟超过最大允许的自旋时间,设置休眠时间
          sleep_us = kThreadSuspendMaxYieldUs / 2;
        }
      }
    }
    VLOG(threads) << "SuspendThreadByPeer waiting to allow thread chance to suspend";
    // 休眠一定时间,以允许线程为机会进入挂起状态
    ThreadSuspendSleep(sleep_us);
    // 调整休眠时间,但不超过最大值
    sleep_us = std::min(sleep_us * 2, kThreadSuspendMaxSleepUs);
  }
}

上述代码每一行都加了注释,逻辑比较好理解,核心就是:

自旋等待 + 挂起标记

自旋等待特别像 Handler + Looper机制,都是采用死循环+休眠(挂起)的方式。 休眠的方式使用的是 ThreadSuspendSleep(sleep_us)来休眠的。

static void ThreadSuspendSleep(useconds_t delay_us) {
  if (delay_us == 0) {
    sched_yield(); // 如果延迟时间为0,则调用sched_yield()函数让出当前线程的CPU时间片给其他线程
  } else {
    usleep(delay_us); // 如果延迟时间不为0,则调用usleep函数使当前线程暂停执行指定的微秒数
  }
}
  • sched_yield()
#include <sched.h>
#include "syscall.h"
int sched_yield()
{
	return syscall(SYS_sched_yield);
}

让调度器放弃当前线程的剩余时间片,但它不会改变线程的状态,让出当前的CPU,立即给其他线程使用,当前的线程仍然保持在就绪状态。

syscall是一个在Linux和其他类UNIX操作系统中常见的低级函数,用于直接从用户空间发起系统调用。

  • usleep(delay_us)
#include <time.h>
#include "syscall.h"
int nanosleep(const struct timespec *req, struct timespec *rem)
{
  return syscall_cp(SYS_nanosleep, req, rem);
}

这个睡眠方式会改变线程状态,也会让出 CPU

为什么不总是使用usleep()?
  • 资源利用和响应速度: sched_yield()可以提高系统的响应速度和资源利用率。它允许当前线程主动让出CPU,但又不脱离就绪状态,这意味着一旦有执行机会,它可以立即继续执行。这在高并发环境下非常有用,可以减少等待时间和提高系统吞吐量。
  • 避免不必要的延迟: 使用usleep()意味着即使系统中没有其他线程需要运行,当前线程也必须等待指定的时间才能继续执行,这可能导致不必要的延迟。

挂起标记位

上述挂起检查中的代码,有两处重点代码都指向了同一个函数。

suspended_thread->ModifySuspendCount(self, +1, nullptr, reason);

这个suspend_thread对应的就是Thread.cc。我们去看一下:

bool Thread::ModifySuspendCountInternal(Thread* self,
                                        int delta,
                                        AtomicInteger* suspend_barrier,
                                        SuspendReason reason) {
  // 检查delta值是否合法,只能为-1或+1
  if (kIsDebugBuild) {
    DCHECK(delta == -1 || delta == +1)
          << reason << " " << delta << " " << this;
    // 确认当前线程持有线程挂起计数锁
    Locks::thread_suspend_count_lock_->AssertHeld(self);
    // 如果当前线程不是自己,并且不是处于挂起状态,确认持有线程列表锁
    if (this != self && !IsSuspended()) {
      Locks::thread_list_lock_->AssertHeld(self);
    }
  }
  // 如果挂起原因是用户代码调用,需要特别检查
  if (UNLIKELY(reason == SuspendReason::kForUserCode)) {
    // 确认持有用户代码挂起锁
    Locks::user_code_suspension_lock_->AssertHeld(self);
    // 检查挂起计数修改是否合法(不能使挂起计数变为负数)
    if (UNLIKELY(delta + tls32_.user_code_suspend_count < 0)) {
      LOG(ERROR) << "attempting to modify suspend count in an illegal way.";
      return false;
    }
  }
  // 如果减少挂起计数时已经为0或更小,记录错误并返回false
  if (UNLIKELY(delta < 0 && tls32_.suspend_count <= 0)) {
    UnsafeLogFatalForSuspendCount(self, this);
    return false;
  }

  // 如果增加挂起计数,并且当前线程不是自己,并且存在flip函数,则返回false以避免死锁
  if (delta > 0 && this != self && tlsPtr_.flip_function != nullptr) {
    return false;
  }

  uint32_t flags = enum_cast<uint32_t>(ThreadFlag::kSuspendRequest);
  // 如果增加挂起计数并指定了挂起屏障
  if (delta > 0 && suspend_barrier != nullptr) {
    uint32_t available_barrier = kMaxSuspendBarriers;
    // 查找可用的挂起屏障位置
    for (uint32_t i = 0; i < kMaxSuspendBarriers; ++i) {
      if (tlsPtr_.active_suspend_barriers[i] == nullptr) {
        available_barrier = i;
        break;
      }
    }
    // 如果没有可用的挂起屏障位置,返回false
    if (available_barrier == kMaxSuspendBarriers) {
      return false;
    }
    // 设置挂起屏障
    tlsPtr_.active_suspend_barriers[available_barrier] = suspend_barrier;
    flags |= enum_cast<uint32_t>(ThreadFlag::kActiveSuspendBarrier);
  }

  // 更新线程的挂起计数
  tls32_.suspend_count += delta;
  switch (reason) {
    case SuspendReason::kForUserCode:
      // 如果原因是用户代码,更新用户代码挂起计数
      tls32_.user_code_suspend_count += delta;
      break;
    case SuspendReason::kInternal:
      // 如果原因是内部原因,则不需要特别操作
      break;
  }

  // 如果挂起计数为0,清除挂起请求标志
  if (tls32_.suspend_count == 0) {
    AtomicClearFlag(ThreadFlag::kSuspendRequest);
  } else {
    // 如果挂起计数不为0,设置挂起请求和可能的挂起屏障标志
    tls32_.state_and_flags.fetch_or(flags, std::memory_order_seq_cst);
    TriggerSuspend();
  }
  return true;  // 返回成功
}

ModifySuspendCount()函数最终会执行ModifySuspendCountInternal(),核心在于设置挂起屏障的代码,实际上就是给 tlsPtr_ 设置一个挂起点,当 suspend_count > 0 说明当前线程需要被挂起,但是仅仅只是设置了一个标记而已,是不是非常像handler 机制中的同步屏障

什么时候执行的挂起?

这里不得不说到 Android 检查点机制,还记得我们GC流程吗? 举个例子, 当我们执行了 System.GC,一定会触发GC吗?面试老手肯定知道不是,一定要等待所有的线程都到了安全点的时候才会触发GC, 那么触发 GC 的时候需要进行 Stop the World(当然ART采用并发GC,无需所有线程都暂停),这个流程其实也涉及检查点(check point)机制。 由于不偏离本文,我们暂且可以理解为

每个线程都会定期检查自己是否有挂起请求,是否存在一个挂起标记位(kSuspendRequest),如果存在则挂起

这部分代码也比较复杂,后续会单独写一篇文章解释这里。

总结一下,我们只是将线程自身加入一个标记位,然后等待自身执行到了检查点后,检查这个标记位,如果是 kSuspendRequest,则触发挂起。

这里补充一下,真正执行挂起的代码流程,这里可能导致知识点不是很连贯,但是先写出来。

art/runtime/base/mutex.cc
void ConditionVariable::WaitHoldingLocks(Thread* self) {
  DCHECK(self == nullptr || self == Thread::Current());  // 断言:传入的线程对象要么为空,要么为当前线程
  guard_.AssertExclusiveHeld(self);  // 断言:当前线程必须独占持有锁
  unsigned int old_recursion_count = guard_.recursion_count_;  // 保存当前的递归锁计数

#if ART_USE_FUTEXES  // 如果使用futexes进行线程同步
  num_waiters_++;  // 等待者数量加一
  guard_.increment_contenders();  // 增加争用者的计数,以确保解锁时可以唤醒线程
  guard_.recursion_count_ = 1;  // 设置递归锁计数为1
  int32_t cur_sequence = sequence_.load(std::memory_order_relaxed);  // 获取当前的序列号,用于futex操作
  guard_.ExclusiveUnlock(self);  // 释放锁,以便其他线程可以进入临界区

  // FUTEX_WAIT_PRIVATE:等待条件变量,只对当前进程内部的线程可见
  if (futex(sequence_.Address(), FUTEX_WAIT_PRIVATE, cur_sequence, nullptr, nullptr, 0) != 0) {
    // 如果futex调用失败
    if ((errno != EINTR) && (errno != EAGAIN)) {  // 如果错误既不是中断也不是无法立即阻塞
      PLOG(FATAL) << "futex wait failed for " << name_;  // 记录致命错误日志
    }
  }
  SleepIfRuntimeDeleted(self);  // 检查运行时是否已删除,如果是,则使线程休眠
  guard_.ExclusiveLock(self);  // 重新获得锁
  CHECK_GT(num_waiters_, 0);  // 检查等待者计数是否大于0
  num_waiters_--;  // 等待者数量减一
  CHECK_GT(guard_.get_contenders(), 0);  // 检查争用者计数是否大于0
  guard_.decrement_contenders();  // 减少争用者计数

#else  // 如果不使用futexes,使用传统的pthread条件变量
  pid_t old_owner = guard_.GetExclusiveOwnerTid();  // 获取当前持有锁的线程ID
  guard_.exclusive_owner_.store(0 /* pid */, std::memory_order_relaxed);  // 清除持有者
  guard_.recursion_count_ = 0;  // 清零递归锁计数
  CHECK_MUTEX_CALL(pthread_cond_wait, (&cond_, &guard_.mutex_));  // 等待pthread条件变量
  guard_.exclusive_owner_.store(old_owner, std::memory_order_relaxed);  // 恢复持有者
#endif
  guard_.recursion_count_ = old_recursion_count;  // 恢复原来的递归锁计数
}

挂起超时原因

ok, 我们已经知道我们虽然执行了ModifySuspendCount()函数,但是还没有真正执行挂起的操作,等到检查点检测到 KSuspendRequest标记的时候,才会真正的执行挂起,而超时就是因为检查点执行超时。

因为这些检测触发的时机通常是在不会影响程序状态的位置,如方法调用、循环迭代末尾或返回之前,可能存在这些位置迟迟没有执行到导致检查点检测被推迟。

如何修复崩溃?

由于超时了,系统会打一个日志:

ThreadSuspendByPeerWarning(soa, ::android::base::FATAL, "Thread suspension timed out", peer);

static void ThreadSuspendByPeerWarning(ScopedObjectAccess& soa,
                                       LogSeverity severity,
                                       const char* message,
                                       jobject peer) REQUIRES_SHARED(Locks::mutator_lock_) {
  ObjPtr<mirror::Object> name =
      WellKnownClasses::java_lang_Thread_name->GetObject(soa.Decode<mirror::Object>(peer));
  if (name == nullptr) {
    LOG(severity) << message << ": " << peer;
  } else {
    LOG(severity) << message << ": " << peer << ":" << name->AsString()->ToModifiedUtf8();
  }
}

这个日志级别是::android::base::FATAL,最终会发射一个abort(),使得进程终止。

由于不可能一个一个去检查崩溃线程为什么推迟检查点,所以只能找些其他办法,所以最后的方案就是直接 hook 这个 ThreadSuspendByPeerWarning() 函数,调用前将LogSeverity的级别从 FATAL 改为 INFO 或者 warning.

示例代码

sys_stub.h

#define SUSPEND_LOG_MSG "Thread suspension timed out"

enum LogSeverity {
    VERBOSE,
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL_WITHOUT_ABORT,  // For loggability tests, this is considered identical to FATAL.
    FATAL,
};


LogSeverity ToLogSeverity(int logLevel);

const char* getThreadSuspendByPeerWarningFunctionName();

sys_stub.cpp

#include <jni.h>
#include "sys_stub.h"

// Function signatures updated for readability
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_14 "_ZN3artL26ThreadSuspendByPeerWarningERNS_18ScopedObjectAccessEN7android4base11LogSeverityEPKcP8_jobject"
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_8_13 "_ZN3artL26ThreadSuspendByPeerWarningEPNS_6ThreadEN7android4base11LogSeverityEPKcP8_jobject"
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_6_7 "_ZN3artL26ThreadSuspendByPeerWarningEPNS_6ThreadENS_11LogSeverityEPKcP8_jobject"
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_5 "_ZN3artL26ThreadSuspendByPeerWarningEPNS_6ThreadEiPKcP8_jobject"


LogSeverity ToLogSeverity(int logLevel) {
    switch (logLevel) {
        case 0:
            return VERBOSE;
        case 1:
            return DEBUG;
        case 2:
            return INFO;
        case 3:
            return WARNING;
        case 4:
            return ERROR;
        case 5:
            return FATAL_WITHOUT_ABORT;
        case 6:
            return FATAL;
        default:
            return INFO;
    }
}

const char *getThreadSuspendByPeerWarningFunctionName() {
    int apiLevel = android_get_device_api_level();
    // Simplified logic based on Android API levels
    if (apiLevel < 23){
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_5;
    } else if (apiLevel < 26) {
        // below android 8
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_6_7;
    } else if (apiLevel < 34) {
        // above android 8 and below android 14
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_8_13;
    } else {
        // android 14+
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_14;
    }
}

com_thread_suspend_hook.cpp

#include <jni.h>
#include <string>
#include <shadowhook.h> // 字节shadowhook的头文件,用于在运行时钩子(hook)函数
#include <android/log.h>
#include <pthread.h>
#include "sys_stub.h"
#include <android/api-level.h>

#define TARGET_ART_LIB "libart.so"
#define LOG_TAG "thread_suspend_hook"

namespace hookThreadSuspendAbort {
    JavaVM *gVm = nullptr; // 全局的Java虚拟机指针
    jobject callbackObj = nullptr; // 全局引用,指向Java层的回调对象

    std::atomic<LogSeverity> m_severity{INFO}; // 日志严重性级别的原子变量,默认为INFO

    void *originalFunction = nullptr; // 指向原始函数的指针
    void *stubFunction = nullptr; // 指向存根函数的指针

    typedef void (*ThreadSuspendByPeerWarning)(void *self, LogSeverity severity,
                                               const char *message, jobject peer); // 函数指针类型定义

    void triggerSuspendTimeout();

    JNIEnv *getJNIEnv(); // 获取JNIEnv的函数声明

    void hookPointFailed(const char *msg); // 钩子设置失败时的处理函数

    void cleanup(JNIEnv *env); // 清理资源的函数

    // Hook 函数实现,替换原始函数
    void threadSuspendByPeerWarning(void *self, LogSeverity severity, const char *message,
                                    jobject peer) {
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hooked point success : %s", message);
        if (severity == FATAL && strcmp(message, SUSPEND_LOG_MSG) == 0) {
            // 如果当前是 FATAL 并且 message 是 Thread suspend timeout 则设置一个非FATAL级别的。
            severity = m_severity.load();
            triggerSuspendTimeout();
        }
        ((ThreadSuspendByPeerWarning) originalFunction)(self, severity, message, peer);
    }

    void maskThreadSuspendTimeout(void *self, LogSeverity severity, const char *message, jobject peer) {
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hooked point success : %s", message);
        if (severity == FATAL && strcmp(message, SUSPEND_LOG_MSG) == 0) {
            // 如果当前是 FATAL 并且 message 是 Thread suspend timeout 则不调用原始函数
            triggerSuspendTimeout();
        }
    }

    void setLogLevel(LogSeverity severity) {
        m_severity.store(severity);
    }

    void releaseHook(); // 释放钩子的函数

    void prepareSetSuspendTimeoutLevel() { // 准备设置挂起超时级别的函数
        releaseHook();
        stubFunction = shadowhook_hook_sym_name(TARGET_ART_LIB,
                                                getThreadSuspendByPeerWarningFunctionName(),
                                                (void *) threadSuspendByPeerWarning,
                                                (void **) &originalFunction);
        if (stubFunction == nullptr) {
            const int err_num = shadowhook_get_errno();
            const char *errMsg = shadowhook_to_errmsg(err_num);
            if (errMsg == nullptr || callbackObj == nullptr) {
                return;
            }
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup failed: %s", errMsg);
            hookPointFailed(errMsg);
            delete errMsg;
        } else {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup success");
        }
    }

    void preparedMaskThreadTimeoutAbort() {
        releaseHook();
        stubFunction = shadowhook_hook_sym_name(TARGET_ART_LIB,
                                                getThreadSuspendByPeerWarningFunctionName(),
                                                (void *) maskThreadSuspendTimeout,
                                                (void **) &originalFunction);
        if (stubFunction == nullptr) {
            const int err_num = shadowhook_get_errno();
            const char *errMsg = shadowhook_to_errmsg(err_num);
            if (errMsg == nullptr || callbackObj == nullptr) {
                return;
            }
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup failed: %s", errMsg);
            hookPointFailed(errMsg);
            delete errMsg;
        } else {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup success");
        }
    }

    void releaseHook() { 
        // 实现释放钩子的功能
        if (stubFunction != nullptr) {
            shadowhook_unhook(stubFunction);
            stubFunction = nullptr;
        }
    }

    void cleanup(JNIEnv *env) { 
        // 清理全局引用和分离线程
        if (callbackObj) {
            env->DeleteGlobalRef(callbackObj);
            callbackObj = nullptr;
        }
        if (gVm->DetachCurrentThread() != JNI_OK) {
            __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "Could not detach current thread.");
        }
    }

    JNIEnv *getJNIEnv() { 
        // 实现获取JNIEnv指针的功能
        JNIEnv *env = nullptr;
        if (gVm == nullptr) {
            return nullptr;
        }
        jint result = gVm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
        if (result == JNI_EDETACHED) {
            if (gVm->AttachCurrentThread(&env, nullptr) != 0) {
                return nullptr;
            }
        } else if (result != JNI_OK) {
            return nullptr;
        }
        return env;
    }

    void hookPointFailed(const char *errMsg) { 
        // 处理钩子设置失败的情况
        JNIEnv *pEnv = getJNIEnv();
        if (pEnv == nullptr) {
            return;
        }
        jclass jThreadHookClass = pEnv->FindClass(
                "com/thread_hook/ThreadSuspendTimeoutCallback");
        if (jThreadHookClass != nullptr) {
            jmethodID jMethodId = pEnv->GetMethodID(jThreadHookClass, "onError",
                                                    "(Ljava/lang/String;)V");
            if (jMethodId != nullptr) {
                pEnv->CallVoidMethod(callbackObj, jMethodId, pEnv->NewStringUTF(errMsg));
            }
        }
        cleanup(pEnv);
    }

    void triggerSuspendTimeout() { 
        // 触发挂起超时处理
        JNIEnv *pEnv = getJNIEnv();
        if (pEnv == nullptr) {
            return;
        }
        jclass jThreadHookClass = pEnv->FindClass(
                "com/thread_hook/ThreadSuspendTimeoutCallback");
        if (jThreadHookClass != nullptr) {
            jmethodID jMethodId = pEnv->GetMethodID(jThreadHookClass, "triggerSuspendTimeout",
                                                    "()V");
            if (jMethodId != nullptr) {
                pEnv->CallVoidMethod(callbackObj, jMethodId);
            }
        }
    }
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { 
    // JNI入口点,初始化JavaVM指针
    using namespace hookThreadSuspendAbort;
    gVm = vm;
    return JNI_VERSION_1_6;
}

extern "C" JNIEXPORT void JNICALL
Java_com_thread_1hook_ThreadHook_setNativeThreadSuspendTimeoutLogLevel(JNIEnv *env,
                                                                                   jobject,
                                                                                   int logLevel,
                                                                                   jobject callback) {
    using namespace hookThreadSuspendAbort;
    if (callbackObj != nullptr) {
        env->DeleteGlobalRef(callbackObj);
    }
    callbackObj = env->NewGlobalRef(callback);
    setLogLevel(ToLogSeverity(logLevel)); // 设置日志级别
    prepareSetSuspendTimeoutLevel();
}


extern "C" JNIEXPORT void JNICALL
Java_com_thread_1hook_ThreadHook_maskNativeThreadSuspendTimeoutAbort(JNIEnv *env,
                                                                                 jobject /*this*/,
                                                                                 jobject callback) {
    using namespace hookThreadSuspendAbort;
    if (callbackObj != nullptr) {
        env->DeleteGlobalRef(callbackObj);
    }
    callbackObj = env->NewGlobalRef(callback);
    preparedMaskThreadTimeoutAbort();
}

比较复杂的是,多版本兼容问题,钩子函数的mangling name有变化,需要多适配测试一下。

关于如何查找到对应的mangling name可以使用 readelf -Ws 命令去找,这里就不详细说明了。

->readelf -Ws libart_android_5_1.so | grep ThreadSuspendByPeerWarning

如何测试生效?

由于本身这个问题不好复现,我们只能采取通过mock代码在某个时机去直接执行ThreadSuspendByPeerWarning()函数。

#include <jni.h>
#include <shadowhook.h>
#include <dlfcn.h>
#include <android/log.h>
#include "sys_stub.h"

#define TARGET_ART_LIB "libart.so"
#define LOG_TAG "suspend_hook_test"

namespace suspend_hook_test {


    typedef void (*ThreadSuspendByPeerWarning)(void *self,
                                               enum LogSeverity severity,
                                               const char *message,
                                               jobject peer);


    extern "C" JNIEXPORT
    void JNICALL
    Java_com_thread_1hook_ThreadHook_callNativeThreadSuspendTimeout(JNIEnv *env,
                                                                                jobject javaThread /* this */,
                                                                                jlong nativePeer,
                                                                                jobject peer) {
        void *handle = shadowhook_dlopen(TARGET_ART_LIB);
        auto hookPointFunc = (ThreadSuspendByPeerWarning) shadowhook_dlsym(handle,
                                                                           getThreadSuspendByPeerWarningFunctionName());
        if (hookPointFunc != nullptr) {
            void *child_thread = reinterpret_cast<void *>(nativePeer);
            // only 14 worked for test.
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "thread_point : %p", child_thread);
            hookPointFunc(child_thread, FATAL, SUSPEND_LOG_MSG, peer);
        } else {
            __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "ELF symbol not found!");
        }
    }
}

如上述代码,dlsym 去拿到句柄,直接执行对应的函数。这里有个注意:

在应用侧触发 mock 函数的时候,需要先通过反射拿到 Thread 中的 nativePeer,它对应的是 nativeThread.cc 的地址。

object Utils {
    fun getNativePeer(thread: Thread): Long? {
        try {
            val threadClass = Class.forName("java.lang.Thread")
            val nativePeerField: Field = threadClass.getDeclaredField("nativePeer")
            nativePeerField.isAccessible = true
            return nativePeerField.getLong(thread)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
        return null
    }
}
thread {
    myThread = thread(name = "EdisonLi-init-name") {
        callThreadSuspendTimeout(myThread!!)
        while (true) {
            // Log.d("EdisonLi",  this@SecondActivity.myThread?.name.toString())
        }
    }
    while (true) {
        Thread.sleep(1000)
        myThread?.name = "Thread-${Random.nextLong(1, 1000)}"
        break
    }
}

并且 callThreadSuspendTimeout(myThread!!) 一定要让被修改名称的线程去调用哦!不然会报错。 ok,经测试在Android14中,执行这个函数以后,不会被 abort()信号终止进程。

至于其他版本,这个方法无法被调用,原因是

image.png

第一个native侧的thread指针需要直接拿到。

其他复现方案

img_v3_02an_0468cabf-5473-4721-83ca-99f1ee3b52eg.jpg

我们可以 hook FromManagedThread()这个函数,在代理函数中睡眠 5 秒左右,之后后续的超时检测就会检测到超时,并触发ThreadSuspendByPeerWarning()的函数了。

img_v3_02an_c6e362f5-c859-495d-874d-ada8596c340g.jpg

img_v3_02an_8de7c5db-6172-4ee5-bcca-5d6c51a75ecg.jpg

所以也可以证明方案的有效性。

预期风险

说明一下,我们如果 hook了 ThreadSuspendByPeerWarning函数,不让他打印::android::base::FATAL, 从而使得进程退出。这里有两个 case。

  • Android 6-12版本中,会直接打断自旋返回一个空指针,返回给调用方本应该是挂起后的线程,但是返回的是 nullptr.
  • Android 12.1 - 14版本中,不会 return nullptr.

ok, 先说明一下,如果 SuspendThreadByPeer 函数返回挂起后的线程如果是空指针,那么在所有的版本中都有空判断,如果为空则打印一个 log。 image.png 因此,这里可以归纳为是否成功挂起线程

  • Case A: 结束自旋检测并且返回一个 nullptr.
  • Case B: 不影响自旋并且一直等待挂起线程成功.
Android Version678.x910111212.11314
现象AAAAAAABBB
是否挂起成功?
是否 避免崩溃(abort)?

所以,目前看,我们需要考虑两点

  • 挂起失败导致 setName 或者 VMStack.getThreadStackTrace()返回给 Java 是空对象。 通过 Kotlin 中的代码来看仅仅影响 debug 获取当前协程所挂靠的线程名称,暂时无影响。

  • 以及 如果继续自旋等待挂起,是否会导致 ANR ?

    由于setName 或者 VMStack.getThreadStackTrace() 调用挂起操作的时候会判断是不是自己挂起自己,如果是这样的话就不会触发挂起检测自旋,只有两个线程之间修改对方的名称才会触发挂起检测自旋,那么存在一种情况主线程去修改子线程的名称或者调用VMStack.getThreadStackTrace(),如果超时时间过长可能会ANR,不过总比crash强一些吧。 综上,由于本身这种挂起超时的次数并不是很多,所以出现上述概率也不大。

总结

目前使用上述自测流程是没有问题的,目前还在测试阶段,提前分享是让大家一起思考一下这种方案的可行性,如果你有更好的方案或者上述有任何问题,请指教!非常感谢。

虽然当前的解决方案可以减少由线程挂起导致的Native Crash,但仍需要进一步研究一下线程和协程的管理策略(不知道是否存在使用姿势问题>_<),以彻底解决问题并提高系统的稳定性和性能。

通过这次深入分析,我们不仅解决了一个长期存在的问题,还增强了对Android底层线程管理机制的理解,这将有助于未来更好地处理类似问题。