本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

前言

“死锁”,这个从接触程序开发的时分就会经常听到的词,它其实也能够被称为一种“艺术”,即互斥资源访问循环的艺术,在Android中,假如主线程发生死锁,那么通常会以ANR结束app的生命周期,假如是两个子线程的死锁,那么就会白白浪费cpu的调度资源,一起也不那么简单被发现,就像一颗“肿瘤”,永久藏在app中。当然,本篇介绍的是业界常见的死锁监控手法,一起也期望经过死锁,去发掘更加底层的常识,一起让咱们更加了解一些常用的监控手法。

咱们很简单模拟一个死锁操作,比方

val lock1 = Object()
val lock2 = Object()
Thread ({
    synchronized(lock1){
        Thread.sleep(2000)
        synchronized(lock2){
        }
    }
},"thread222").start()
Thread ({
    synchronized(lock2) {
        Thread.sleep(1000)
        synchronized(lock1) {
        }
    }
},"thread111").start()

由于thread111跟thread222都一起持有着对方想要的临界资源(互斥资源),因而这两个线程都处在互相等待对方的状况。

死锁检测

咱们怎么判别死锁:是否存在一个线程所持有的锁被另一个线程所持有,一起另一个线程也持有该线程所需求的锁,因而咱们需求知道以下信息才干进行死锁剖析:

  1. 线程所要获取的锁是什么
  2. 该锁被什么线程所持有
  3. 是否发生循环依赖的约束(本篇就不触及了,由于咱们知道了前两个就能够自行剖析了)

线程Block状况

经过咱们对synchronized的了解,当线程屡次获取不到锁的时分,此刻线程就会进入失望锁状况,因而线程就会尝试进入堵塞状况,防止进一步的cpu资源耗费,因而此刻两个线程都会处于block 堵塞的状况,咱们就能知道,处于被block状况的线程就有或许发生死锁(只是有或许),咱们能够经过遍历一切线程,检查是否处于block状况,来进行死锁判别的第一步

val threads = getAllThread()
threads.forEach {
    if(it?.isAlive == true && it.state == Thread.State.BLOCKED){
       进入死锁判别
    }
}

获取一切线程

private fun getAllThread():Array<Thread?>{
    val threadGroup = Thread.currentThread().threadGroup;
    val total = Thread.activeCount()
    val array = arrayOfNulls<Thread>(total)
    threadGroup?.enumerate(array)
    return array
}

经过对线程的判别,咱们能够扫除大部分非死锁的线程,那么下一步咱们要怎么做呢?假如线程发生了死锁,那么必定具有一个已经持有的互斥资源并且不开释才有或许造成死锁对不对!那么咱们下一步,便是要检测当时线程所持有的锁,假如两个线程一起持有对方所需求的锁,那么就会发生死锁

获取当时线程所恳求的锁

尽管咱们在java层没有相关的api提供给咱们获取线程当时想要恳求的锁,可是在咱们的native层,却能够轻松做到,由于它在art中得到更多的支持。

ObjPtr<mirror::Object> Monitor::GetContendedMonitor(Thread* thread) {
    // This is used to implement JDWP's ThreadReference.CurrentContendedMonitor, and has a bizarre
    // definition of contended that includes a monitor a thread is trying to enter...
    ObjPtr<mirror::Object> result = thread->GetMonitorEnterObject();
    if (result == nullptr) {
        // ...but also a monitor that the thread is waiting on.
        MutexLock mu(Thread::Current(), *thread->GetWaitMutex());
        Monitor* monitor = thread->GetWaitMonitor();
        if (monitor != nullptr) {
            result = monitor->GetObject();
        }
    }
    return result;
}

其间第一步尝试着经过thread->GetMonitorEnterObject()去拿

mirror::Object* GetMonitorEnterObject() const REQUIRES_SHARED(Locks::mutator_lock_) {
        return tlsPtr_.monitor_enter_object;
}

其间tlsPtr_ 其实便是art虚拟机中对于线程ThreadLocal的代表,即代表着只归于线程的本地目标,会先尝试从这儿拿,拿不到的话经过Thread类中的wait_mutex_目标去拿

Mutex* GetWaitMutex() const LOCK_RETURNED(wait_mutex_) {
        return wait_mutex_;
}

GetContendedMonitor 提供了一个办法查询当时线程想要的锁目标,这个锁目标以ObjPtrmirror::Object目标表明,其间mirror::Object类型是art中相对应于java层的Object类的代表,咱们了解一下即可。看到这儿咱们或许还有一个疑问,这个Thread* thread的入参是什么呢?(其实是nativePeer,下文咱们会了解)

咱们有办法能够查询到线程当时恳求的锁,那么这个锁被谁持有呢?只有处理这两个问题,咱们才干进行死锁的判别对不对,咱们持续往下

经过锁获取当时持有的线程

咱们还记得上文中回来的锁目标是以ObjPtrmirror::Object表明的,当然,art中相同提供了办法,让咱们经过这个锁目标去查询当时是哪个线程持有

uint32_t Monitor::GetLockOwnerThreadId(ObjPtr<mirror::Object> obj) {
    DCHECK(obj != nullptr);
    LockWord lock_word = obj->GetLockWord(true);
    switch (lock_word.GetState()) {
        case LockWord::kHashCode:
            // Fall-through.
        case LockWord::kUnlocked:
            return ThreadList::kInvalidThreadId;
        case LockWord::kThinLocked:
            return lock_word.ThinLockOwner();
        case LockWord::kFatLocked: {
            Monitor* mon = lock_word.FatLockMonitor();
            return mon->GetOwnerThreadId();
        }
        default: {
            LOG(FATAL) << "Unreachable";
            UNREACHABLE();
        }
    }
}

这儿函数比较简单,假如当时调用正常,那么履行的便是LockWord::kFatLocked,回来的是native层的Thread的tid,终究是以uint32_t类型表明

线程发动

咱们来看一下native层主线程的发动,它跟着art虚拟机的发动随即发动,咱们都知道java层的线程其实在没有跟操作体系的线程绑定的时分,它只能算是一块内存!只需经过与native线程绑定后,这时的Thread才干真正具有线程调度的能力,下面咱们以主线程发动举比方:

thread.cc
void Thread::FinishStartup() {
Runtime* runtime = Runtime::Current();
CHECK(runtime->IsStarted());
// Finish attaching the main thread.
ScopedObjectAccess soa(Thread::Current());
// 这儿是要害,为什么主线程称为“main线程”的原因
soa.Self()->CreatePeer("main", false, runtime->GetMainThreadGroup());
soa.Self()->AssertNoPendingException();
runtime->RunRootClinits(soa.Self());
soa.Self()->NotifyThreadGroup(soa, runtime->GetMainThreadGroup());
soa.Self()->AssertNoPendingException();
}

能够看到,为什么主线程被称为“主线程”,是由于在art虚拟机发动的时分,经过CreatePeer函数,创建的名称是“main”,CreatePeer是native线程中非常重要的存在,一切线程创建都经过它,这个函数有点长,笔者这儿做了删减


void Thread::CreatePeer(const char* name, bool as_daemon, jobject thread_group) {
    Runtime* runtime = Runtime::Current();
    CHECK(runtime->IsStarted());
    JNIEnv* env = tlsPtr_.jni_env;
    if (thread_group == nullptr) {
        thread_group = runtime->GetMainThreadGroup();
    }
    // 设置了线程名字
    ScopedLocalRef<jobject> thread_name(env, env->NewStringUTF(name));
    // Add missing null check in case of OOM b/18297817
    if (name != nullptr && thread_name.get() == nullptr) {
        CHECK(IsExceptionPending());
        return;
    }
    // 设置Thread的各种特点
    jint thread_priority = GetNativePriority();
    jboolean thread_is_daemon = as_daemon;
    // 创建了一个java层的Thread目标,名字叫做peer
    ScopedLocalRef<jobject> peer(env, env->AllocObject(WellKnownClasses::java_lang_Thread));
    if (peer.get() == nullptr) {
        CHECK(IsExceptionPending());
        return;
    }
    {
        ScopedObjectAccess soa(this);
        tlsPtr_.opeer = soa.Decode<mirror::Object>(peer.get()).Ptr();
    }
    env->CallNonvirtualVoidMethod(peer.get(),
                                  WellKnownClasses::java_lang_Thread,
                                  WellKnownClasses::java_lang_Thread_init,
                                  thread_group, thread_name.get(), thread_priority, thread_is_daemon);
    if (IsExceptionPending()) {
        return;
    }
    // 看到这儿,非常要害,self 指向了当时native Thread目标 self->Thread
    Thread* self = this;
    DCHECK_EQ(self, Thread::Current());
    env->SetLongField(peer.get(),
                      WellKnownClasses::java_lang_Thread_nativePeer,
                      reinterpret_cast64<jlong>(self));
    ScopedObjectAccess soa(self);
    StackHandleScope<1> hs(self);
   ....
}

这儿其实便是一次jni调用,把java中的Thread 的nativePeer 进行了赋值,而赋值的内容,正是经过了这个调用SetLongField

env->SetLongField(peer.get(),
                      WellKnownClasses::java_lang_Thread_nativePeer,
                      reinterpret_cast64<jlong>(self));

这儿咱们简单了解一下SetLongField,假如进行过jni开发的同学应该能过了解,其实便是把peer.get()得到的目标(其实便是java层的Thread目标)的nativePeer特点,赋值为了self(native层的Thread目标的指针),并强转化为了jlong类型。咱们接下来回到java层

Thread.java
private volatile long nativePeer;

说了一大堆,那么这个nativePeer究竟是个什么?经过上面的代码剖析,咱们能够了解了,Thread.java中的nativePeer便是一个指针,它所指向的内容正是native层中的Thread

Andoird性能优化 - 死锁监控与其背后的小知识

nativePeer 与 native Thread tid 与java Thread tid

经过了上面一段落,咱们了解了nativePeer,那么咱们持续比照一下java层Thread tid 与native层Thread tid。咱们经过在kotlin/java中,调用Thread目标的id特点,其实得到的是这个

private long tid;

它的生成办法如下

/* Set thread ID */
tid = nextThreadID();

private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}

能够看到,尽管它的确能代表一个java层中Thread的标识,可是生成其实能够看到,他也仅仅是一个普通的累积id生成,一起也并没有在native层中被当作仅有标识进行运用。

而native Thread 的 tid特点,才是真正的线程id

Andoird性能优化 - 死锁监控与其背后的小知识
在art中,经过GetTid获取

pid_t GetTid() const {
    return tls32_.tid;
}

一起咱们也能够注意到,tid 是保存在 tls32_结构体中,并且其坐落Thread目标的最初,从内存散布上看,tid坐落state_and_flagssuspend_countthink_lock_thread_id之后,还记得咱们上面说过的nativePeer嘛?咱们一直着重native是Thread的指针目标

Andoird性能优化 - 死锁监控与其背后的小知识
因而咱们能够经过指针的偏移,然后算出nativePeer到tid的换算公式,即nativePeer指针向下偏移三位就找到了tid(由于state_and_flags,state_and_flags,think_lock_thread_id都是int类型,那么对应的指针也便是int * )这儿有点绕,由于触及指针的内容

int *pInt = reinterpret_cast<int *>(native_peer);
//地址 +3,得到tid
pInt = pInt + 3;
return *pInt;

nativePeer目标由于就在java层,咱们很简单经过反射就能拿到

val nativePeer = Thread::class.java.getDeclaredField("nativePeer")
nativePeer.isAccessible = true
val currentNativePeer = nativePeer.get(it)

这儿咱们经过nativePeer换算成tid能够写成一个jni办法

external fun nativePeer2Threadid(nativePeer:Long):Int

完成便是

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_signal_MainActivity_nativePeer2Threadid(JNIEnv *env, jobject thiz,
                                                         jlong native_peer) {
    if (native_peer != 0) {
            //long 强转 int
            int *pInt = reinterpret_cast<int *>(native_peer);
            //地址 +3,得到 native id
            pInt = pInt + 3;
            return *pInt;
        }
    }
}

dlsym与调用

咱们上面总算把死锁能触及到的点都讲完,比方如何获取线程所恳求的锁,当时锁又被那个线程持有,如何经过nativePeer获取Thread id 做了剖析,可是还有一个点咱们还没能处理,便是如何调用这些函数。咱们需求调用的是GetContendedMonitor,GetLockOwnerThreadId,这个时分dlsym体系调用就出来了,咱们能够经过dlsym 进行调用咱们想要调用的函数

void* dlsym(void* __handle, const char* __symbol);

这儿的symbol是什么呢?其实咱们一切的elf(so也是一种elf文件)的一切调用函数都会生成一个符号,代表着这个函数,它在elf的.text中。而咱们android中,就会经过加载so的方式加载体系库,加载的体系库libart.so里面就包含着咱们想要调用的函数GetContendedMonitor,GetLockOwnerThreadId的符号

咱们能够经过objdump -t libart.so 检查符号

Andoird性能优化 - 死锁监控与其背后的小知识

这儿咱们直接给出来各个符号,读者能够直接用objdump检查符号

GetContendedMonitor 对应的符号是

_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE

GetLockOwnerThreadId 对应的符号

sdk <= 29
_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE

>29是这个
_ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE

体系约束

然后到这儿,咱们还是没能完结调用,由于dlsym等dl系列的体系调用,由于从Android 7.0开端,Android体系开端阻挠App中直接运用dlopen(), dlsym()等函数打开体系动态库,好家伙!谷歌大兄弟为了安全的考虑,做了很多约束。可是这个防君子不防程序员,业界仍旧有很多绕过体系的约束的办法,咱们看一下dlsym

__attribute__((__weak__))
void* dlsym(void* handle, const char* symbol) {
    const void* caller_addr = __builtin_return_address(0);
    return __loader_dlsym(handle, symbol, caller_addr);
}

__builtin_return_address是Linux一个内建函数(通常由编译器增加),__builtin_return_address(0)用于回来当时函数的回来地址。

在__loader_dlsym 会进行回来地址的校验,假如此刻回来地址不是归于体系库的地址,那么调用就不成功,这也是art虚拟机保护手法,因而咱们很简单就得出一个主意,咱们是不是能够用体系的某个函数去调用dlsym,然后把成果给到咱们自己的函数消费就能够了?是的,业界已经有很多这个方案了,比方ndk_dlopen

咱们拿arm架构进行剖析,arm架构中LR寄存器便是保存了当时函数的回来地址,那么咱们是不是在调用dlsym时能够经过汇编代码直接修改LR寄存器的地址为某个体系库的函数地址就能够了?嗯!是的,可是咱们还需求把本来的LR地址给保存起来,否则就没办法还原本来的调用了。

Andoird性能优化 - 死锁监控与其背后的小知识
这儿咱们拿ndk_dlopen的完成举比方

if (SDK_INT <= 0) {
    char sdk[PROP_VALUE_MAX];
    __system_property_get("ro.build.version.sdk", sdk);
    SDK_INT = atoi(sdk);
    LOGI("SDK_INT = %d", SDK_INT);
    if (SDK_INT >= 24) {
        static __attribute__((__aligned__(PAGE_SIZE))) uint8_t __insns[PAGE_SIZE];
        STUBS.generic_stub = __insns;
        mprotect(__insns, sizeof(__insns), PROT_READ | PROT_WRITE | PROT_EXEC);
        // we are currently hijacking "FatalError" as a fake system-call trampoline
        uintptr_t pv = (uintptr_t)(*env)->FatalError;
        uintptr_t pu = (pv | (PAGE_SIZE - 1)) + 1u;
        uintptr_t pd = (pv & ~(PAGE_SIZE - 1));
        mprotect((void *)pd, pv + 8u >= pu ? PAGE_SIZE * 2u : PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC);
        quick_on_stack_back = (void *)pv;
  // arm架构汇编完成
#elif defined(__arm__)
            // r0~r3
            /*
             0x0000000000000000:     08 E0 2D E5     str lr, [sp, #-8]!
             0x0000000000000004:     02 E0 A0 E1     mov lr, r2
             0x0000000000000008:     13 FF 2F E1     bx r3
            */
            memcpy(__insns, "\x08\xE0\x2D\xE5\x02\xE0\xA0\xE1\x13\xFF\x2F\xE1", 12);
            if ((pv & 1u) != 0u) { // Thumb
                /*
                 0x0000000000000000:     0C BC   pop {r2, r3}
                 0x0000000000000002:     10 47   bx r2
                */
                memcpy((void *)(pv - 1), "\x0C\xBC\x10\x47", 4);
            } else {
                /*
                 0x0000000000000000:     0C 00 BD E8     pop {r2, r3}
                 0x0000000000000004:     12 FF 2F E1     bx r2
                */
                memcpy(quick_on_stack_back, "\x0C\x00\xBD\xE8\x12\xFF\x2F\xE1", 8);
            } //if

其间咱们拿(*env)->FatalError作为了混杂体系调用的stub,咱们参照着流程图去了解上述代码:

  • 02 E0 A0 E1 mov lr, r2 把r2寄存器的内容放到了lr寄存器,这个r2存的东西便是FatalError的地址
  • 0x0000000000000008: 13 FF 2F E1 bx r3 ,经过bx指令调转,就能够正常履行咱们的dlsym了,r3便是咱们自己的dlsym的地址
  • 0x0000000000000000: 0C 00 BD E8 pop {r2, r3} 调用完r3寄存器的办法把r2寄存器放到调用栈下,提供给后边的履行进行消费
  • 0x0000000000000004: 12 FF 2F E1 bx r2 ,最终就回到了咱们的r2,完结了一次调用

总归,咱们想要做到dl系列的调用,便是想尽办法去修改对应架构的函数回来地址的数值。

死锁检测一切代码


const char *get_lock_owner_symbol_name() {
    if (SDK_INT <= 29) {
        return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE";
    } else {
        return "_ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE";
    }
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_signal_MyHandler_deadLockMonitor(JNIEnv *env, jobject thiz,
                                                  jlong native_thread) {
    //1、初始化
    ndk_init(env);
    //2、打开动态库libart.so
    void *so_addr = ndk_dlopen("libart.so", RTLD_NOLOAD);
    void * get_contended_monitor = ndk_dlsym(so_addr, "_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE");
    void * get_lock_owner_thread = ndk_dlsym(so_addr, get_lock_owner_symbol_name());
    int monitor_thread_id = 0;
    if (get_contended_monitor != nullptr && get_lock_owner_thread != nullptr) {
        //1、调用一下获取monitor的函数,回来当时线程想要竞争的monitor
        int monitorObj = ((int (*)(long)) get_contended_monitor)(native_thread);
        if (monitorObj != 0) {
            // 2、获取这个monitor被哪个线程持有,回来该线程id
            monitor_thread_id = ((int (*)(int)) get_lock_owner_thread)(monitorObj);
        } else {
            monitor_thread_id = 0;
        }
    }
    return monitor_thread_id;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_signal_MainActivity_nativePeer2Threadid(JNIEnv *env, jobject thiz,
                                                         jlong native_peer) {
    if (native_peer != 0) {
        if (SDK_INT > 20) {
            //long 强转 int
            int *pInt = reinterpret_cast<int *>(native_peer);
            //地址 +3,得到 native id
            pInt = pInt + 3;
            return *pInt;
        }
    }
}
extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    char sdk[PROP_VALUE_MAX];
    __system_property_get("ro.build.version.sdk", sdk);
    SDK_INT = atoi(sdk);
    return JNI_VERSION_1_4;
}

对应java层

external fun deadLockMonitor(nativeThread:Long):Int
private fun getAllThread():Array<Thread?>{
    val threadGroup = Thread.currentThread().threadGroup;
    val total = Thread.activeCount()
    val array = arrayOfNulls<Thread>(total)
    threadGroup?.enumerate(array)
    return array
}
external fun nativePeer2Threadid(nativePeer:Long):Int

总结

咱们经过死锁这个比方,去了解了native层Thread的相关办法,一起也了解了如何运用dlsym打开函数符号并调用。本篇Android性能优化就到此结束,感谢观看!