use serde::{Deserialize, Serialize};
use super::btf_offsets::{RHT_PTR_LOCK_BIT, SCX_DSQ_LNODE_ITER_CURSOR, ScxWalkerOffsets};
use super::dump::TaskWalkerEntry;
use super::guest::GuestKernel;
use super::idr::translate_any_kva;
use super::reader::{GuestMem, WalkContext};
const MAX_NODES_PER_LIST: u32 = 4096;
const MAX_RHT_NODES: u32 = 8192;
const MAX_RHT_BUCKETS: u32 = 65_536;
const PER_BUCKET_CHAIN_CAP: u32 = 1024;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RqScxState {
pub cpu: u32,
pub nr_running: u32,
pub flags: u32,
pub cpu_released: bool,
pub ops_qseq: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kick_sync: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nr_immed: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rq_clock: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub curr_pid: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub curr_comm: Option<String>,
pub runnable_task_kvas: Vec<u64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub runnable_truncated: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DsqState {
pub id: u64,
pub origin: String,
pub nr: u32,
pub seq: u32,
pub task_kvas: Vec<u64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub truncated: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ScxSchedState {
pub aborting: bool,
pub bypass_depth: i32,
pub exit_kind: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watchdog_timeout: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sched_kva: Option<u64>,
}
#[allow(dead_code)]
pub fn walk_rq_scx(
kernel: &GuestKernel,
cpu: u32,
rq_kva: u64,
rq_pa: u64,
offsets: &ScxWalkerOffsets,
) -> Option<(RqScxState, Vec<TaskWalkerEntry>)> {
let rq_offs = offsets.rq.as_ref()?;
let scx_rq_offs = offsets.scx_rq.as_ref()?;
let task_offs = offsets.task.as_ref()?;
let mem = kernel.mem();
let walk = kernel.walk_context();
let scx_off = rq_offs.scx;
let nr_running = mem.read_u32(rq_pa, scx_off + scx_rq_offs.nr_running);
let flags = mem.read_u32(rq_pa, scx_off + scx_rq_offs.flags);
let cpu_released = mem.read_u8(rq_pa, scx_off + scx_rq_offs.cpu_released) != 0;
let ops_qseq = mem.read_u64(rq_pa, scx_off + scx_rq_offs.ops_qseq);
let kick_sync = scx_rq_offs
.kick_sync
.map(|off| mem.read_u64(rq_pa, scx_off + off));
let nr_immed = scx_rq_offs
.nr_immed
.map(|off| mem.read_u32(rq_pa, scx_off + off));
let rq_clock = scx_rq_offs
.clock
.map(|off| mem.read_u64(rq_pa, scx_off + off));
let curr_kva = mem.read_u64(rq_pa, rq_offs.curr);
let (curr_pid, curr_comm) =
read_task_pid_comm(mem, walk, curr_kva, task_offs.pid, task_offs.comm);
let (runnable_task_kvas, runnable_truncated) = if let Some(see_offs) = offsets.see.as_ref() {
let list_head_off = scx_off + scx_rq_offs.runnable_list;
let head_kva = rq_kva.wrapping_add(list_head_off as u64);
let head_pa = rq_pa.wrapping_add(list_head_off as u64);
let runnable_node_off_in_task = task_offs.scx + see_offs.runnable_node;
walk_list_head_for_task_kvas(mem, walk, head_kva, head_pa, runnable_node_off_in_task)
} else {
(Vec::new(), false)
};
let walker_entries: Vec<TaskWalkerEntry> = runnable_task_kvas
.iter()
.map(|&task_kva| TaskWalkerEntry {
task_kva,
is_runnable_in_scx: true,
running_pc: None,
})
.collect();
let state = RqScxState {
cpu,
nr_running,
flags,
cpu_released,
ops_qseq,
kick_sync,
nr_immed,
rq_clock,
curr_pid,
curr_comm,
runnable_task_kvas,
runnable_truncated,
};
Some((state, walker_entries))
}
#[allow(dead_code)]
pub fn read_scx_sched_state(
kernel: &GuestKernel,
scx_root_kva: u64,
offsets: &ScxWalkerOffsets,
) -> Option<(u64, ScxSchedState)> {
let Some(sched_offs) = offsets.sched.as_ref() else {
tracing::debug!(
"read_scx_sched_state: ScxSchedOffsets BTF sub-group missing — \
vmlinux lacks `struct scx_sched` (kernel without sched_ext or stripped vmlinux)",
);
return None;
};
let mem = kernel.mem();
let walk = kernel.walk_context();
if scx_root_kva == 0 {
tracing::debug!(
"read_scx_sched_state: scx_root_kva is 0 — vmlinux had no \
`scx_root` symbol (pre-6.16 kernel or stripped vmlinux)",
);
return None;
}
let root_pa = kernel.text_kva_to_pa(scx_root_kva);
let sched_kva = mem.read_u64(root_pa, 0);
if sched_kva == 0 {
tracing::debug!(
scx_root_kva = format_args!("{:#x}", scx_root_kva),
root_pa = format_args!("{:#x}", root_pa),
"read_scx_sched_state: *scx_root == 0 — no scheduler attached at the freeze instant",
);
return None;
}
let Some(sched_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
sched_kva,
walk.l5,
walk.tcr_el1,
) else {
tracing::debug!(
sched_kva = format_args!("{:#x}", sched_kva),
"read_scx_sched_state: translate_any_kva failed for sched_kva — \
page-table walk yielded no PA (slab page race or torn read)",
);
return None;
};
let aborting = sched_offs
.aborting
.map(|off| mem.read_u8(sched_pa, off) != 0)
.unwrap_or(false);
let bypass_depth = sched_offs
.bypass_depth
.map(|off| mem.read_u32(sched_pa, off) as i32)
.unwrap_or(0);
let exit_kind = mem.read_u32(sched_pa, sched_offs.exit_kind);
Some((
sched_kva,
ScxSchedState {
aborting,
bypass_depth,
exit_kind,
watchdog_timeout: None,
source: Some(SCX_SCHED_STATE_SOURCE_LIVE.to_string()),
sched_kva: Some(sched_kva),
},
))
}
pub const SCX_SCHED_STATE_SOURCE_LIVE: &str = "live";
pub const SCX_SCHED_STATE_SOURCE_BSS: &str = "bss_snapshot";
const SCX_TASK_CURSOR: u32 = 1 << 31;
#[allow(dead_code)]
pub fn walk_scx_tasks_global(
kernel: &GuestKernel,
scx_tasks_kva: u64,
tasks_node_off_in_task: usize,
tasks_node_off_in_see: usize,
flags_off_in_see: usize,
) -> Vec<u64> {
if scx_tasks_kva == 0 {
tracing::debug!(
"walk_scx_tasks_global: scx_tasks_kva is 0 — vmlinux had no \
`scx_tasks` symbol (kernel without sched_ext or stripped vmlinux)",
);
return Vec::new();
}
let mem = kernel.mem();
let walk = kernel.walk_context();
let head_kva = scx_tasks_kva;
let head_pa = kernel.text_kva_to_pa(scx_tasks_kva);
let mut task_kvas: Vec<u64> = Vec::new();
let mut node_kva = mem.read_u64(head_pa, 0);
if node_kva == 0 {
tracing::debug!(
scx_tasks_kva = format_args!("{:#x}", scx_tasks_kva),
head_pa = format_args!("{:#x}", head_pa),
"walk_scx_tasks_global: head.next read as 0 — list-head bytes \
unmapped or torn read; no tasks harvested",
);
return task_kvas;
}
let mut visited: u32 = 0;
while node_kva != head_kva {
if visited >= MAX_NODES_PER_LIST {
return task_kvas;
}
visited += 1;
let see_kva = node_kva.wrapping_sub(tasks_node_off_in_see as u64);
let cursor = match translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
see_kva,
walk.l5,
walk.tcr_el1,
) {
Some(see_pa) => {
let flags = mem.read_u32(see_pa, flags_off_in_see);
flags & SCX_TASK_CURSOR != 0
}
None => false,
};
if !cursor {
let task_kva = node_kva.wrapping_sub(tasks_node_off_in_task as u64);
task_kvas.push(task_kva);
}
let Some(node_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
node_kva,
walk.l5,
walk.tcr_el1,
) else {
return task_kvas;
};
let next_kva = mem.read_u64(node_pa, 0);
if next_kva == 0 {
return task_kvas;
}
node_kva = next_kva;
}
task_kvas
}
#[allow(dead_code)]
pub fn walk_local_dsqs(
kernel: &GuestKernel,
rq_kvas: &[u64],
rq_pas: &[u64],
per_cpu_offsets: &[u64],
offsets: &ScxWalkerOffsets,
) -> Option<(Vec<DsqState>, Vec<TaskWalkerEntry>)> {
let Some(rq_offs) = offsets.rq.as_ref() else {
tracing::debug!(
"walk_local_dsqs: ScxWalkerOffsets.rq sub-group missing — \
local DSQ pass blinded",
);
return None;
};
let Some(scx_rq_offs) = offsets.scx_rq.as_ref() else {
tracing::debug!(
"walk_local_dsqs: ScxWalkerOffsets.scx_rq sub-group missing — \
local DSQ pass blinded",
);
return None;
};
let Some(dsq_offs) = offsets.dsq.as_ref() else {
tracing::debug!(
"walk_local_dsqs: ScxWalkerOffsets.dsq sub-group missing — \
local DSQ pass blinded",
);
return None;
};
let Some(dsq_lnode_offs) = offsets.dsq_lnode.as_ref() else {
tracing::debug!(
"walk_local_dsqs: ScxWalkerOffsets.dsq_lnode sub-group missing — \
local DSQ pass blinded",
);
return None;
};
let Some(task_offs) = offsets.task.as_ref() else {
tracing::debug!(
"walk_local_dsqs: ScxWalkerOffsets.task sub-group missing — \
local DSQ pass blinded",
);
return None;
};
let Some(see_offs) = offsets.see.as_ref() else {
tracing::debug!(
"walk_local_dsqs: ScxWalkerOffsets.see sub-group missing — \
local DSQ pass blinded",
);
return None;
};
let mem = kernel.mem();
let walk = kernel.walk_context();
let mut states: Vec<DsqState> = Vec::new();
let mut entries: Vec<TaskWalkerEntry> = Vec::new();
for (cpu, (&rq_kva, &rq_pa)) in rq_kvas.iter().zip(rq_pas.iter()).enumerate() {
let cpu_off = per_cpu_offsets.get(cpu).copied();
match cpu_off {
Some(off) if off == 0 && cpu > 0 => continue,
None if cpu > 0 => continue,
_ => {}
}
let local_dsq_off = rq_offs.scx + scx_rq_offs.local_dsq;
let dsq_kva = rq_kva.wrapping_add(local_dsq_off as u64);
let dsq_pa = rq_pa.wrapping_add(local_dsq_off as u64);
if let Some((state, e)) = walk_one_dsq(
mem,
walk,
dsq_kva,
dsq_pa,
|| format!("local cpu {cpu}"),
dsq_offs,
dsq_lnode_offs,
task_offs,
see_offs,
) {
entries.extend(e);
states.push(state);
}
}
Some((states, entries))
}
#[allow(dead_code)]
pub fn walk_dsqs(
kernel: &GuestKernel,
sched_pa: u64,
per_cpu_offsets: &[u64],
nr_nodes: u32,
offsets: &ScxWalkerOffsets,
) -> (Vec<DsqState>, Vec<TaskWalkerEntry>) {
let mem = kernel.mem();
let walk = kernel.walk_context();
let mut dsq_states: Vec<DsqState> = Vec::new();
let mut all_entries: Vec<TaskWalkerEntry> = Vec::new();
let (Some(dsq_offs), Some(dsq_lnode_offs), Some(task_offs), Some(see_offs)) = (
offsets.dsq.as_ref(),
offsets.dsq_lnode.as_ref(),
offsets.task.as_ref(),
offsets.see.as_ref(),
) else {
return (dsq_states, all_entries);
};
if let (Some(sched_offs), Some(pcpu_offs)) =
(offsets.sched.as_ref(), offsets.sched_pcpu.as_ref())
&& let (Some(sched_pcpu_off), Some(bypass_dsq_off)) =
(sched_offs.pcpu, pcpu_offs.bypass_dsq)
{
let pcpu_kva = mem.read_u64(sched_pa, sched_pcpu_off);
if pcpu_kva != 0 {
for (cpu, &cpu_off) in per_cpu_offsets.iter().enumerate() {
if cpu_off == 0 && cpu > 0 {
continue;
}
let dsq_kva = pcpu_kva
.wrapping_add(cpu_off)
.wrapping_add(bypass_dsq_off as u64);
if let Some(dsq_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
dsq_kva,
walk.l5,
walk.tcr_el1,
) && let Some((state, entries)) = walk_one_dsq(
mem,
walk,
dsq_kva,
dsq_pa,
|| format!("bypass cpu {cpu}"),
dsq_offs,
dsq_lnode_offs,
task_offs,
see_offs,
) {
all_entries.extend(entries);
dsq_states.push(state);
}
}
}
}
if let (Some(sched_offs), Some(pnode_offs)) =
(offsets.sched.as_ref(), offsets.sched_pnode.as_ref())
&& let (Some(sched_pnode_off), Some(global_dsq_off)) =
(sched_offs.pnode, pnode_offs.global_dsq)
{
let pnode_kva = mem.read_u64(sched_pa, sched_pnode_off);
if pnode_kva != 0
&& let Some(pnode_arr_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
pnode_kva,
walk.l5,
walk.tcr_el1,
)
{
for node in 0..nr_nodes as u64 {
let pnode_ptr_kva = mem.read_u64(pnode_arr_pa, (node * 8) as usize);
if pnode_ptr_kva == 0 {
continue;
}
let Some(pnode_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
pnode_ptr_kva,
walk.l5,
walk.tcr_el1,
) else {
continue;
};
let dsq_kva = pnode_ptr_kva.wrapping_add(global_dsq_off as u64);
let dsq_pa = pnode_pa.wrapping_add(global_dsq_off as u64);
if let Some((state, entries)) = walk_one_dsq(
mem,
walk,
dsq_kva,
dsq_pa,
|| format!("global node {node}"),
dsq_offs,
dsq_lnode_offs,
task_offs,
see_offs,
) {
all_entries.extend(entries);
dsq_states.push(state);
}
}
}
}
if let (Some(sched_offs), Some(rht_offs)) = (offsets.sched.as_ref(), offsets.rht.as_ref()) {
let rht_pa = sched_pa.wrapping_add(sched_offs.dsq_hash as u64);
let (user_dsqs, user_dsqs_truncated) =
walk_user_dsq_hash(mem, walk, rht_pa, rht_offs, dsq_offs);
if user_dsqs_truncated {
tracing::warn!(
visited = user_dsqs.len(),
cap_buckets = MAX_RHT_BUCKETS,
cap_nodes = MAX_RHT_NODES,
"walk_user_dsq_hash: truncated — bucket-table or node cap fired; \
dsq_kvas list is incomplete",
);
}
for dsq_kva in user_dsqs {
let Some(dsq_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
dsq_kva,
walk.l5,
walk.tcr_el1,
) else {
continue;
};
if let Some((state, entries)) = walk_one_dsq(
mem,
walk,
dsq_kva,
dsq_pa,
|| "user".to_string(),
dsq_offs,
dsq_lnode_offs,
task_offs,
see_offs,
) {
all_entries.extend(entries);
dsq_states.push(state);
}
}
}
(dsq_states, all_entries)
}
#[allow(clippy::too_many_arguments)]
fn walk_one_dsq(
mem: &GuestMem,
walk: WalkContext,
dsq_kva: u64,
dsq_pa: u64,
origin: impl FnOnce() -> String,
dsq_offs: &super::btf_offsets::ScxDispatchQOffsets,
dsq_lnode_offs: &super::btf_offsets::ScxDsqListNodeOffsets,
task_offs: &super::btf_offsets::TaskStructCoreOffsets,
see_offs: &super::btf_offsets::SchedExtEntityOffsets,
) -> Option<(DsqState, Vec<TaskWalkerEntry>)> {
if dsq_pa == 0 {
tracing::debug!(
dsq_kva = format_args!("{:#x}", dsq_kva),
"walk_one_dsq: dsq_pa == 0 — would alias the boot page; \
skipping to avoid surfacing phantom all-zero DSQ state",
);
return None;
}
let origin = origin();
let id = mem.read_u64(dsq_pa, dsq_offs.id);
let nr = mem.read_u32(dsq_pa, dsq_offs.nr);
let seq = mem.read_u32(dsq_pa, dsq_offs.seq);
let head_kva = dsq_kva.wrapping_add(dsq_offs.list as u64);
let head_pa = dsq_pa.wrapping_add(dsq_offs.list as u64);
let dsq_node_off_in_task = task_offs.scx + see_offs.dsq_list + dsq_lnode_offs.node;
let (task_kvas, truncated) = walk_list_head_for_dsq_task_kvas(
mem,
walk,
head_kva,
head_pa,
dsq_node_off_in_task,
dsq_lnode_offs,
);
let entries: Vec<TaskWalkerEntry> = task_kvas
.iter()
.map(|&task_kva| TaskWalkerEntry {
task_kva,
is_runnable_in_scx: false,
running_pc: None,
})
.collect();
Some((
DsqState {
id,
origin,
nr,
seq,
task_kvas,
truncated,
},
entries,
))
}
fn walk_list_head_for_task_kvas(
mem: &GuestMem,
walk: WalkContext,
head_kva: u64,
head_pa: u64,
runnable_node_off_in_task: usize,
) -> (Vec<u64>, bool) {
let mut task_kvas = Vec::new();
let mut node_kva = mem.read_u64(head_pa, 0);
if node_kva == 0 {
return (task_kvas, false);
}
let mut visited: u32 = 0;
while node_kva != head_kva {
if visited >= MAX_NODES_PER_LIST {
return (task_kvas, true);
}
visited += 1;
let task_kva = node_kva.wrapping_sub(runnable_node_off_in_task as u64);
task_kvas.push(task_kva);
let Some(node_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
node_kva,
walk.l5,
walk.tcr_el1,
) else {
return (task_kvas, false);
};
let next_kva = mem.read_u64(node_pa, 0);
if next_kva == 0 {
return (task_kvas, false);
}
node_kva = next_kva;
}
(task_kvas, false)
}
fn walk_list_head_for_dsq_task_kvas(
mem: &GuestMem,
walk: WalkContext,
head_kva: u64,
head_pa: u64,
dsq_node_off_in_task: usize,
dsq_lnode_offs: &super::btf_offsets::ScxDsqListNodeOffsets,
) -> (Vec<u64>, bool) {
let mut task_kvas = Vec::new();
let mut node_kva = mem.read_u64(head_pa, 0);
if node_kva == 0 {
return (task_kvas, false);
}
let mut visited: u32 = 0;
while node_kva != head_kva {
if visited >= MAX_NODES_PER_LIST {
return (task_kvas, true);
}
visited += 1;
let lnode_kva = node_kva.wrapping_sub(dsq_lnode_offs.node as u64);
let is_cursor = match translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
lnode_kva,
walk.l5,
walk.tcr_el1,
) {
Some(lnode_pa) => {
let lnode_flags = mem.read_u32(lnode_pa, dsq_lnode_offs.flags);
Some(lnode_flags & SCX_DSQ_LNODE_ITER_CURSOR != 0)
}
None => None,
};
let skip_entry = match is_cursor {
Some(true) => true, Some(false) => false, None => true, };
if !skip_entry {
let task_kva = node_kva.wrapping_sub(dsq_node_off_in_task as u64);
task_kvas.push(task_kva);
}
let Some(node_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
node_kva,
walk.l5,
walk.tcr_el1,
) else {
return (task_kvas, false);
};
let next_kva = mem.read_u64(node_pa, 0);
if next_kva == 0 {
return (task_kvas, false);
}
node_kva = next_kva;
}
(task_kvas, false)
}
fn walk_user_dsq_hash(
mem: &GuestMem,
walk: WalkContext,
rht_pa: u64,
rht_offs: &super::btf_offsets::RhashtableOffsets,
dsq_offs: &super::btf_offsets::ScxDispatchQOffsets,
) -> (Vec<u64>, bool) {
let mut dsq_kvas = Vec::new();
let tbl_kva = mem.read_u64(rht_pa, rht_offs.tbl);
if tbl_kva == 0 {
return (dsq_kvas, false);
}
let Some(tbl_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
tbl_kva,
walk.l5,
walk.tcr_el1,
) else {
return (dsq_kvas, false);
};
let size = mem.read_u32(tbl_pa, rht_offs.bucket_table_size);
let bucket_count = size.min(MAX_RHT_BUCKETS) as u64;
let mut truncated = size as u64 > bucket_count;
let buckets_off = rht_offs.bucket_table_buckets;
let mut total_nodes: u32 = 0;
for i in 0..bucket_count {
if total_nodes >= MAX_RHT_NODES {
return (dsq_kvas, true);
}
let entry_off = buckets_off + (i as usize) * 8;
let raw_ptr = mem.read_u64(tbl_pa, entry_off);
let head_kva = raw_ptr & !RHT_PTR_LOCK_BIT;
if head_kva == 0 {
continue;
}
let mut node_kva = head_kva;
let mut chain_visited: u32 = 0;
let mut chain_terminated_naturally = false;
while node_kva != 0 && total_nodes < MAX_RHT_NODES && chain_visited < PER_BUCKET_CHAIN_CAP {
chain_visited += 1;
total_nodes += 1;
let dsq_kva = node_kva.wrapping_sub(dsq_offs.hash_node as u64);
dsq_kvas.push(dsq_kva);
let Some(node_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
node_kva,
walk.l5,
walk.tcr_el1,
) else {
chain_terminated_naturally = true;
break;
};
let next_raw = mem.read_u64(node_pa, rht_offs.rhash_head_next);
if next_raw & RHT_PTR_LOCK_BIT != 0 || next_raw == 0 {
chain_terminated_naturally = true;
break;
}
node_kva = next_raw;
}
if !chain_terminated_naturally {
truncated = true;
}
}
(dsq_kvas, truncated)
}
fn read_task_pid_comm(
mem: &GuestMem,
walk: WalkContext,
task_kva: u64,
pid_off: usize,
comm_off: usize,
) -> (Option<i32>, Option<String>) {
if task_kva == 0 {
return (None, None);
}
let Some(task_pa) = translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
task_kva,
walk.l5,
walk.tcr_el1,
) else {
return (None, None);
};
let pid = mem.read_u32(task_pa, pid_off) as i32;
let mut buf = [0u8; 16];
mem.read_bytes(task_pa + comm_off as u64, &mut buf);
let n = buf.iter().position(|&b| b == 0).unwrap_or(16);
let comm = String::from_utf8_lossy(&buf[..n]).to_string();
(Some(pid), Some(comm))
}
#[cfg(test)]
#[path = "scx_walker_tests.rs"]
mod tests;