一、问题背景
在多线程环境下调试或定位问题时,有时我们会发现某重要线程被卡住在等待某个锁上,但具体是哪个线程或哪段代码拿了锁(特别是代码逻辑比较复杂、线程较多的情况下)又无法一下看出来。
这时我们就需要在调试器里把所有线程点一遍,看他们当前的调用栈,然后再对照源代码看各线程的调用栈上有没有哪个方法是加了这把锁的。
在源码不太熟的情况下,这是个非常费时费力的工作。
二、解决思路
参考上一篇关于dispatch_semaphore优先级反转里讲的,对于某几种(常用的)锁,操作系统会记录其持有线程(owner
)是谁的信息。
那么究竟是怎么记的呢?我们能不能直接从锁的实例拿到这个owner
信息呢?
三、实验和结果
基于关于dispatch_semaphore优先级反转这篇贴子里描述的,记录了 owner
信息的最底层的锁有两种:pthread_mutex
和os_unfair_lock
。
NSLock
和NSRecursiveLock
是基于pthread_mutex
实现的,它们也比较常用,所以下面我们把这几种锁挨个试验一下:
1. pthread_mutex
互斥锁
示例代码:
//生成 mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//加锁
pthread_mutex_lock(&mutex);
网上搜索一下它的源码,发现在pthread_mutex_t
结构体里有一个__data.__owner
字段存的是相关的信息。
在Xcode
中断点调试了一下,发现加完锁后,mutex
的0x18
偏移处的确放了当前线程的tid
,也即可以从锁实例直接定位其owner
线程。
注:看内存可以在
lldb
中调用x/64x &mutex
;查线程tid
可以调用thread info
命令。
所以取pthread_mutex_t
类型的变量mutex
的owner
线程tid
的取法为:
owner_tid = *(int *)((char *)&mutex + 0x18)
注:拿到
tid
以后可以调用lldb
命令thread list
来得到tid
与常见的线程序号的对应关系
2. NSLock
和NSRecursiveLock
锁
示例代码:
NSLock *lock = [[NSLock alloc] init];
[lock lock];
NSRecursiveLock *recLock = [[NSRecursiveLock alloc] init];
[recLock lock];
这两种锁内部实现是基于pthread_mutex_t
的(可以用Hopper
直接反编译 Foundation
,也可以在Xcode
里对-[NSLock lock]
打断点看汇编)。
注:下文中附的是模拟器中的反汇编结果,其注释比较明确,真机的注释中的符号显示与实际不一致,容易误导读者。但实质上真机和模拟器上锁的内部实现是一致的,经测试取到的
tid
偏移也一样。
-[NSLock lock]
反汇编结果如下:
-[NSRecursiveLock lock]
反汇编结果如下:
其实现等价于:
// -[NSLock lock]
pthread_mutex_lock(object_getIndexedIvars(self)); //self 为 lock 实例
可以看出,可以直接对lock
对象调用object_getIndexedIvars
方法即可得到指向pthread_mutex_t
实例的指针。
经测试,模拟器上和真机上调用object_getIndexedIvars(lock)
的结果都是(char *)lock + 0x10
,所以:
NSLock*
或NSRecursiveLock*
类型的变量lock
的owner
线程 id 的取法为:
owner_tid = *(int *)((char *)lock + 0x10 + 0x18);
3. os_unfair_lock
锁
示例代码:
os_unfair_lock unfairlock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&unfairlock);
使用同样的方法,经过实验验证,os_unfair_lock类型的变量其 owner 线程取法为:
owner_id = *(unsigned int *)((char *)&unfairlock);
注意: 这里为os_unfair_lock
取到的owner_id
与实际线程的tid
并不一致,系统库中比较锁的owner
与当前线程是否一致时是取当前线程tsd
中的3
号元素(pthread_getspecific(3)
),跟这个(owner_id | 1
)比较。
//判断 lock owner 是否是当前线程
void
os_unfair_lock_assert_owner(os_unfair_lock_t lock)
{
_os_unfair_lock_t l = (_os_unfair_lock_t)lock;
//取当前线程的 id 信息
os_lock_owner_t self = _os_lock_owner_get_self();
//取 lock 的 owner 信息
os_ulock_value_t current = os_atomic_load2o(l, oul_value, relaxed);
if (unlikely(OS_ULOCK_IS_NOT_OWNER(current, self, 0))) {
__LIBPLATFORM_CLIENT_CRASH__(current, "Assertion failed: "
"Lock unexpectedly not owned by current thread");
}
}
static inline os_lock_owner_t
_os_lock_owner_get_self(void)
{
os_lock_owner_t self;
self = (os_lock_owner_t)_os_tsd_get_direct(__TSD_MACH_THREAD_SELF); // 同 pthread_getspecific(3)
return self;
}
至于怎么从owner_id
得到tid
还需要进一步调研,不过目前来说写个 lldb 脚本跑一下各线程的pthread_getspecific(3)
也能定位出来 tid 和 owner_id 的对应关系。
四、结论及展望
综上可知,通过简单的取偏移办法,就能在运行时定位到某个锁当前的owner
线程。
后续可以写一个lldb
脚本实现一个owner_tid
命令,支持直接在lldb
命令行里调用,调试起来就更方便了。
另外,卡顿和死锁检测模块也可以使用这个owner
信息,帮助识别卡顿的根本原因。
注:本文主要是方案调研,笔者只在一两个版本的模拟器和真机上进行了试验来验证可行性。理论上这几个结构体应该不怎么会变,不过如果要真正投入线上使用,还是请在支持的系统版本上进行完整验证后再使用。