proxygen
deadlock.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 
3 from collections import defaultdict
4 from enum import Enum
5 import gdb
6 import re
7 
8 
9 class DiGraph(object):
10  '''
11  Adapted from networkx: http://networkx.github.io/
12  Represents a directed graph. Edges can store (key, value) attributes.
13  '''
14 
15  def __init__(self):
16  # Map of node -> set of nodes
17  self.adjacency_map = {}
18  # Map of (node1, node2) -> map string -> arbitrary attribute
19  # This will not be copied in subgraph()
20  self.attributes_map = {}
21 
22  def neighbors(self, node):
23  return self.adjacency_map.get(node, set())
24 
25  def edges(self):
26  edges = []
27  for node, neighbors in self.adjacency_map.items():
28  for neighbor in neighbors:
29  edges.append((node, neighbor))
30  return edges
31 
32  def nodes(self):
33  return self.adjacency_map.keys()
34 
35  def attributes(self, node1, node2):
36  return self.attributes_map[(node1, node2)]
37 
38  def add_edge(self, node1, node2, **kwargs):
39  if node1 not in self.adjacency_map:
40  self.adjacency_map[node1] = set()
41  if node2 not in self.adjacency_map:
42  self.adjacency_map[node2] = set()
43  self.adjacency_map[node1].add(node2)
44  self.attributes_map[(node1, node2)] = kwargs
45 
46  def remove_node(self, node):
47  self.adjacency_map.pop(node, None)
48  for _, neighbors in self.adjacency_map.items():
49  neighbors.discard(node)
50 
51  def subgraph(self, nodes):
52  graph = DiGraph()
53  for node in nodes:
54  for neighbor in self.neighbors(node):
55  if neighbor in nodes:
56  graph.add_edge(node, neighbor)
57  return graph
58 
59  def node_link_data(self):
60  '''
61  Returns the graph as a dictionary in a format that can be
62  serialized.
63  '''
64  data = {
65  'directed': True,
66  'multigraph': False,
67  'graph': {},
68  'links': [],
69  'nodes': [],
70  }
71 
72  # Do one pass to build a map of node -> position in nodes
73  node_to_number = {}
74  for node in self.adjacency_map.keys():
75  node_to_number[node] = len(data['nodes'])
76  data['nodes'].append({'id': node})
77 
78  # Do another pass to build the link information
79  for node, neighbors in self.adjacency_map.items():
80  for neighbor in neighbors:
81  link = self.attributes_map[(node, neighbor)].copy()
82  link['source'] = node_to_number[node]
83  link['target'] = node_to_number[neighbor]
84  data['links'].append(link)
85  return data
86 
87 
88 def strongly_connected_components(G): # noqa: C901
89  '''
90  Adapted from networkx: http://networkx.github.io/
91  Parameters
92  ----------
93  G : DiGraph
94  Returns
95  -------
96  comp : generator of sets
97  A generator of sets of nodes, one for each strongly connected
98  component of G.
99  '''
100  preorder = {}
101  lowlink = {}
102  scc_found = {}
103  scc_queue = []
104  i = 0 # Preorder counter
105  for source in G.nodes():
106  if source not in scc_found:
107  queue = [source]
108  while queue:
109  v = queue[-1]
110  if v not in preorder:
111  i = i + 1
112  preorder[v] = i
113  done = 1
114  v_nbrs = G.neighbors(v)
115  for w in v_nbrs:
116  if w not in preorder:
117  queue.append(w)
118  done = 0
119  break
120  if done == 1:
121  lowlink[v] = preorder[v]
122  for w in v_nbrs:
123  if w not in scc_found:
124  if preorder[w] > preorder[v]:
125  lowlink[v] = min([lowlink[v], lowlink[w]])
126  else:
127  lowlink[v] = min([lowlink[v], preorder[w]])
128  queue.pop()
129  if lowlink[v] == preorder[v]:
130  scc_found[v] = True
131  scc = {v}
132  while (
133  scc_queue and preorder[scc_queue[-1]] > preorder[v]
134  ):
135  k = scc_queue.pop()
136  scc_found[k] = True
137  scc.add(k)
138  yield scc
139  else:
140  scc_queue.append(v)
141 
142 
143 def simple_cycles(G): # noqa: C901
144  '''
145  Adapted from networkx: http://networkx.github.io/
146  Parameters
147  ----------
148  G : DiGraph
149  Returns
150  -------
151  cycle_generator: generator
152  A generator that produces elementary cycles of the graph.
153  Each cycle is represented by a list of nodes along the cycle.
154  '''
155 
156  def _unblock(thisnode, blocked, B):
157  stack = set([thisnode])
158  while stack:
159  node = stack.pop()
160  if node in blocked:
161  blocked.remove(node)
162  stack.update(B[node])
163  B[node].clear()
164 
165  # Johnson's algorithm requires some ordering of the nodes.
166  # We assign the arbitrary ordering given by the strongly connected comps
167  # There is no need to track the ordering as each node removed as processed.
168  # save the actual graph so we can mutate it here
169  # We only take the edges because we do not want to
170  # copy edge and node attributes here.
171  subG = G.subgraph(G.nodes())
172  sccs = list(strongly_connected_components(subG))
173  while sccs:
174  scc = sccs.pop()
175  # order of scc determines ordering of nodes
176  startnode = scc.pop()
177  # Processing node runs 'circuit' routine from recursive version
178  path = [startnode]
179  blocked = set() # vertex: blocked from search?
180  closed = set() # nodes involved in a cycle
181  blocked.add(startnode)
182  B = defaultdict(set) # graph portions that yield no elementary circuit
183  stack = [(startnode, list(subG.neighbors(startnode)))]
184  while stack:
185  thisnode, nbrs = stack[-1]
186  if nbrs:
187  nextnode = nbrs.pop()
188  if nextnode == startnode:
189  yield path[:]
190  closed.update(path)
191  elif nextnode not in blocked:
192  path.append(nextnode)
193  stack.append((nextnode, list(subG.neighbors(nextnode))))
194  closed.discard(nextnode)
195  blocked.add(nextnode)
196  continue
197  # done with nextnode... look for more neighbors
198  if not nbrs: # no more nbrs
199  if thisnode in closed:
200  _unblock(thisnode, blocked, B)
201  else:
202  for nbr in subG.neighbors(thisnode):
203  if thisnode not in B[nbr]:
204  B[nbr].add(thisnode)
205  stack.pop()
206  path.pop()
207  # done processing this node
208  subG.remove_node(startnode)
209  H = subG.subgraph(scc) # make smaller to avoid work in SCC routine
210  sccs.extend(list(strongly_connected_components(H)))
211 
212 
213 def find_cycle(graph):
214  '''
215  Looks for a cycle in the graph. If found, returns the first cycle.
216  If nodes a1, a2, ..., an are in a cycle, then this returns:
217  [(a1,a2), (a2,a3), ... (an-1,an), (an, a1)]
218  Otherwise returns an empty list.
219  '''
220  cycles = list(simple_cycles(graph))
221  if cycles:
222  nodes = cycles[0]
223  nodes.append(nodes[0])
224  edges = []
225  prev = nodes[0]
226  for node in nodes[1:]:
227  edges.append((prev, node))
228  prev = node
229  return edges
230  else:
231  return []
232 
233 
234 def get_stacktrace(thread_id):
235  '''
236  Returns the stack trace for the thread id as a list of strings.
237  '''
238  gdb.execute('thread %d' % thread_id, from_tty=False, to_string=True)
239  output = gdb.execute('bt', from_tty=False, to_string=True)
240  stacktrace_lines = output.strip().split('\n')
241  return stacktrace_lines
242 
243 
245  thread_id, top_line, expected_top_line, expected_frame
246 ):
247  '''
248  Returns True if we found expected_top_line in top_line, and
249  we found the expected_frame in the thread's stack trace.
250  '''
251  if expected_top_line not in top_line:
252  return False
253  stacktrace_lines = get_stacktrace(thread_id)
254  return any(expected_frame in line for line in stacktrace_lines)
255 
256 
257 class MutexType(Enum):
258  '''Types of mutexes that we can detect deadlocks.'''
259 
260  PTHREAD_MUTEX_T = 'pthread_mutex_t'
261  PTHREAD_RWLOCK_T = 'pthread_rwlock_t'
262 
263  @staticmethod
264  def get_mutex_type(thread_id, top_line):
265  '''
266  Returns the probable mutex type, based on the first line
267  of the thread's stack. Returns None if not found.
268  '''
269 
271  thread_id, top_line, '__lll_lock_wait', 'pthread_mutex'
272  ):
273  return MutexType.PTHREAD_MUTEX_T
275  thread_id, top_line, 'futex_wait', 'pthread_rwlock'
276  ):
277  return MutexType.PTHREAD_RWLOCK_T
278  return None
279 
280  @staticmethod
282  '''
283  Returns a function to resolve the mutex owner and address for
284  the given type. The returned function f has the following
285  signature:
286 
287  f: args: (map of thread lwp -> thread id), blocked thread lwp
288  returns: (lwp of thread owning mutex, mutex address)
289  or (None, None) if not found.
290 
291  Returns None if there is no function for this mutex_type.
292  '''
293  if mutex_type == MutexType.PTHREAD_MUTEX_T:
294  return get_pthread_mutex_t_owner_and_address
295  if mutex_type == MutexType.PTHREAD_RWLOCK_T:
296  return get_pthread_rwlock_t_owner_and_address
297  return None
298 
299 
300 def print_cycle(graph, lwp_to_thread_id, cycle):
301  '''Prints the threads and mutexes involved in the deadlock.'''
302  for (m, n) in cycle:
303  print(
304  'Thread %d (LWP %d) is waiting on %s (0x%016x) held by '
305  'Thread %d (LWP %d)' % (
306  lwp_to_thread_id[m], m,
307  graph.attributes(m, n)['mutex_type'].value,
308  graph.attributes(m, n)['mutex'], lwp_to_thread_id[n], n
309  )
310  )
311 
312 
314  '''
315  Returns a pair of:
316  - map of LWP -> thread ID
317  - map of blocked threads LWP -> potential mutex type
318  '''
319  # LWP -> thread ID
320  lwp_to_thread_id = {}
321 
322  # LWP -> potential mutex type it is blocked on
323  blocked_threads = {}
324 
325  output = gdb.execute('info threads', from_tty=False, to_string=True)
326  lines = output.strip().split('\n')[1:]
327  regex = re.compile(r'[\s\*]*(\d+).*Thread.*\(LWP (\d+)\).*')
328  for line in lines:
329  try:
330  thread_id = int(regex.match(line).group(1))
331  thread_lwp = int(regex.match(line).group(2))
332  lwp_to_thread_id[thread_lwp] = thread_id
333  mutex_type = MutexType.get_mutex_type(thread_id, line)
334  if mutex_type:
335  blocked_threads[thread_lwp] = mutex_type
336  except Exception:
337  continue
338 
339  return (lwp_to_thread_id, blocked_threads)
340 
341 
342 def get_pthread_mutex_t_owner_and_address(lwp_to_thread_id, thread_lwp):
343  '''
344  Finds the thread holding the mutex that this thread is blocked on.
345  Returns a pair of (lwp of thread owning mutex, mutex address),
346  or (None, None) if not found.
347  '''
348  # Go up the stack to the pthread_mutex_lock frame
349  gdb.execute(
350  'thread %d' % lwp_to_thread_id[thread_lwp],
351  from_tty=False,
352  to_string=True
353  )
354  gdb.execute('frame 1', from_tty=False, to_string=True)
355 
356  # Get the owner of the mutex by inspecting the internal
357  # fields of the mutex.
358  try:
359  mutex_info = gdb.parse_and_eval('mutex').dereference()
360  mutex_owner_lwp = int(mutex_info['__data']['__owner'])
361  return (mutex_owner_lwp, int(mutex_info.address))
362  except gdb.error:
363  return (None, None)
364 
365 
366 def get_pthread_rwlock_t_owner_and_address(lwp_to_thread_id, thread_lwp):
367  '''
368  If the thread is waiting on a write-locked pthread_rwlock_t, this will
369  return the pair of:
370  (lwp of thread that is write-owning the mutex, mutex address)
371  or (None, None) if not found, or if the mutex is read-locked.
372  '''
373  # Go up the stack to the pthread_rwlock_{rd|wr}lock frame
374  gdb.execute(
375  'thread %d' % lwp_to_thread_id[thread_lwp],
376  from_tty=False,
377  to_string=True
378  )
379  gdb.execute('frame 2', from_tty=False, to_string=True)
380 
381  # Get the owner of the mutex by inspecting the internal
382  # fields of the mutex.
383  try:
384  rwlock_info = gdb.parse_and_eval('rwlock').dereference()
385  rwlock_owner_lwp = int(rwlock_info['__data']['__writer'])
386  # We can only track the owner if it is currently write-locked.
387  # If it is not write-locked or if it is currently read-locked,
388  # possibly by multiple threads, we cannot find the owner.
389  if rwlock_owner_lwp != 0:
390  return (rwlock_owner_lwp, int(rwlock_info.address))
391  else:
392  return (None, None)
393  except gdb.error:
394  return (None, None)
395 
396 
397 class Deadlock(gdb.Command):
398  '''Detects deadlocks'''
399 
400  def __init__(self):
401  super(Deadlock, self).__init__('deadlock', gdb.COMMAND_NONE)
402 
403  def invoke(self, arg, from_tty):
404  '''Prints the threads and mutexes in a deadlock, if it exists.'''
405  lwp_to_thread_id, blocked_threads = get_thread_info()
406 
407  # Nodes represent threads. Edge (A,B) exists if thread A
408  # is waiting on a mutex held by thread B.
409  graph = DiGraph()
410 
411  # Go through all the blocked threads and see which threads
412  # they are blocked on, and build the thread wait graph.
413  for thread_lwp, mutex_type in blocked_threads.items():
414  get_owner_and_address_func = \
415  MutexType.get_mutex_owner_and_address_func_for_type(mutex_type)
416  if not get_owner_and_address_func:
417  continue
418  mutex_owner_lwp, mutex_address = get_owner_and_address_func(
419  lwp_to_thread_id, thread_lwp
420  )
421  if mutex_owner_lwp and mutex_address:
422  graph.add_edge(
423  thread_lwp,
424  mutex_owner_lwp,
425  mutex=mutex_address,
426  mutex_type=mutex_type
427  )
428 
429  # A deadlock exists if there is a cycle in the graph.
430  cycle = find_cycle(graph)
431  if cycle:
432  print('Found deadlock!')
433  print_cycle(graph, lwp_to_thread_id, cycle)
434  else:
435  print(
436  'No deadlock detected. '
437  'Do you have debug symbols installed?'
438  )
439 
440 
441 def load():
442  # instantiate the Deadlock command
443  Deadlock()
444  print('Type "deadlock" to detect deadlocks.')
445 
446 
447 def info():
448  return 'Detect deadlocks'
449 
450 
451 if __name__ == '__main__':
452  load()
def info()
Definition: deadlock.py:447
void * object
Definition: AtFork.cpp:32
def edges(self)
Definition: deadlock.py:25
def get_pthread_rwlock_t_owner_and_address(lwp_to_thread_id, thread_lwp)
Definition: deadlock.py:366
def get_thread_info()
Definition: deadlock.py:313
def get_mutex_type(thread_id, top_line)
Definition: deadlock.py:264
auto add
Definition: BaseTest.cpp:70
def __init__(self)
Definition: deadlock.py:15
def neighbors(self, node)
Definition: deadlock.py:22
void append(std::unique_ptr< IOBuf > &buf, StringPiece str)
Definition: IOBufTest.cpp:37
def print_cycle(graph, lwp_to_thread_id, cycle)
Definition: deadlock.py:300
def load()
Definition: deadlock.py:441
def node_link_data(self)
Definition: deadlock.py:59
def attributes(self, node1, node2)
Definition: deadlock.py:35
Composed any(Predicate pred=Predicate())
Definition: Base.h:758
def __init__(self)
Definition: deadlock.py:400
constexpr std::decay< T >::type copy(T &&value) noexcept(noexcept(typename std::decay< T >::type(std::forward< T >(value))))
Definition: Utility.h:72
def invoke(self, arg, from_tty)
Definition: deadlock.py:403
def strongly_connected_components(G)
Definition: deadlock.py:88
def get_pthread_mutex_t_owner_and_address(lwp_to_thread_id, thread_lwp)
Definition: deadlock.py:342
LogLevel min
Definition: LogLevel.cpp:30
Encoder::MutableCompressedList list
S split(const StringPiece source, char delimiter)
Definition: String.h:61
def is_thread_blocked_with_frame(thread_id, top_line, expected_top_line, expected_frame)
Definition: deadlock.py:246
def get_mutex_owner_and_address_func_for_type(mutex_type)
Definition: deadlock.py:281
def find_cycle(graph)
Definition: deadlock.py:213
def remove_node(self, node)
Definition: deadlock.py:46
Optional< NamedGroup > group
def nodes(self)
Definition: deadlock.py:32
def add_edge(self, node1, node2, kwargs)
Definition: deadlock.py:38
def subgraph(self, nodes)
Definition: deadlock.py:51
Definition: Traits.h:592
def simple_cycles(G)
Definition: deadlock.py:143
def get_stacktrace(thread_id)
Definition: deadlock.py:234
constexpr detail::Dereference dereference
Definition: Base-inl.h:2575