Skip to content

Commit 14e173d

Browse files
basilclaude
andcommitted
Add core dump support to plgrp
Support core files as operands, bringing plgrp in line with the other tools (pargs, pauxv, pcred, penv, pfiles, psig). For core dumps, HOME shows ? (running CPU not captured by systemd-coredump) and CPU affinity is derived from Cpus_allowed_list in COREDUMP_PROC_STATUS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ed68fe9 commit 14e173d

File tree

2 files changed

+165
-53
lines changed

2 files changed

+165
-53
lines changed

build.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,14 @@ $ ptree -ag `pgrep ssh`
431431
name: "plgrp",
432432
about: "display home NUMA node and thread affinities",
433433
description: "Display the home NUMA node for each thread in the specified \
434-
processes. The home node is the NUMA node of the CPU on which the \
435-
thread is currently running. With the -a option, also display \
436-
whether each thread's CPU affinity includes CPUs on the requested \
437-
nodes.",
438-
synopsis: "[-a node_list] pid[/tid] ...",
434+
processes or process core files. The home node is the NUMA node \
435+
of the CPU on which the thread is currently running. With the -a \
436+
option, also display whether each thread's CPU affinity includes \
437+
CPUs on the requested nodes. For core files, the HOME column \
438+
shows ? because the running CPU is not captured by \
439+
systemd-coredump(8), and CPU affinity is derived from \
440+
Cpus_allowed_list in the saved process status.",
441+
synopsis: "[-a node_list] [pid[/tid] | core] ...",
439442
options: &[(
440443
"-a node_list",
441444
"Display affinity information for the specified NUMA nodes. \
@@ -445,7 +448,20 @@ $ ptree -ag `pgrep ssh`
445448
bound if the thread's CPU affinity mask includes any CPU on \
446449
that node, or none otherwise.",
447450
)],
448-
operands: &[],
451+
operands: &[
452+
(
453+
"pid[/tid]",
454+
"Process ID, optionally followed by a slash and a thread ID \
455+
to display a single thread.",
456+
),
457+
(
458+
"core",
459+
"Process core file, as produced by systemd-coredump(8). The core file \
460+
does not need to exist on disk; if it has been removed, the \
461+
corresponding systemd journal entry will be used instead. See \
462+
NOTES below.",
463+
),
464+
],
449465
examples: &[
450466
Example {
451467
title: "Example 1 Display home nodes",
@@ -468,9 +484,13 @@ $ plgrp -a 0-2 101398
468484
exit_status: DEFAULT_EXIT_STATUS,
469485
files: "/proc/pid/task/tid/stat\tThread scheduling information.\n\
470486
/sys/devices/system/node/\tNUMA topology information.",
471-
notes: "",
472-
see_also: "taskset(1), numactl(8), sched_getaffinity(2), proc(5)",
473-
warnings: "",
487+
notes: CORE_NOTES,
488+
see_also: "taskset(1), numactl(8), coredumpctl(1), sched_getaffinity(2), proc(5)",
489+
warnings: "For core files, the HOME column always shows ? because \
490+
systemd-coredump(8) does not capture which CPU each thread \
491+
was running on at the time of the crash. Only the main thread \
492+
is available from core files; information for other threads \
493+
is not displayed.",
474494
},
475495
out_dir,
476496
);

src/bin/plgrp.rs

Lines changed: 136 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@
1414
// limitations under the License.
1515
//
1616

17+
use std::path::PathBuf;
1718
use std::process;
1819

1920
use nix::sched::{sched_getaffinity, CpuSet};
2021
use nix::unistd::Pid;
2122

2223
use ptools::{
2324
cpu_to_node, enumerate_tids_from, numa_node_cpus, numa_online_nodes, parse_pid_spec,
24-
LiveProcess, ProcSource,
25+
CoredumpSource, LiveProcess, ProcSource,
2526
};
2627

28+
enum Operand {
29+
Live(ptools::PidSpec),
30+
Core(Box<dyn ProcSource>),
31+
}
32+
2733
/// Get the CPU number a thread is currently running on (field 39 of /proc/PID/task/TID/stat).
2834
fn get_thread_cpu(source: &dyn ProcSource, tid: u64) -> Option<u32> {
2935
let stat = source.read_tid_stat(tid).ok()?;
@@ -117,14 +123,31 @@ fn thread_has_affinity_for_node(affinity: &CpuSet, node: u32) -> bool {
117123
.any(|&cpu| affinity.is_set(cpu as usize).unwrap_or(false))
118124
}
119125

126+
/// Check whether a list of allowed CPUs includes any CPU on the given node.
127+
fn cpus_include_node(allowed: &[u32], node: u32) -> bool {
128+
let Ok(cpus) = numa_node_cpus(node) else {
129+
return false;
130+
};
131+
cpus.iter().any(|cpu| allowed.contains(cpu))
132+
}
133+
134+
/// Extract Cpus_allowed_list from /proc status and parse it.
135+
fn get_affinity_from_status(source: &dyn ProcSource, tid: u64) -> Option<Vec<u32>> {
136+
let status = source.read_tid_status(tid).ok()?;
137+
let line = status
138+
.lines()
139+
.find_map(|l| l.strip_prefix("Cpus_allowed_list:"))?;
140+
ptools::parse_list_format(line.trim()).ok()
141+
}
142+
120143
struct Args {
121144
affinity_nodes: Option<Vec<u32>>,
122145
had_bad_nodes: bool,
123-
specs: Vec<ptools::PidSpec>,
146+
operands: Vec<Operand>,
124147
}
125148

126149
fn print_usage() {
127-
eprintln!("Usage: plgrp [-a node_list] pid[/tid] ...");
150+
eprintln!("Usage: plgrp [-a node_list] [pid[/tid] | core] ...");
128151
eprintln!("Display home NUMA node and thread affinities.");
129152
eprintln!();
130153
eprintln!("Options:");
@@ -139,7 +162,7 @@ fn parse_args() -> Args {
139162
let mut args = Args {
140163
affinity_nodes: None,
141164
had_bad_nodes: false,
142-
specs: Vec::new(),
165+
operands: Vec::new(),
143166
};
144167
let mut parser = lexopt::Parser::from_env();
145168

@@ -176,10 +199,18 @@ fn parse_args() -> Args {
176199
Value(val) => {
177200
let s = val.to_string_lossy();
178201
match parse_pid_spec(&s) {
179-
Ok(spec) => args.specs.push(spec),
180-
Err(e) => {
181-
eprintln!("plgrp: {e}");
182-
process::exit(2);
202+
Ok(spec) => args.operands.push(Operand::Live(spec)),
203+
Err(_) => {
204+
let path = PathBuf::from(&*s);
205+
match CoredumpSource::from_corefile(&path) {
206+
Ok(source) => {
207+
args.operands.push(Operand::Core(Box::new(source)));
208+
}
209+
Err(e) => {
210+
eprintln!("plgrp: {}: {e}", path.display());
211+
process::exit(2);
212+
}
213+
}
183214
}
184215
}
185216
}
@@ -190,38 +221,65 @@ fn parse_args() -> Args {
190221
}
191222
}
192223

193-
if args.specs.is_empty() {
194-
eprintln!("plgrp: at least one pid[/tid] required");
224+
if args.operands.is_empty() {
225+
eprintln!("plgrp: at least one pid[/tid] or core required");
195226
process::exit(2);
196227
}
197228
args
198229
}
199230

200-
fn print_thread(source: &dyn ProcSource, tid: u64, affinity_nodes: &Option<Vec<u32>>) -> bool {
231+
fn print_thread(
232+
source: &dyn ProcSource,
233+
tid: u64,
234+
affinity_nodes: &Option<Vec<u32>>,
235+
is_coredump: bool,
236+
) -> bool {
201237
let pid = source.pid();
202-
let Some(cpu) = get_thread_cpu(source, tid) else {
203-
eprintln!("plgrp: cannot read CPU for {}/{}", pid, tid);
204-
return false;
238+
let home = if is_coredump {
239+
"?".to_string()
240+
} else {
241+
match get_thread_cpu(source, tid) {
242+
Some(cpu) => cpu_to_node(cpu)
243+
.map(|n| n.to_string())
244+
.unwrap_or_else(|| "?".to_string()),
245+
None => {
246+
eprintln!("plgrp: cannot read CPU for {}/{}", pid, tid);
247+
return false;
248+
}
249+
}
205250
};
206-
let home = cpu_to_node(cpu)
207-
.map(|n| n.to_string())
208-
.unwrap_or_else(|| "?".to_string());
209251

210252
let pid_tid = format!("{}/{}", pid, tid);
211253
if let Some(nodes) = affinity_nodes {
212-
let affinity = get_thread_affinity(tid);
213-
let aff_str: String = nodes
214-
.iter()
215-
.map(|&n| {
216-
let label = match &affinity {
217-
Some(cpuset) if thread_has_affinity_for_node(cpuset, n) => "bound",
218-
Some(_) => "none",
219-
None => "?",
220-
};
221-
format!("{}/{}", n, label)
222-
})
223-
.collect::<Vec<_>>()
224-
.join(",");
254+
let aff_str: String = if is_coredump {
255+
let allowed = get_affinity_from_status(source, tid);
256+
nodes
257+
.iter()
258+
.map(|&n| {
259+
let label = match &allowed {
260+
Some(cpus) if cpus_include_node(cpus, n) => "bound",
261+
Some(_) => "none",
262+
None => "?",
263+
};
264+
format!("{}/{}", n, label)
265+
})
266+
.collect::<Vec<_>>()
267+
.join(",")
268+
} else {
269+
let affinity = get_thread_affinity(tid);
270+
nodes
271+
.iter()
272+
.map(|&n| {
273+
let label = match &affinity {
274+
Some(cpuset) if thread_has_affinity_for_node(cpuset, n) => "bound",
275+
Some(_) => "none",
276+
None => "?",
277+
};
278+
format!("{}/{}", n, label)
279+
})
280+
.collect::<Vec<_>>()
281+
.join(",")
282+
};
225283
println!("{:>14} {:>4} {}", pid_tid, home, aff_str);
226284
} else {
227285
println!("{:>14} {:>4}", pid_tid, home);
@@ -240,22 +298,56 @@ fn main() {
240298
}
241299

242300
let mut error = false;
243-
for spec in &args.specs {
244-
let source = LiveProcess::new(spec.pid);
245-
if let Some(tid) = spec.tid {
246-
if !print_thread(&source, tid, &args.affinity_nodes) {
247-
error = true;
248-
}
249-
} else {
250-
let tids = enumerate_tids_from(&source);
251-
if tids.is_empty() {
252-
eprintln!("plgrp: cannot read threads for PID {}", spec.pid);
253-
error = true;
254-
continue;
301+
for operand in &args.operands {
302+
match operand {
303+
Operand::Live(spec) => {
304+
let source = LiveProcess::new(spec.pid);
305+
if let Some(tid) = spec.tid {
306+
if !print_thread(&source, tid, &args.affinity_nodes, false) {
307+
error = true;
308+
}
309+
} else {
310+
let tids = enumerate_tids_from(&source);
311+
if tids.is_empty() {
312+
eprintln!("plgrp: cannot read threads for PID {}", spec.pid);
313+
error = true;
314+
continue;
315+
}
316+
for tid in tids {
317+
if !print_thread(&source, tid, &args.affinity_nodes, false) {
318+
error = true;
319+
}
320+
}
321+
}
255322
}
256-
for tid in tids {
257-
if !print_thread(&source, tid, &args.affinity_nodes) {
323+
Operand::Core(source) => {
324+
let tids = enumerate_tids_from(source.as_ref());
325+
if tids.is_empty() {
326+
eprintln!("plgrp: cannot read threads for PID {}", source.pid());
258327
error = true;
328+
continue;
329+
}
330+
// Warn if the process had more threads than are available.
331+
if let Ok(status) = source.read_status() {
332+
if let Some(n) = status
333+
.lines()
334+
.find_map(|l| l.strip_prefix("Threads:"))
335+
.and_then(|v| v.trim().parse::<usize>().ok())
336+
{
337+
if n > tids.len() {
338+
eprintln!(
339+
"warning: process had {} threads but only {} available; \
340+
output may be incomplete",
341+
n,
342+
tids.len()
343+
);
344+
}
345+
}
346+
}
347+
for tid in tids {
348+
if !print_thread(source.as_ref(), tid, &args.affinity_nodes, true) {
349+
error = true;
350+
}
259351
}
260352
}
261353
}

0 commit comments

Comments
 (0)