Android性能優(yōu)化死鎖監(jiān)控知識點詳解_第1頁
Android性能優(yōu)化死鎖監(jiān)控知識點詳解_第2頁
Android性能優(yōu)化死鎖監(jiān)控知識點詳解_第3頁
Android性能優(yōu)化死鎖監(jiān)控知識點詳解_第4頁
Android性能優(yōu)化死鎖監(jiān)控知識點詳解_第5頁
已閱讀5頁,還剩12頁未讀 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領

文檔簡介

第Android性能優(yōu)化死鎖監(jiān)控知識點詳解目錄前言死鎖檢測線程Block狀態(tài)獲取當前線程所請求的鎖通過鎖獲取當前持有的線程線程啟動nativePeer與nativeThreadtid與javaThreadtiddlsym與調(diào)用系統(tǒng)限制死鎖檢測所有代碼總結

前言

死鎖,這個從接觸程序開發(fā)的時候就會經(jīng)常聽到的詞,它其實也可以被稱為一種藝術,即互斥資源訪問循環(huán)的藝術,在Android中,如果主線程產(chǎn)生死鎖,那么通常會以ANR結束app的生命周期,如果是兩個子線程的死鎖,那么就會白白浪費cpu的調(diào)度資源,同時也不那么容易被發(fā)現(xiàn),就像一顆腫瘤,永遠藏在app中。當然,本篇介紹的是業(yè)內(nèi)常見的死鎖監(jiān)控手段,同時也希望通過死鎖,去挖掘更加底層的知識,同時讓我們更加了解一些常用的監(jiān)控手段。

我們很容易模擬一個死鎖操作,比如

vallock1=Object()

vallock2=Object()

Thread({

synchronized(lock1){

Thread.sleep(2000)

synchronized(lock2){

},"thread222").start()

Thread({

synchronized(lock2){

Thread.sleep(1000)

synchronized(lock1){

},"thread111").start()

因為thread111跟thread222都同時持有著對方想要的臨界資源(互斥資源),因此這兩個線程都處在互相等待對方的狀態(tài)。

死鎖檢測

我們怎么判斷死鎖:是否存在一個線程所持有的鎖被另一個線程所持有,同時另一個線程也持有該線程所需要的鎖,因此我們需要知道以下信息才能進行死鎖分析:

線程所要獲取的鎖是什么該鎖被什么線程所持有是否產(chǎn)生循環(huán)依賴的限制(本篇就不涉及了,因為我們知道了前兩個就可以自行分析了)

線程Block狀態(tài)

通過我們對synchronized的了解,當線程多次獲取不到鎖的時候,此時線程就會進入悲觀鎖狀態(tài),因此線程就會嘗試進入阻塞狀態(tài),避免進一步的cpu資源消耗,因此此時兩個線程都會處于block阻塞的狀態(tài),我們就能知道,處于被block狀態(tài)的線程就有可能產(chǎn)生死鎖(只是有可能),我們可以通過遍歷所有線程,查看是否處于block狀態(tài),來進行死鎖判斷的第一步

valthreads=getAllThread()

threads.forEach{

if(it.isAlive==trueit.state==Thread.State.BLOCKED){

進入死鎖判斷

獲取所有線程

privatefungetAllThread():ArrayThread{

valthreadGroup=Thread.currentThread().threadGroup;

valtotal=Thread.activeCount()

valarray=arrayOfNullsThread(total)

threadGroup.enumerate(array)

returnarray

通過對線程的判斷,我們能夠排除大部分非死鎖的線程,那么下一步我們要怎么做呢?如果線程發(fā)生了死鎖,那么一定擁有一個已經(jīng)持有的互斥資源并且不釋放才有可能造成死鎖對不對!那么我們下一步,就是要檢測當前線程所持有的鎖,如果兩個線程同時持有對方所需要的鎖,那么就會產(chǎn)生死鎖

獲取當前線程所請求的鎖

雖然我們在java層沒有相關的api提供給我們獲取線程當前想要請求的鎖,但是在我們的native層,卻可以輕松做到,因為它在art中得到更多的支持。

ObjPtrmirror::ObjectMonitor::GetContendedMonitor(Thread*thread){

//ThisisusedtoimplementJDWP'sThreadReference.CurrentContendedMonitor,andhasabizarre

//definitionofcontendedthatincludesamonitorathreadistryingtoenter...

ObjPtrmirror::Objectresult=thread-GetMonitorEnterObject();

if(result==nullptr){

//...butalsoamonitorthatthethreadiswaitingon.

MutexLockmu(Thread::Current(),*thread-GetWaitMutex());

Monitor*monitor=thread-GetWaitMonitor();

if(monitor!=nullptr){

result=monitor-GetObject();

returnresult;

其中第一步嘗試著通過thread-GetMonitorEnterObject()去拿

mirror::Object*GetMonitorEnterObject()constREQUIRES_SHARED(Locks::mutator_lock_){

returntlsPtr_.monitor_enter_object;

其中tlsPtr_其實就是art虛擬機中對于線程ThreadLocal的代表,即代表著只屬于線程的本地對象,會先嘗試從這里拿,拿不到的話通過Thread類中的wait_mutex_對象去拿

Mutex*GetWaitMutex()constLOCK_RETURNED(wait_mutex_){

returnwait_mutex_;

GetContendedMonitor提供了一個方法查詢當前線程想要的鎖對象,這個鎖對象以ObjPtrmirror::Object對象表示,其中mirror::Object類型是art中相對應于java層的Object類的代表,我們了解一下即可??吹竭@里我們可能還有一個疑問,這個Thread*thread的入?yún)⑹鞘裁茨??(其實是nativePeer,下文我們會了解)

我們有辦法能夠查詢到線程當前請求的鎖,那么這個鎖被誰持有呢?只有解決這兩個問題,我們才能進行死鎖的判斷對不對,我們繼續(xù)往下

通過鎖獲取當前持有的線程

我們還記得上文中返回的鎖對象是以ObjPtrmirror::Object表示的,當然,art中同樣提供了方法,讓我們通過這個鎖對象去查詢當前是哪個線程持有

uint32_tMonitor::GetLockOwnerThreadId(ObjPtrmirror::Objectobj){

DCHECK(obj!=nullptr);

LockWordlock_word=obj-GetLockWord(true);

switch(lock_word.GetState()){

caseLockWord::kHashCode:

//Fall-through.

caseLockWord::kUnlocked:

returnThreadList::kInvalidThreadId;

caseLockWord::kThinLocked:

returnlock_word.ThinLockOwner();

caseLockWord::kFatLocked:{

Monitor*mon=lock_word.FatLockMonitor();

returnmon-GetOwnerThreadId();

default:{

LOG(FATAL)"Unreachable";

UNREACHABLE();

這里函數(shù)比較簡單,如果當前調(diào)用正常,那么執(zhí)行的就是LockWord::kFatLocked,返回的是native層的Thread的tid,最終是以uint32_t類型表示

注意這里GetLockOwnerThreadId中返回的Threadid千萬不要跟Java層的Thread對象的tid混淆,這里的tid才是真正的線程id標識

線程啟動

我們來看一下native層主線程的啟動,它隨著art虛擬機的啟動隨即啟動,我們都知道java層的線程其實在沒有跟操作系統(tǒng)的線程綁定的時候,它只能算是一塊內(nèi)存!只要經(jīng)過與native線程綁定后,這時的Thread才能真正具備線程調(diào)度的能力,下面我們以主線程啟動舉例子:

thread.cc

voidThread::FinishStartup(){

Runtime*runtime=Runtime::Current();

CHECK(runtime-IsStarted());

//Finishattachingthemainthread.

ScopedObjectAccesssoa(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函數(shù),創(chuàng)建的名稱是main,CreatePeer是native線程中非常重要的存在,所有線程創(chuàng)建都經(jīng)過它,這個函數(shù)有點長,筆者這里做了刪減

voidThread::CreatePeer(constchar*name,boolas_daemon,jobjectthread_group){

Runtime*runtime=Runtime::Current();

CHECK(runtime-IsStarted());

JNIEnv*env=tlsPtr_.jni_env;

if(thread_group==nullptr){

thread_group=runtime-GetMainThreadGroup();

//設置了線程名字

ScopedLocalRefjobjectthread_name(env,env-NewStringUTF(name));

//AddmissingnullcheckincaseofOOMb/18297817

if(name!=nullptrthread_name.get()==nullptr){

CHECK(IsExceptionPending());

return;

//設置Thread的各種屬性

jintthread_priority=GetNativePriority();

jbooleanthread_is_daemon=as_daemon;

//創(chuàng)建了一個java層的Thread對象,名字叫做peer

ScopedLocalRefjobjectpeer(env,env-AllocObject(WellKnownClasses::java_lang_Thread));

if(peer.get()==nullptr){

CHECK(IsExceptionPending());

return;

ScopedObjectAccesssoa(this);

tlsPtr_.opeer=soa.Decodemirror::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指向了當前nativeThread對象self-Thread

Thread*self=this;

DCHECK_EQ(self,Thread::Current());

env-SetLongField(peer.get(),

WellKnownClasses::java_lang_Thread_nativePeer,

reinterpret_cast64jlong(self));

ScopedObjectAccesssoa(self);

StackHandleScope1hs(self);

這里其實就是一次jni調(diào)用,把java中的Thread的nativePeer進行了賦值,而賦值的內(nèi)容,正是通過了這個調(diào)用SetLongField

env-SetLongField(peer.get(),

WellKnownClasses::java_lang_Thread_nativePeer,

reinterpret_cast64jlong(self));

這里我們簡單了解一下SetLongField,如果進行過jni開發(fā)的同學應該能過明白,其實就是把peer.get()得到的對象(其實就是java層的Thread對象)的nativePeer屬性,賦值為了self(native層的Thread對象的指針),并強轉(zhuǎn)換為了jlong類型。我們接下來回到java層

Thread.java

privatevolatilelongnativePeer;

說了一大堆,那么這個nativePeer究竟是個什么?通過上面的代碼分析,我們能夠明白了,Thread.java中的nativePeer就是一個指針,它所指向的內(nèi)容正是native層中的Thread

nativePeer與nativeThreadtid與javaThreadtid

經(jīng)過了上面一段落,我們了解了nativePeer,那么我們繼續(xù)對比一下java層Threadtid與native層Threadtid。我們通過在kotlin/java中,調(diào)用Thread對象的id屬性,其實得到的是這個

privatelongtid;

它的生成方法如下

/*SetthreadID*/

tid=nextThreadID();

privatestaticsynchronizedlongnextThreadID(){

return++threadSeqNumber;

可以看到,雖然它的確能代表一個java層中Thread的標識,但是生成其實可以看到,他也僅僅是一個普通的累積id生成,同時也并沒有在native層中被當作唯一標識進行使用。

而nativeThread的tid屬性,才是真正的線程id

在art中,通過GetTid獲取

pid_tGetTid()const{

returntls32_.tid;

同時我們也可以注意到,tid是保存在tls32_結構體中,并且其位于Thread對象的開頭,從內(nèi)存分布上看,tid位于state_and_flags、suspend_count、think_lock_thread_id之后,還記得我們上面說過的nativePeer嘛?我們一直強調(diào)native是Thread的指針對象

因此我們可以通過指針的偏移,從而算出nativePeer到tid的換算公式,即nativePeer指針向下偏移三位就找到了tid(因為state_and_flags,state_and_flags,think_lock_thread_id都是int類型,那么對應的指針也就是int*)這里有點繞,因為涉及指針的內(nèi)容

int*pInt=reinterpret_castint*(native_peer);

//地址+3,得到tid

pInt=pInt+3;

return*pInt;

nativePeer對象因為就在java層,我們很容易通過反射就能拿到

valnativePeer=Thread::class.java.getDeclaredField("nativePeer")

nativePeer.isAccessible=true

valcurrentNativePeer=nativePeer.get(it)

這里我們通過nativePeer換算成tid可以寫成一個jni方法

externalfunnativePeer2Threadid(nativePeer:Long):Int

實現(xiàn)就是

extern"C"

JNIEXPORTjintJNICALL

Java_com_example_signal_MainActivity_nativePeer2Threadid(JNIEnv*env,jobjectthiz,

jlongnative_peer){

if(native_peer!=0){

//long強轉(zhuǎn)int

int*pInt=reinterpret_castint*(native_peer);

//地址+3,得到nativeid

pInt=pInt+3;

return*pInt;

dlsym與調(diào)用

我們上面終于把死鎖能涉及到的點都講完,比如如何獲取線程所請求的鎖,當前鎖又被那個線程持有,如何通過nativePeer獲取Threadid做了分析,但是還有一個點我們還沒能解決,就是如何調(diào)用這些函數(shù)。我們需要調(diào)用的是GetContendedMonitor,GetLockOwnerThreadId,這個時候dlsym系統(tǒng)調(diào)用就出來了,我們可以通過dlsym進行調(diào)用我們想要調(diào)用的函數(shù)

void*dlsym(void*__handle,constchar*__symbol);

這里的symbol是什么呢?其實我們所有的elf(so也是一種elf文件)的所有調(diào)用函數(shù)都會生成一個符號,代表著這個函數(shù),它在elf的.text中。而我們android中,就會通過加載so的方式加載系統(tǒng)庫,加載的系統(tǒng)庫libart.so里面就包含著我們想要調(diào)用的函數(shù)GetContendedMonitor,GetLockOwnerThreadId的符號

我們可以通過objdump-tlibart.so查看符號

這里我們直接給出來各個符號,讀者可以直接用objdump查看符號

GetContendedMonitor對應的符號是

_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE

GetLockOwnerThreadId對應的符號

sdk=29

_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE

29是這個

_ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE

系統(tǒng)限制

然后到這里,我們還是沒能完成調(diào)用,因為dlsym等dl系列的系統(tǒng)調(diào)用,因為從Android7.0開始,Android系統(tǒng)開始阻止App中直接使用dlopen(),dlsym()等函數(shù)打開系統(tǒng)動態(tài)庫,好家伙!谷歌大兄弟為了安全的考慮,做了很多限制。但是這個防君子不防程序員,業(yè)內(nèi)依舊有很多繞過系統(tǒng)的限制的方法,我們看一下dlsym

__attribute__((__weak__))

void*dlsym(void*handle,constchar*symbol){

constvoid*caller_addr=__builtin_return_address(0);

return__loader_dlsym(handle,symbol,caller_addr);

__builtin_return_address是Linux一個內(nèi)建函數(shù)(通常由編譯器添加),__builtin_return_address(0)用于返回當前函數(shù)的返回地址。

在__loader_dlsym會進行返回地址的校驗,如果此時返回地址不是屬于系統(tǒng)庫的地址,那么調(diào)用就不成功,這也是art虛擬機保護手段,因此我們很容易就得出一個想法,我們是不是可以用系統(tǒng)的某個函數(shù)去調(diào)用dlsym,然后把結果給到我們自己的函數(shù)消費就可以了?是的,業(yè)內(nèi)已經(jīng)有很多這個方案了,比如ndk_dlopen

我們拿arm架構進行分析,arm架構中LR寄存器就是保存了當前函數(shù)的返回地址,那么我們是不是在調(diào)用dlsym時可以通過匯編代碼直接修改LR寄存器的地址為某個系統(tǒng)庫的函數(shù)地址就可以了?嗯!是的,但是我們還需要把原來的LR地址給保存起來,不然就沒辦法還原原來的調(diào)用了。

這里我們拿ndk_dlopen的實現(xiàn)舉例子

if(SDK_INT=0){

charsdk[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);

//wearecurrentlyhijacking"FatalError"asafakesystem-calltrampoline

uintptr_tpv=(uintptr_t)(*env)-FatalError;

uintptr_tpu=(pv|(PAGE_SIZE-1))+1u;

uintptr_tpd=(pv~(PAGE_SIZE-1));

mprotect((void*)pd,pv+8u=puPAGE_SIZE*2u:PAGE_SIZE,PROT_READ|PROT_WRITE|PROT_EXEC);

quick_on_stack_back=(void*)pv;

//arm架構匯編實現(xiàn)

#elifdefined(__arm__)

//r0~r3

0x0000000000000000:08E02DE5strlr,[sp,#-8]!

0x0000000000000004:02E0A0E1movlr,r2

0x0000000000000008:13FF2FE1bxr3

memcpy(__insns,"\x08\xE0\x2D\xE5\x02\xE0\xA0\xE1\x13\xFF\x2F\xE1",12);

if((pv1u)!=0u){//Thumb

0x0000000000000000:0CBCpop{r2,r3}

0x0000000000000002:1047bxr2

memcpy((void*)(pv-1),"\x0C\xBC\x10\x47",4);

}else{

0x0000000000000000:0C00BDE8pop{r2,r3}

0x0000000000000004:12FF2FE1bxr2

memcpy(quick_on_stack_back,"\x0C\x00\xBD\xE8\x12\xFF\x2F\xE1",8);

}//if

其中我們拿(*env)-FatalError作為了混淆系統(tǒng)調(diào)用的stub,我們參照著流程圖去理解上述代碼:

02E0A0E1movlr,r2把r2寄存器的內(nèi)容放到了lr寄存器,這個r2存的東西就是FatalError的地址0x0000000000000008:13FF2FE1bxr3,通過bx指令調(diào)轉(zhuǎn),就可以正常執(zhí)行我們的dlsym了,r3就是我們自己的dlsym的地址0x0000000000000000:0C00BDE8pop{r2,r3}調(diào)用完r3寄存器的方法把r2寄存器放到調(diào)用棧下,提供給后面的執(zhí)行進行消費0x0000000000000004:12FF2FE1bxr2,最后就回到了我們的r2,完成了一次調(diào)用

總之,我們想要做到dl系列的調(diào)用,就是想盡方法去修改對應架構的函數(shù)返回地址的數(shù)值。

死鎖檢測所有代碼

constchar*get_lock_owner_symbol_name(){

if(SDK_INT=29){

return"_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE";

}else{

return"_ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE";

extern"C"

JNIEXPORTjintJNICALL

Java_com_example_signal_MyHandler_deadLockMonitor(JNIEnv*env,jobjectthiz,

jlongnative_thread){

//1、初始化

ndk_init(env);

//2、打開動態(tài)庫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());

intmonitor_thread_id=0;

if(get_contended_monitor!=nullptrget_lock

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
  • 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論