Skip to content

Commit 557f137

Browse files
committed
drgn: add runq command
Signed-off-by: Richard Li <[email protected]>
1 parent b53e65b commit 557f137

File tree

2 files changed

+403
-0
lines changed

2 files changed

+403
-0
lines changed

drgn/commands/_builtin/crash/runq.py

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# Copyright (c) 2025, Oracle and/or its affiliates.
2+
# SPDX-License-Identifier: LGPL-2.1-or-later
3+
# This file contains code adapted from:
4+
# https://github.com/oracle-samples/drgn-tools (Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/)
5+
6+
"""
7+
crash runq - Display the tasks on the run queues of each cpu.
8+
9+
Implements the crash "runq" command for drgn
10+
"""
11+
12+
import argparse
13+
from typing import Any, Set
14+
15+
from drgn import Object, Program, cast, container_of
16+
from drgn.commands import argument, drgn_argument
17+
from drgn.commands.crash import crash_command
18+
from drgn.helpers.common.format import CellFormat, escape_ascii_string, print_table
19+
from drgn.helpers.linux.cpumask import for_each_online_cpu
20+
from drgn.helpers.linux.list import list_for_each_entry
21+
from drgn.helpers.linux.percpu import per_cpu
22+
23+
24+
def has_member(obj: Object, name: str) -> bool:
25+
"""
26+
Return true if a given object has a member with the given name.
27+
:param obj: Drgn object to check
28+
:param name: string member name to check
29+
:returns: whether the object has a member by that name
30+
"""
31+
try:
32+
obj.member_(name)
33+
return True
34+
except LookupError:
35+
return False
36+
37+
38+
def task_thread_info(task: Object) -> Object:
39+
"""
40+
Return a task's ``thread_info``
41+
42+
This is an equivalent to the kernel function / inline / macro
43+
``task_thread_info()``, but it must cover a wide variety of versions and
44+
configurations.
45+
46+
:param task: Object of type ``struct task_struct *``
47+
:returns: The ``struct thread_info *`` for this task
48+
"""
49+
if has_member(task, "thread_info"):
50+
return task.thread_info.address_of_()
51+
return cast("struct thread_info *", task.stack)
52+
53+
54+
def task_cpu(task: Object) -> int:
55+
"""
56+
Return the CPU on which a task is running.
57+
58+
This is an equivalent to the kernel function ``task_cpu()``, but it covers
59+
a wide variety of variations in kernel version and configuration. It would
60+
be a bit impractical to spell out all the variants, but essentially, if
61+
there's a "cpu" field in ``struct task_struct``, then we can just use that.
62+
Otherwise, we need to get it from the ``thread_info``.
63+
64+
:param task: Object of type ``struct task_struct *``
65+
:retruns: The cpu as a Python int
66+
"""
67+
if has_member(task, "cpu"):
68+
return task.cpu.value_()
69+
return task_thread_info(task).cpu.value_()
70+
71+
72+
def runq_clock(prog: Program, cpu: int) -> int:
73+
"""
74+
Get clock of cpu runqueue ``struct rq``
75+
76+
:param prog: drgn program
77+
:param cpu: cpu index
78+
:returns: cpu runqueue clock in ns granularity
79+
"""
80+
rq = per_cpu(prog["runqueues"], cpu)
81+
return rq.clock.value_()
82+
83+
84+
def get_task_arrival_time(task: Object) -> int:
85+
"""
86+
Get a task's arrival time on cpu
87+
88+
A task's arrival time is only updated when the task is put ON a cpu via
89+
context_switch.
90+
91+
:param task: ``struct task_struct *``
92+
:returns: arrival time instance in ns granularity
93+
"""
94+
95+
if has_member(task, "last_run"):
96+
arrival_time = task.last_run.value_()
97+
elif has_member(task, "timestamp"):
98+
arrival_time = task.timestamp.value_()
99+
else:
100+
arrival_time = task.sched_info.last_arrival.value_()
101+
102+
return arrival_time
103+
104+
105+
def task_lastrun2now(task: Object) -> int:
106+
"""
107+
Get the duration from task last run timestamp to now
108+
109+
The return duration will cover task's last run time on cpu and also
110+
the time staying in current status, usually the time slice for task
111+
on cpu will be short, so this can roughly tell how long this task
112+
has been staying in current status.
113+
For task status in "RU" status, if it's still on cpu, then this return
114+
the duration time this task has been running, otherwise it roughly tell
115+
how long this task has been staying in runqueue.
116+
117+
:param prog: drgn program
118+
:param task: ``struct task_struct *``
119+
:returns: duration in ns granularity
120+
"""
121+
prog = task.prog_
122+
arrival_time = get_task_arrival_time(task)
123+
rq_clock = runq_clock(prog, task_cpu(task))
124+
125+
return rq_clock - arrival_time
126+
127+
128+
def _parse_cpus_arg(cpus_arg: str, max_cpu: int) -> Set[int]:
129+
"""
130+
Parse argument to -c for cpu restriction (e.g. '1,3,5-8').
131+
132+
:param cpus_arg: str
133+
:param max_cpu: int
134+
:returns: a set of specified cpus
135+
"""
136+
cpus = set()
137+
for part in cpus_arg.split(","):
138+
part = part.strip()
139+
if "-" in part:
140+
start, end = part.split("-")
141+
for cpu in range(int(start), int(end) + 1):
142+
if 0 <= cpu < max_cpu:
143+
cpus.add(cpu)
144+
elif part:
145+
cpu = int(part)
146+
if 0 <= cpu < max_cpu:
147+
cpus.add(cpu)
148+
return cpus
149+
150+
151+
def _get_runqueue_timestamps(runqueue: Object) -> int:
152+
"""
153+
Get runqueue clock timestamp.
154+
155+
:param runque: Object
156+
:returns: rq timestamp
157+
"""
158+
# Try common fields in order; not all will exist on all kernels
159+
rq_ts = 0
160+
for name in ("clock", "most_recent_timestamp", "timestamp_last_tick"):
161+
if has_member(runqueue, name):
162+
try:
163+
rq_ts = getattr(runqueue, name).value_()
164+
break
165+
except Exception:
166+
pass
167+
return rq_ts
168+
169+
170+
def dump_rt_runq(runqueue: Object) -> None:
171+
"""
172+
Dump runq in rt scheduler
173+
174+
:param runque: Object
175+
"""
176+
count = 0
177+
prio_array = (
178+
hex(runqueue.rt.active.address_ - 16) if runqueue.rt.active.address_ else 0
179+
)
180+
print(" RT PRIO_ARRAY:", prio_array)
181+
rt_prio_array = runqueue.rt.active.queue
182+
for que in rt_prio_array:
183+
for t in list_for_each_entry(
184+
"struct sched_rt_entity", que.address_of_(), "run_list"
185+
):
186+
tsk = container_of(t, "struct task_struct", "rt")
187+
if tsk == runqueue.curr:
188+
continue
189+
count += 1
190+
print(
191+
" " * 4,
192+
'[{:3d}] PID: {:<6d} TASK: {} COMMAND: "{}"'.format(
193+
tsk.prio.value_(),
194+
tsk.pid.value_(),
195+
hex(tsk),
196+
escape_ascii_string(tsk.comm.string_()),
197+
),
198+
)
199+
if count == 0:
200+
print(" [no tasks queued]")
201+
202+
203+
def dump_cfs_runq(runqueue: Object, task_group: bool = False) -> None:
204+
"""
205+
Dump runq in cfs scheduler
206+
207+
:param runque: Object
208+
"""
209+
cfs_root = hex(runqueue.cfs.tasks_timeline.address_of_().value_())
210+
if not task_group:
211+
print(" CFS RB_ROOT:", cfs_root)
212+
count = 0
213+
runq = runqueue.address_of_()
214+
for t in list_for_each_entry(
215+
"struct task_struct", runq.cfs_tasks.address_of_(), "se.group_node"
216+
):
217+
if t == runqueue.curr:
218+
continue
219+
count += 1
220+
print(
221+
" " * 4,
222+
'[{:3d}] PID: {:<6d} TASK: {} COMMAND: "{}"'.format(
223+
t.prio.value_(),
224+
t.pid.value_(),
225+
hex(t),
226+
escape_ascii_string(t.comm.string_()),
227+
),
228+
)
229+
if count == 0:
230+
print(" [no tasks queued]")
231+
232+
233+
def timestamp_str(ns: int) -> str:
234+
"""Convert timestamp int to formatted str"""
235+
value = ns // 1000000
236+
ms = value % 1000
237+
value = value // 1000
238+
secs = value % 60
239+
value = value // 60
240+
mins = value % 60
241+
value = value // 60
242+
hours = value % 24
243+
days = value // 24
244+
return "%d %02d:%02d:%02d.%03d" % (days, hours, mins, secs, ms)
245+
246+
247+
def run_queue(prog: Program, args: argparse.Namespace) -> None:
248+
"""
249+
Print runqueue with detailed info.
250+
251+
:param prog: drgn program
252+
:param args: argparse Namespace
253+
"""
254+
online_cpus = list(for_each_online_cpu(prog))
255+
max_cpu = max(online_cpus) + 1 if online_cpus else 0
256+
257+
if args.cpus:
258+
selected_cpus = _parse_cpus_arg(args.cpus, max_cpu)
259+
cpus = [cpu for cpu in online_cpus if cpu in selected_cpus]
260+
else:
261+
cpus = online_cpus
262+
table_format = False
263+
if args.show_timestamps or args.show_lag or args.pretty_runtime:
264+
table_format = True
265+
table = []
266+
runq_clocks = {}
267+
for cpu, i in enumerate(cpus):
268+
runqueue = per_cpu(prog["runqueues"], cpu)
269+
curr_task_addr = runqueue.curr.value_()
270+
curr_task = runqueue.curr[0]
271+
run_time = task_lastrun2now(curr_task)
272+
if args.show_lag:
273+
runq_clocks[cpu] = runq_clock(prog, cpu)
274+
if i == len(cpus) - 1:
275+
max_clock = max(runq_clocks.values())
276+
lags = {
277+
cpu: max_clock - runq_clock
278+
for cpu, runq_clock in runq_clocks.items()
279+
}
280+
sorted_lags = dict(sorted(lags.items(), key=lambda item: item[1]))
281+
[
282+
print(f"CPU {cpu}: {lag/1e9:.2f} secs")
283+
for cpu, lag in sorted_lags.items()
284+
]
285+
return
286+
else:
287+
continue
288+
comm = escape_ascii_string(curr_task.comm.string_())
289+
pid = curr_task.pid.value_()
290+
prio = curr_task.prio.value_()
291+
292+
if table_format:
293+
row = [
294+
CellFormat(cpu, ">"),
295+
CellFormat(pid, ">"),
296+
CellFormat(curr_task_addr, "x"),
297+
CellFormat(prio, ">"),
298+
CellFormat(comm, "<"),
299+
]
300+
if args.pretty_runtime:
301+
row.append(CellFormat(timestamp_str(run_time), ">"))
302+
303+
if args.show_timestamps:
304+
rq_ts = _get_runqueue_timestamps(runqueue)
305+
task_ts = get_task_arrival_time(curr_task)
306+
# newest_rq_ts = max(rq_timestamps.values()) if rq_timestamps and args.show_lag else None
307+
308+
row += [
309+
CellFormat(f"{rq_ts:013d}", ">"),
310+
CellFormat(f"{task_ts:013d}", "<"),
311+
]
312+
313+
table.append(row)
314+
else:
315+
print(f"CPU {cpu} RUNQUEUE: {hex(runqueue.address_of_().value_())}")
316+
print(
317+
f" CURRENT: PID: {pid:<6d} TASK: {hex(curr_task_addr)} PRIO: {prio}"
318+
f' COMMAND: "{comm}"'
319+
# f" RUNTIME: {timestamp_str(run_time)}",
320+
)
321+
root_task_group_addr = prog["root_task_group"].address_of_().value_()
322+
if args.group:
323+
print(f" ROOT_TASK_GROUP: {hex(root_task_group_addr)}")
324+
print(
325+
" " * 4,
326+
'[{:3d}] PID: {:<6d} TASK: {} COMMAND: "{}" [CURRENT]'.format(
327+
prio,
328+
pid,
329+
hex(curr_task_addr),
330+
comm,
331+
),
332+
)
333+
334+
else:
335+
# RT PRIO_ARRAY
336+
dump_rt_runq(runqueue)
337+
# CFS RB_ROOT
338+
dump_cfs_runq(runqueue, args.group)
339+
print()
340+
continue
341+
headers = [
342+
CellFormat("CPU", "<"),
343+
CellFormat("PID", "<"),
344+
CellFormat("TASK", "<"),
345+
CellFormat("PRIO", "<"),
346+
CellFormat("COMMAND", "<"),
347+
]
348+
if args.show_timestamps:
349+
headers += [
350+
CellFormat("RQ_TIMESTAMP", "<"),
351+
CellFormat("TASK_TIMESTAMP", "<"),
352+
]
353+
if args.pretty_runtime:
354+
headers.append(CellFormat("RUNTIME", "<"))
355+
if table_format:
356+
print_table([headers] + table)
357+
358+
359+
@crash_command(
360+
description="Display the tasks on the run queues of each cpu.",
361+
arguments=(
362+
argument("-t", action="store_true", dest="show_timestamps"),
363+
argument("-T", action="store_true", dest="show_lag"),
364+
argument("-m", action="store_true", dest="pretty_runtime"),
365+
argument("-g", action="store_true", dest="group"),
366+
argument("-c", type=str, default="", dest="cpus"),
367+
drgn_argument,
368+
),
369+
)
370+
def _crash_cmd_runq(
371+
prog: Program, name: str, args: argparse.Namespace, **kwargs: Any
372+
) -> None:
373+
run_queue(prog, args)

0 commit comments

Comments
 (0)