Commit 1657dcf2 authored by Kenny Yu's avatar Kenny Yu Committed by Facebook Github Bot

folly: extend gdb deadlock detector for pthread_rwlock_t

Summary:
If a pthread_rwlock_t shared mutex is currently write-locked, we can
find its owner. However, if it is not write-locked or read-locked
(possibly by multiple threads), we will be unable to find the owner.

We can extend our deadlock detector to find deadlocks involving write-locked
pthread_rwlock_t mutexes as well.

Example output:

  Type "deadlock" to detect deadlocks.
  (gdb) deadlock
  Found deadlock!
  Thread 2 (LWP ...) is waiting on pthread_mutex_t (0x...) held by Thread 3 (LWP ...)
  Thread 3 (LWP ...) is waiting on pthread_rwlock_t (0x...) held by Thread 2 (LWP ...)

Reviewed By: yfeldblum

Differential Revision: D6974509

fbshipit-source-id: 8e64cb35f93958a9785070184df4c818b086ff0f
parent 2324cfbe
#!/usr/bin/env python3
from collections import defaultdict
from enum import Enum
import gdb
import re
......@@ -230,14 +231,81 @@ def find_cycle(graph):
return []
def get_stacktrace(thread_id):
'''
Returns the stack trace for the thread id as a list of strings.
'''
gdb.execute('thread %d' % thread_id, from_tty=False, to_string=True)
output = gdb.execute('bt', from_tty=False, to_string=True)
stacktrace_lines = output.strip().split('\n')
return stacktrace_lines
def is_thread_blocked_with_frame(
thread_id, top_line, expected_top_line, expected_frame
):
'''
Returns True if we found expected_top_line in top_line, and
we found the expected_frame in the thread's stack trace.
'''
if expected_top_line not in top_line:
return False
stacktrace_lines = get_stacktrace(thread_id)
return any(expected_frame in line for line in stacktrace_lines)
class MutexType(Enum):
'''Types of mutexes that we can detect deadlocks.'''
PTHREAD_MUTEX_T = 'pthread_mutex_t'
PTHREAD_RWLOCK_T = 'pthread_rwlock_t'
@staticmethod
def get_mutex_type(thread_id, top_line):
'''
Returns the probable mutex type, based on the first line
of the thread's stack. Returns None if not found.
'''
if is_thread_blocked_with_frame(
thread_id, top_line, '__lll_lock_wait', 'pthread_mutex'
):
return MutexType.PTHREAD_MUTEX_T
if is_thread_blocked_with_frame(
thread_id, top_line, 'futex_wait', 'pthread_rwlock'
):
return MutexType.PTHREAD_RWLOCK_T
return None
@staticmethod
def get_mutex_owner_and_address_func_for_type(mutex_type):
'''
Returns a function to resolve the mutex owner and address for
the given type. The returned function f has the following
signature:
f: args: (map of thread lwp -> thread id), blocked thread lwp
returns: (lwp of thread owning mutex, mutex address)
or (None, None) if not found.
Returns None if there is no function for this mutex_type.
'''
if mutex_type == MutexType.PTHREAD_MUTEX_T:
return get_pthread_mutex_t_owner_and_address
if mutex_type == MutexType.PTHREAD_RWLOCK_T:
return get_pthread_rwlock_t_owner_and_address
return None
def print_cycle(graph, lwp_to_thread_id, cycle):
'''Prints the threads and mutexes involved in the deadlock.'''
for (m, n) in cycle:
print(
'Thread %d (LWP %d) is waiting on mutex (0x%016x) held by '
'Thread %d (LWP %d) is waiting on %s (0x%016x) held by '
'Thread %d (LWP %d)' % (
lwp_to_thread_id[m], m, graph.attributes(m, n)['mutex'],
lwp_to_thread_id[n], n
lwp_to_thread_id[m], m,
graph.attributes(m, n)['mutex_type'].value,
graph.attributes(m, n)['mutex'], lwp_to_thread_id[n], n
)
)
......@@ -246,28 +314,32 @@ def get_thread_info():
'''
Returns a pair of:
- map of LWP -> thread ID
- set of blocked threads LWP
- map of blocked threads LWP -> potential mutex type
'''
# LWP -> thread ID
lwp_to_thread_id = {}
# Set of threads blocked on mutexes
blocked_threads = set()
# LWP -> potential mutex type it is blocked on
blocked_threads = {}
output = gdb.execute('info threads', from_tty=False, to_string=True)
lines = output.strip().split('\n')[1:]
regex = re.compile(r'[\s\*]*(\d+).*Thread.*\(LWP (\d+)\).*')
for line in lines:
thread_id = int(regex.match(line).group(1))
thread_lwp = int(regex.match(line).group(2))
lwp_to_thread_id[thread_lwp] = thread_id
if '__lll_lock_wait' in line:
blocked_threads.add(thread_lwp)
try:
thread_id = int(regex.match(line).group(1))
thread_lwp = int(regex.match(line).group(2))
lwp_to_thread_id[thread_lwp] = thread_id
mutex_type = MutexType.get_mutex_type(thread_id, line)
if mutex_type:
blocked_threads[thread_lwp] = mutex_type
except Exception:
continue
return (lwp_to_thread_id, blocked_threads)
def get_mutex_owner_and_address(lwp_to_thread_id, thread_lwp):
def get_pthread_mutex_t_owner_and_address(lwp_to_thread_id, thread_lwp):
'''
Finds the thread holding the mutex that this thread is blocked on.
Returns a pair of (lwp of thread owning mutex, mutex address),
......@@ -291,6 +363,37 @@ def get_mutex_owner_and_address(lwp_to_thread_id, thread_lwp):
return (None, None)
def get_pthread_rwlock_t_owner_and_address(lwp_to_thread_id, thread_lwp):
'''
If the thread is waiting on a write-locked pthread_rwlock_t, this will
return the pair of:
(lwp of thread that is write-owning the mutex, mutex address)
or (None, None) if not found, or if the mutex is read-locked.
'''
# Go up the stack to the pthread_rwlock_{rd|wr}lock frame
gdb.execute(
'thread %d' % lwp_to_thread_id[thread_lwp],
from_tty=False,
to_string=True
)
gdb.execute('frame 2', from_tty=False, to_string=True)
# Get the owner of the mutex by inspecting the internal
# fields of the mutex.
try:
rwlock_info = gdb.parse_and_eval('rwlock').dereference()
rwlock_owner_lwp = int(rwlock_info['__data']['__writer'])
# We can only track the owner if it is currently write-locked.
# If it is not write-locked or if it is currently read-locked,
# possibly by multiple threads, we cannot find the owner.
if rwlock_owner_lwp != 0:
return (rwlock_owner_lwp, int(rwlock_info.address))
else:
return (None, None)
except gdb.error:
return (None, None)
class Deadlock(gdb.Command):
'''Detects deadlocks'''
......@@ -307,13 +410,21 @@ class Deadlock(gdb.Command):
# Go through all the blocked threads and see which threads
# they are blocked on, and build the thread wait graph.
for thread_lwp in blocked_threads:
mutex_owner_lwp, mutex_address = get_mutex_owner_and_address(
for thread_lwp, mutex_type in blocked_threads.items():
get_owner_and_address_func = \
MutexType.get_mutex_owner_and_address_func_for_type(mutex_type)
if not get_owner_and_address_func:
continue
mutex_owner_lwp, mutex_address = get_owner_and_address_func(
lwp_to_thread_id, thread_lwp
)
if mutex_owner_lwp and mutex_address:
graph.add_edge(thread_lwp, mutex_owner_lwp,
mutex=mutex_address)
graph.add_edge(
thread_lwp,
mutex_owner_lwp,
mutex=mutex_address,
mutex_type=mutex_type
)
# A deadlock exists if there is a cycle in the graph.
cycle = find_cycle(graph)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment