首頁 > 軟體

Android效能優化死鎖監控知識點詳解

2022-11-01 14:02:23

前言

“死鎖”,這個從接觸程式開發的時候就會經常聽到的詞,它其實也可以被稱為一種“藝術”,即互斥資源存取回圈的藝術,在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都同時持有著對方想要的臨界資源(互斥資源),因此這兩個執行緒都處在互相等待對方的狀態。

死鎖檢測

我們怎麼判斷死鎖:是否存在一個執行緒所持有的鎖被另一個執行緒所持有,同時另一個執行緒也持有該執行緒所需要的鎖,因此我們需要知道以下資訊才能進行死鎖分析:

  • 執行緒所要獲取的鎖是什麼
  • 該鎖被什麼執行緒所持有
  • 是否產生迴圈依賴的限制(本篇就不涉及了,因為我們知道了前兩個就可以自行分析了)

執行緒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型別表示

注意這裡GetLockOwnerThreadId中返回的Thread id千萬不要跟Java層的Thread物件的tid混淆,這裡的tid才是真正的執行緒id標識

執行緒啟動

我們來看一下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

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

在art中,通過GetTid獲取

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

同時我們也可以注意到,tid 是儲存在 tls32_結構體中,並且其位於Thread物件的開頭,從記憶體分佈上看,tid位於state_and_flagssuspend_countthink_lock_thread_id之後,還記得我們上面說過的nativePeer嘛?我們一直強調native是Thread的指標物件

因此我們可以通過指標的偏移,從而算出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 檢視符號

這裡我們直接給出來各個符號,讀者可以直接用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地址給儲存起來,不然就沒辦法還原原來的呼叫了。

這裡我們拿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, "x08xE0x2DxE5x02xE0xA0xE1x13xFFx2FxE1", 12);
            if ((pv & 1u) != 0u) { // Thumb
                /*
                 0x0000000000000000:     0C BC   pop {r2, r3}
                 0x0000000000000002:     10 47   bx r2
                */
                memcpy((void *)(pv - 1), "x0CxBCx10x47", 4);
            } else {
                /*
                 0x0000000000000000:     0C 00 BD E8     pop {r2, r3}
                 0x0000000000000004:     12 FF 2F E1     bx r2
                */
                memcpy(quick_on_stack_back, "x0Cx00xBDxE8x12xFFx2FxE1", 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效能優化就到此結束,更多關於Android效能優化死鎖監控的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com