上周,小伙伴调试NDB的LINUX版本时遇到硬石头。开始调试后,NanoCode界面卡住不动了。
今天一早,我亲自上手,把gdb附加到NanoCode的extension Host进程,然后thread apply all bt看各个线程的状态。
很快,看到了问题。关键的调试引擎线程死锁了。
#0 futex_wait (private=0, expected=2, futex_word=0x7f4c4b4448 <zk+144>) at ../sysdeps/nptl/futex-internal.h:146
#1 __GI___lll_lock_wait (futex=futex@entry=0x7f4c4b4448 <zk+144>, private=private@entry=0) at ./nptl/lowlevellock.c:49
#2 0x0000007f969569e4 in lll_mutex_lock_optimized (mutex=0x7f4c4b4448 <zk+144>) at ./nptl/pthread_mutex_lock.c:48
#3 ___pthread_mutex_lock (mutex=0x7f4c4b4448 <zk+144>) at ./nptl/pthread_mutex_lock.c:93
#4 0x0000007f4bee5fec in non-virtual thunk to dxer::GetCurrentProcessExecutableName(char*, unsigned int, unsigned int*) ()
at ../elk/dxer.hpp:2510
#5 0x0000007f4c687e74 in NdBoss::UpdateCurDebuggee (this=0x3400ae6000) at ../ndi/NdBoss.cpp:3188
#6 0x0000007f4c68646c in NdBoss::CbEngineStateChange (this=0x3400ae6000, ulFlags=1, ul64Argument=0)
at ../ndi/NdBoss.cpp:2338
#7 0x0000007f4c672ee0 in CEventCallbacks::ChangeEngineState (this=0x3400f4b780, Flags=1, Argument=0)
at ../ndi/EventCallbacks.cpp:204
#8 0x0000007f4bee1c98 in NotifyChangeEngineState
#9 0x0000007f4bf17bc4 in zhuge::ready_to_break
(this=this@entry=0x7f4c4a3d50 <zg>, ExtraStatusFlags=ExtraStatusFlags@entry=0) at ../elk/zhuge.cpp:499
#10 0x0000007f4bf08de0 in poll_meta (Flags=0, Timeout=4294967295) at ../elk/dxer_ctrl.cpp:3382
#11 0x0000007f4bf092b4 in non-virtual thunk to dxer::WaitForEvent(unsigned int, unsigned int) () at ../elk/dxer.hpp:851
#12 0x0000007f4c683e58 in NdBoss::SessionLoop (this=0x3400ae6000) at ../ndi/NdBoss.cpp:1112
#13 0x0000007f4c6831f4 in SessionThreadProc (lpParameter=0x3400ae6000) at ../ndi/NdBoss.cpp:763
#14 0x0000007f969537d0 in start_thread (arg=0x7f982d9780) at ./nptl/pthread_create.c:444
#15 0x0000007f969bf5cc in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone3.S:76
扫视一番调用栈的函数,卡在著名的lll(low level lock)函数。
切换到这个线程。
(gdb) thread 16
[Switching to thread 16 (Thread 0x7f4ba1ea80 (LWP 3891))]
再切换到2号栈帧。然后打印mutex对象。
(gdb) p mutex
$1 = (pthread_mutex_t *) 0x7f4c4b4448 <zk+144>
因为是指针,所以需要加个星。
(gdb) p *mutex
$2 = {__data = {__lock = 2, __count = 0,
__owner = 3891, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0,
__next = 0x0}},
__size = "\002\000\000\000\000\000\000\0003\017\000\000\001",
'\000' <repeats 34 times>, __align = 2}
看关键的owner字段:__owner = 3891
线程ID是3891,居然是它自己。
为什么会是这个线程自己呢?因为这个线程的父函数里已经获得这个互斥量,然后它的子函数又调用了需要加锁的函数,又获得这个互斥量。
从count字段为2也可以看出这个特征。
这个代码本来是在Windows下开发的,使用的是关键区做保护。在Windows下,同一个线程是可以多次进同一个关键区的,也就是所谓的可递归(重入)。
移植到Linux时,使用了pthread_mutex_t来替代关键区,但是Linux下的pthread_mutex_t默认是不可以递归的,也就是即使是同一个线程,也不允许第二次获得同一个互斥量。
如何解决这个问题呢?一种方法是使用C++标准库的recursive_mutex。另一种方法是修改pthread_mutex_t的类型,让它支持重入。我选择了第二种方法。
进一步说,pthread_mutex_t支持四种类型:
- PTHREAD_MUTEX_TIMED_NP,这是默认值,也就是普通锁。
- PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,这个行为与Windows是类似的。
- PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型相同,为了保证不出现最简单情况下的死锁。
- PTHREAD_MUTEX_ADAPTIVE_NP,自适应锁。
因为pthread_mutex_init函数需要通过一个属性结构体来设置锁的类型,所以新的代码大致如下:
pthread_mutexattr_t ma;
pthread_mutexattr_init(&ma);
pthread_mutexattr_settype (&ma, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &ma);
这样修改后,问题解决了。
顺便预告一下,本周三晚上,有一期《内核夜话》,欢迎新老朋友来听。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号