描述一次在工作中遇到的简单问题,其现象以及排查流程
问题描述
程序死锁,gstack
调用栈,发现pthread_rwlock_rdlock
死锁。
Thread 3 (Thread 0x7f50e76fa700 (LWP 28329)):
#0 0x00007f514f2e9184 in pthread_rwlock_rdlock () from /usr/lib64/libpthread.so.0
#1 0x00000000004fbd33 in UDESessionImplement::_check_loop (this=0x2506990) at ude_session.cpp:886
#2 0x00000000004fbb80 in UDESessionImplement::_check_thread (arg=0x2506990) at ude_session.cpp:838
#3 0x00007f514f2e5ea5 in start_thread () from /usr/lib64/libpthread.so.0
#4 0x00007f514f00e9fd in clone () from /usr/lib64/libc.so.6
Thread 1 (Thread 0x7f51542aecc0 (LWP 28268)):
#0 0x00007f514f2e9184 in pthread_rwlock_rdlock () from /usr/lib64/libpthread.so.0
#1 0x00000000004fb716 in UDESessionImplement::_chose_link (this=0x2506990, param=0x7ffff98d0930, table_name=0x0)
at ude_session.cpp:743
#2 0x00000000004fa6e0 in UDESessionImplement::handle_request (this=0x2506990,
req_data=0x7ffff98d0930 "{\"msgId\":\"1\",\"opCode\":\"11\",\"data\":{\"tableInfo\":\"SYN_D_NCH_INIT_NODE\"}}", req_len=71,
rsp_data=0x25193a0 "", rsp_len=@0x7ffff98d1610: 4096, timeout=3) at ude_session.cpp:378
#3 0x00000000004ee01b in Sync_Client::GetFirst (this=0xeb8640 <DGWSyncClient::InitSyncClient(int)::_client>,
sname=0xeb7e00 <sync_nchInfo()::nchInit> "SYN_D_NCH_INIT_NODE", data=0x25193a0 "", len=@0x7ffff98d166c: 4096,
seq=@0x7ffff98d1668: -1, timeout=3) at sync_client.cpp:531
故障分析
打印读写锁的值:
p _connect_locker $1 = {__data = {__lock = 0, __nr_readers = 0, __readers_wakeup = 3544, __writer_wakeup = 393220, __nr_readers_queued = 1094998026, __nr_writers_queued = 1213417806, __writer = 0, __shared = 0, __pad1 = 0, __pad2 = 0, __flags = 3456}, __size = "\000\000\000\000\000\000\000\000\330\r\000\000\004\000\006\000\nXDANISH", '\000' <repeats 24 times>, "\200\r\000\000\003\000\a", __align = 0}
- 读写锁中各个字段的含义:
变量 | 说明 |
---|---|
__lock | 管理读写锁全局竞争的锁,无论是读锁写锁还是解锁,都会互斥 |
__writer | 写锁持有者的线程ID,如果为0则表示当前无线程持有写锁 |
__nr_reads | 读锁持有线程的个数 |
__nr_reads_queued | 读锁的派对等待线程的个数 |
__nr_writers_queued | 写锁的排队等待线程的个数 |
读写锁的逻辑
- 对于读锁请求
- 无线程持有写锁,即
__writer==0
- 采用的是读者优先策略或没有写锁的等待者(
__nr_writers_queued==0
) 当满足这两个条件时,读锁请求都可以立即获得读锁,返回之前执行__nr_readers++
,表示增加了一个线程占有读锁。 不满足的话,则执行__nr_readers_queued++
,表示增加一个读锁等待者,然后调用futex
,陷入阻塞。醒来之后,会执行__nr_readers_queued–
,然后再次判断是否同时满足条件1和2。
- 无线程持有写锁,即
- 对于写锁请求
- 无线程持有写锁,即
__writer==0
。 - 没有线程持有读锁,即
__nr_readers==0
。 只要满足上述条件,就会立刻拿到写锁,将__writer
置为线程的ID(调度域)。 如果不满足,那么执行__nr_writers_queued++
,表示增加一个写锁等待者线程,然后执行mutex
陷入等待。醒来后,先执行__nr_writers_queued–
,然后重新判断条件1和2。
- 无线程持有写锁,即
- 对于解锁而言
- 如果当前锁是写锁
一 执行
__writer=0
,表示释放写锁。 二 根据__nr_writers_queued
判断有没有写锁等待者,如果有则唤醒一个写锁等待者。 三 如果没有写锁等待者,则判断有没有读锁等待者;如果有,则将所有的读锁等待者一起唤醒。 - 如果当前锁是读锁
一 执行
__nr_readers–
,表示读锁占有者少了一个 二 判断__nr_readers
是否等于0,是的话则表示自己是最后一个读锁占有者,需要唤醒写锁等待者或者读锁等待者:根据__nr_writers_queued
判断是否存在写锁等待者,若有,则唤醒一个写锁等待线程;如果没有写锁等待者,判断是否存在读锁等待者,若有,则唤醒所有的读锁等待者。
- 如果当前锁是写锁
一 执行
## 故障结论
从读写锁的逻辑看出,如果在读锁加锁的时候,__nr_writers_queued
不为0,那么就会阻塞读锁。从打印看出__nr_readers
为0,__writer=0
表示没有线程持有读锁,也没有线程持有写锁,但是读写锁的值却异常了。只有两种可能,一是未初始化,二是内存被改写。走查代码,发现两个构造函数中,只有一个构造函数初始化了读写锁。