Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a drgn helper for qspinlocks #101

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions drgn_tools/locking.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Helper for linux kernel locking
"""
import enum
from collections import defaultdict
from typing import Iterable
from typing import Optional
from typing import Tuple
Expand All @@ -18,6 +19,7 @@
from drgn import Program
from drgn import StackFrame
from drgn.helpers import ValidationError
from drgn.helpers.linux.cpumask import for_each_online_cpu
from drgn.helpers.linux.list import list_empty
from drgn.helpers.linux.list import list_for_each_entry
from drgn.helpers.linux.list import validate_list_for_each_entry
Expand All @@ -27,8 +29,10 @@
from drgn.helpers.linux.sched import task_state_to_char

from drgn_tools.bt import bt
from drgn_tools.bt import frame_name
from drgn_tools.mm import AddrKind
from drgn_tools.table import FixedTable
from drgn_tools.task import get_command
from drgn_tools.task import get_current_run_time
from drgn_tools.task import task_lastrun2now
from drgn_tools.util import per_cpu_owner
Expand Down Expand Up @@ -658,3 +662,135 @@ def get_lock_from_frame(
if is_task_blocked_on_lock(pid, kind, lock):
return lock
return None


######################################
# qspinlock
######################################
_QSPINLOCK_UNLOCKED_VAL = 0


def qspinlock_is_locked(qsp: Object) -> bool:
"""
Check if a qspinlock is locked or not

:param qsp: ``struct qspinlock *``
:returns: True if qspinlock is locked, False otherwise.
"""
return qsp.locked.value_() != _QSPINLOCK_UNLOCKED_VAL


def get_qspinlock_tail_cpu(qsp: Object) -> int:
"""
Get tail cpu that spins on the qspinlock

:param qsp: ``struct qspinlock *``
:returns: tail cpu that spins on the qspinlock, -1 if None
"""
tail = qsp.tail.value_()
tail_cpu = (tail >> 2) - 1
return tail_cpu


def get_tail_cpu_qnode(qsp: Object) -> Iterable[Object]:
"""
Only for UEK6 and above.
Given a qspinlock, find qnodes associated with the tail cpu spining on the qspinlock.

:param qsp: ``struct qspinlock *``
:returns: Iterator of qnode
"""
tail_cpu = get_qspinlock_tail_cpu(qsp)
prog = qsp.prog_
if tail_cpu < 0:
return []
tail_qnodes = per_cpu(prog["qnodes"], tail_cpu)
for qnode in tail_qnodes:
yield qnode


def dump_qnode_address_for_each_cpu(prog: Program, cpu: int = -1) -> None:
"""
Only for UEK6 and above.
Dump all qnode addresses per cpu. If cpu is specified, dump qnode address on that cpu only.

:param prog: drgn program
:param cpu: cpu id
"""
print(
"%-20s %-20s"
% (
"cpu",
"qnode",
)
)
online_cpus = list(for_each_online_cpu(prog))
if cpu > -1:
if cpu in online_cpus:
qnode_addr = per_cpu(prog["qnodes"], cpu).address_of_().value_()
print("%-20s %-20lx" % (cpu, qnode_addr))
else:
for cpu_id in online_cpus:
qnode_addr = per_cpu(prog["qnodes"], cpu_id).address_of_().value_()
print("%-20s %-20lx" % (cpu_id, qnode_addr))


def scan_bt_for_spinlocks(
prog: Program, show_unlocked_only: bool = True
) -> None:
"""
Scan spinlocks on bt and dump their stats. Set show_unlocked_only to False to also include locked spinlocks stats.

:param prog: drgn program
:param show_unlocked_only: bool
"""
wait_on_spin_lock_key_words = {
"__pv_queued_spin_lock_slowpath",
"native_queued_spin_lock_slowpath",
}

# stores the lock status, running time, pid, spinlock address, task address, cpu and command
# dictionary is used here for formatting purpose later
# the maximum number of elements stored is at most the numer of CPUs
sp_lock_stats = defaultdict(list)

for cpu in for_each_online_cpu(prog):
task = cpu_curr(prog, cpu)
trace = prog.stack_trace(task)

for frame in trace:
if frame_name(prog, frame) in wait_on_spin_lock_key_words:
if "lock" in frame.locals():
sp = frame["lock"]
sp_addr = sp.value_()
is_locked = qspinlock_is_locked(sp)
run_time = timestamp_str(get_current_run_time(prog, cpu))
pid = task.pid.value_()
cmd = get_command(task)
task_addr = task.value_()

sp_lock_stats[(sp_addr, is_locked)].append(
(task_addr, pid, cpu, run_time, cmd)
)

if show_unlocked_only:
print("Dumping unlocked spinlocks stats only...")
for sp, stats in sp_lock_stats.items():
is_locked = sp[1]
if show_unlocked_only and is_locked:
continue
if show_unlocked_only:
print(
f"spinlock {hex(sp[0])} has {len(stats)} spinner(s), which are as follows: "
)
else:
print(
f"spinlock {hex(sp[0])} (is_locked = {is_locked}) has {len(stats)} spinner(s), which are as follows: "
)

tbl = FixedTable(
["TASK:>x", "PID:>", "CPU:>", "CURRENT SPINTIME:>", "COMMAND:>"]
)
for stat in stats:
tbl.row(stat[0], stat[1], stat[2], stat[3], stat[4])
tbl.write()
8 changes: 8 additions & 0 deletions tests/test_locking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) 2024, Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
from drgn_tool import locking


# test for qspinlock
def test_scan_bt_for_spinlocks(prog):
locking.scan_bt_for_spinlocks(prog, show_unlocked_only=False)