網站首頁 編程語言 正文
前言
“死鎖”,這個從接觸程序開發的時候就會經常聽到的詞,它其實也可以被稱為一種“藝術”,即互斥資源訪問循環的藝術,在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_flags、suspend_count、think_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, "\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
總結
原文鏈接:https://juejin.cn/post/7159784805293359141
相關推薦
- 2022-10-25 React?中?setState?的異步操作案例詳解_React
- 2022-05-23 c++?qt自定義搜索編輯框的實現方法_C 語言
- 2022-11-15 Golang?使用os?庫的?ReadFile()?讀文件最佳實踐_Golang
- 2022-06-16 C#實現二叉查找樹_C#教程
- 2021-12-05 Go語言配置數據庫連接池的實現_Golang
- 2023-01-03 淺析Go語言中閉包的使用_Golang
- 2022-04-28 shell命令返回值判斷的方法實現_linux shell
- 2022-09-27 C#中的const和readonly關鍵字詳解_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支