kernel: Linux-compatible /proc/[pid]/stat format

Implements proper /proc/[pid]/stat output matching Linux fs/proc/array.c format:

- utime/stime: separate user/kernel CPU time accounting in context switch
- pgrp/session/start_time: process group, session, boot-time start
- proc_rss(): Resident Set Size from page table grant walker
- num_threads: thread-group counting via owner_proc_id filter
- nice: derived from priority (prio/2 - 20)
- proc_stat_line(): proper 52-field /proc/[pid]/stat format
- proc_status_text(): VmSize/VmRSS in kB

Infrastructure:
- require_zero_offset(): EINVAL for non-zero read/write offsets
- validate_kfmap_flags(): KFMAP flag validation (MAP_SHARED/PRIVATE)
- ContextHandle::Ctx: atomic FullContextRegs save/restore
- kwriteoff: offset parameter for regs/ctx/attr/start writes
- try_stop_context: preserve HardBlocked::NotYetStarted status on restore
This commit is contained in:
2026-07-08 17:47:20 +03:00
parent eba85456c1
commit ae03a7e60b
4 changed files with 243 additions and 19 deletions
+15
View File
@@ -100,6 +100,10 @@ pub struct Context {
pub switch_time: u128,
/// Amount of CPU time used
pub cpu_time: u128,
/// User-mode CPU time used
pub utime: u128,
/// Kernel-mode CPU time used
pub stime: u128,
/// Scheduler CPU affinity. If set, [`cpu_id`] can except [`None`] never be anything else than
/// this value.
pub sched_affinity: LogicalCpuSet,
@@ -140,6 +144,12 @@ pub struct Context {
pub fmap_ret: Option<Frame>,
/// Priority
pub prio: usize,
/// Process group ID.
pub pgrp: usize,
/// Session ID.
pub session: usize,
/// Context start time in nanoseconds since boot.
pub start_time: u128,
// TODO: id can reappear after wraparound?
pub owner_proc_id: Option<NonZeroUsize>,
@@ -186,6 +196,8 @@ impl Context {
cpu_id: None,
switch_time: 0,
cpu_time: 0,
utime: 0,
stime: 0,
sched_affinity: LogicalCpuSet::all(),
inside_syscall: false,
syscall_head: SyscallFrame::Free(RaiiFrame::allocate()?),
@@ -207,6 +219,9 @@ impl Context {
egid: 0,
pid: 0,
groups: Vec::new(),
pgrp: 0,
session: 0,
start_time: 0,
#[cfg(feature = "syscall_debug")]
syscall_debug_info: crate::syscall::debug::SyscallDebugInfo::default(),
+4
View File
@@ -131,6 +131,7 @@ pub fn init(token: &mut CleanLockToken) {
context.status = Status::Runnable;
context.running = true;
context.cpu_id = Some(crate::cpu_id());
context.start_time = crate::time::monotonic(token);
let context_lock = Arc::new(ContextLock::new(context));
@@ -223,6 +224,9 @@ pub fn spawn(
let stack = Kstack::new()?;
let mut context = Context::new(owner_proc_id)?;
context.start_time = crate::time::monotonic(token);
context.pgrp = context.pid;
context.session = context.pid;
let _ = context.set_addr_space(Some(AddrSpaceWrapper::new()?), token.downgrade());
context
+7 -1
View File
@@ -221,7 +221,13 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
// Update times
if !was_idle {
prev_context.cpu_time += switch_time.saturating_sub(prev_context.switch_time);
let delta = switch_time.saturating_sub(prev_context.switch_time);
prev_context.cpu_time += delta;
if prev_context.userspace {
prev_context.utime += delta;
} else {
prev_context.stime += delta;
}
}
next_context.switch_time = switch_time;
if next_context.userspace {
+217 -18
View File
@@ -89,7 +89,9 @@ fn try_stop_context<T>(
let (context, token) = context.token_split();
let ret = callback(context, token);
context.status = prev_status;
if matches!(context.status, Status::HardBlocked { reason: HardBlockedReason::NotYetStarted }) {
context.status = prev_status;
}
ret
}
@@ -112,7 +114,9 @@ enum ContextHandle {
privileged: bool,
}, // can write ContextVerb
/// Deprecated compatibility handles; prefer `ctx` for atomic save/restore.
Regs(RegsKind),
Ctx,
Sighandler,
Start,
NewFiletable {
@@ -237,6 +241,54 @@ fn proc_vsize(addr_space: &Option<Arc<AddrSpaceWrapper>>, token: &mut CleanLockT
.unwrap_or(0)
}
fn proc_rss(addr_space: &Option<Arc<AddrSpaceWrapper>>, token: &mut CleanLockToken) -> usize {
addr_space
.as_ref()
.map(|addrspace| {
let guard = addrspace.acquire_read(token.downgrade());
guard
.grants
.iter()
.map(|(_, info)| {
use crate::context::memory::Provider;
match info.provider {
Provider::Allocated { .. }
| Provider::AllocatedShared { .. }
| Provider::PhysBorrowed { .. }
| Provider::External { .. }
| Provider::FmapBorrowed { .. } => info.page_count() * PAGE_SIZE,
}
})
.sum()
})
.unwrap_or(0)
}
fn require_zero_offset(offset: u64) -> Result<()> {
if offset == 0 {
Ok(())
} else {
Err(Error::new(EINVAL))
}
}
fn validate_kfmap_flags(flags: MapFlags, consume: bool) -> Result<()> {
let flags = MapFlags::from_bits(flags.bits()).ok_or(Error::new(EINVAL))?;
let shared = flags.contains(MapFlags::MAP_SHARED);
let private = flags.contains(MapFlags::MAP_PRIVATE);
if shared == private {
return Err(Error::new(EINVAL));
}
if flags.contains(MapFlags::MAP_FIXED) && flags.contains(MapFlags::MAP_FIXED_NOREPLACE) {
return Err(Error::new(EINVAL));
}
if consume && !shared {
return Err(Error::new(EINVAL));
}
Ok(())
}
fn proc_context(pid: usize, token: &mut CleanLockToken) -> Option<Arc<ContextLock>> {
let mut contexts = context::contexts(token.downgrade());
let (contexts, mut token2) = contexts.token_split();
@@ -254,7 +306,14 @@ fn proc_stat_line(
ppid: usize,
priority: usize,
utime: u128,
stime: u128,
pgrp: usize,
session: usize,
nice: i32,
num_threads: usize,
starttime: u128,
vsize: usize,
rss: usize,
) -> String {
format!(
"{pid} ({comm}) {state} {ppid} {pgrp} {session} {tty_nr} {tpgid} {flags} {minflt} {cminflt} {majflt} {cmajflt} {utime} {stime} {cutime} {cstime} {priority} {nice} {num_threads} {itrealvalue} {starttime} {vsize} {rss} {rsslim}\n",
@@ -262,8 +321,8 @@ fn proc_stat_line(
comm = comm,
state = state,
ppid = ppid,
pgrp = ppid,
session = ppid,
pgrp = pgrp,
session = session,
tty_nr = 0,
tpgid = 0,
flags = 0,
@@ -272,32 +331,41 @@ fn proc_stat_line(
majflt = 0,
cmajflt = 0,
utime = utime,
stime = 0,
stime = stime,
cutime = 0,
cstime = 0,
priority = priority,
nice = 0,
num_threads = 1,
nice = nice,
num_threads = num_threads,
itrealvalue = 0,
starttime = 0,
starttime = starttime,
vsize = vsize,
rss = 0,
rss = rss / PAGE_SIZE,
rsslim = 0,
)
}
fn proc_status_text(context: &Context) -> String {
fn proc_status_text(context: &Context, num_threads: usize, vsize: usize, rss: usize) -> String {
let comm = context.name.as_str();
let state = proc_status_char(&context.status);
let ppid = context.owner_proc_id.map_or(0, NonZeroUsize::get);
format!(
"Name:\t{}\nState:\t{}\nPid:\t{}\nPPid:\t{}\nUid:\t{}\nGid:\t{}\nThreads:\t1\nCpus_allowed_list:\t{}\n",
"Name:\t{}\nState:\t{}\nPid:\t{}\nPPid:\t{}\nUid:\t{}\t{}\t{}\t{}\nGid:\t{}\t{}\t{}\t{}\nVmSize:\t{} kB\nVmRSS:\t{} kB\nThreads:\t{}\nCpus_allowed_list:\t{}\n",
comm,
state,
context.pid,
ppid,
context.euid,
context.euid,
context.euid,
context.euid,
context.egid,
context.egid,
context.egid,
context.egid,
vsize / 1024,
rss / 1024,
num_threads,
context.sched_affinity
)
}
@@ -361,6 +429,8 @@ impl ProcScheme {
),
"current-addrspace" => (ContextHandle::CurrentAddrSpace, false),
"current-filetable" => (ContextHandle::CurrentFiletable, false),
"ctx" => (ContextHandle::Ctx, false),
// Deprecated compatibility handles; prefer `ctx` for atomic save/restore.
"regs/float" => (ContextHandle::Regs(RegsKind::Float), false),
"regs/int" => (ContextHandle::Regs(RegsKind::Int), false),
"regs/env" => (ContextHandle::Regs(RegsKind::Env), false),
@@ -667,7 +737,7 @@ impl KernelScheme for ProcScheme {
let mut notify_files = Vec::new();
// TODO: Validate flags
validate_kfmap_flags(map.flags, consume)?;
let result_base = if consume {
dst_addr_space.r#move(
Some((addrspace, &mut *src_addr_space)),
@@ -802,7 +872,7 @@ impl KernelScheme for ProcScheme {
&self,
id: usize,
buf: UserSliceRo,
_offset: u64,
offset: u64,
_fcntl_flags: u32,
_stored_flags: u32,
token: &mut CleanLockToken,
@@ -817,7 +887,7 @@ impl KernelScheme for ProcScheme {
};
let Handle { context, kind } = handle;
kind.kwriteoff(id, context, buf, token)
kind.kwriteoff(id, context, buf, offset, token)
}
fn kfpath(&self, _id: usize, buf: UserSliceWo, _token: &mut CleanLockToken) -> Result<usize> {
@@ -1091,12 +1161,21 @@ impl ContextHandle {
id: usize,
context: Arc<ContextLock>,
buf: UserSliceRo,
offset: u64,
token: &mut CleanLockToken,
) -> Result<usize> {
match self {
Self::AddrSpace { addrspace } => {
let mut chunks = buf.usizes();
let mut words_read = 0;
let word_size = size_of::<usize>() as u64;
if offset % word_size != 0 {
return Err(Error::new(EINVAL));
}
let offset_words = usize::try_from(offset / word_size).map_err(|_| Error::new(EINVAL))?;
for _ in 0..offset_words {
chunks.next().ok_or(Error::new(EINVAL))??;
}
let mut next = || {
words_read += 1;
chunks.next().ok_or(Error::new(EINVAL))
@@ -1164,8 +1243,24 @@ impl ContextHandle {
}
Ok(words_read * size_of::<usize>())
}
ContextHandle::Ctx => {
require_zero_offset(offset)?;
let regs = unsafe { buf.read_exact::<syscall::FullContextRegs>()? };
try_stop_context(context, token, |context, _| {
context.set_fx_regs(regs.float);
match context.regs_mut() {
None => Err(Error::new(ENOTRECOVERABLE)),
Some(stack) => {
stack.load(&regs.int);
context.write_env_regs(regs.env)
}
}
})?;
Ok(size_of::<syscall::FullContextRegs>())
}
ContextHandle::Regs(kind) => match kind {
RegsKind::Float => {
require_zero_offset(offset)?;
let regs = unsafe { buf.read_exact::<FloatRegisters>()? };
try_stop_context(context, token, |context, _| {
@@ -1179,6 +1274,7 @@ impl ContextHandle {
})
}
RegsKind::Int => {
require_zero_offset(offset)?;
let regs = unsafe { buf.read_exact::<IntRegisters>()? };
try_stop_context(context, token, |context, _| match context.regs_mut() {
@@ -1198,12 +1294,14 @@ impl ContextHandle {
})
}
RegsKind::Env => {
require_zero_offset(offset)?;
let regs = unsafe { buf.read_exact::<EnvRegisters>()? };
write_env_regs(context, regs, token)?;
Ok(size_of::<EnvRegisters>())
}
},
ContextHandle::Sighandler => {
require_zero_offset(offset)?;
let data = unsafe { buf.read_exact::<SetSighandlerData>()? };
if data.user_handler >= crate::USER_END_OFFSET
@@ -1259,6 +1357,7 @@ impl ContextHandle {
Ok(size_of::<SetSighandlerData>())
}
ContextHandle::Start => match context.write(token.token()).status {
_ if offset != 0 => Err(Error::new(EINVAL)),
ref mut status @ Status::HardBlocked {
reason: HardBlockedReason::NotYetStarted,
} => {
@@ -1272,6 +1371,7 @@ impl ContextHandle {
}
ContextHandle::CurrentFiletable => {
require_zero_offset(offset)?;
let filetable_fd = buf.read_usize()?;
let (hopefully_this_scheme, number) = extract_scheme_number(filetable_fd, token)?;
verify_scheme(hopefully_this_scheme)?;
@@ -1317,6 +1417,7 @@ impl ContextHandle {
Ok(size_of::<usize>())
}
ContextHandle::CurrentAddrSpace => {
require_zero_offset(offset)?;
let mut iter = buf.usizes();
let addrspace_fd = iter.next().ok_or(Error::new(EINVAL))??;
let sp = iter.next().ok_or(Error::new(EINVAL))??;
@@ -1354,6 +1455,7 @@ impl ContextHandle {
Ok(written)
}
Self::MmapMinAddr(ref addrspace) => {
require_zero_offset(offset)?;
let val = buf.read_usize()?;
if val % PAGE_SIZE != 0 || val > crate::USER_END_OFFSET {
return Err(Error::new(EINVAL));
@@ -1363,6 +1465,7 @@ impl ContextHandle {
Ok(size_of::<usize>())
}
Self::SchedAffinity => {
require_zero_offset(offset)?;
let mask = unsafe { buf.read_exact::<crate::cpu_set::RawMask>()? };
context
@@ -1398,7 +1501,16 @@ impl ContextHandle {
guard.status = Status::HardBlocked {
reason: HardBlockedReason::Stopped,
};
// TODO: wait for context to be switched away from, and/or IPI?
drop(guard);
let mut spins = 10_000usize;
while context.read(token.token()).running {
if spins == 0 {
return Err(Error::new(EAGAIN));
}
spins -= 1;
context::switch(token);
}
Ok(size_of::<usize>())
}
ContextVerb::Unstop => {
@@ -1458,6 +1570,7 @@ impl ContextHandle {
}
}
ContextHandle::Attr => {
require_zero_offset(offset)?;
let info = unsafe { buf.read_exact::<ProcSchemeAttrs>()? };
let mut guard = context.write(token.token());
@@ -1476,9 +1589,16 @@ impl ContextHandle {
guard.euid = info.euid;
guard.egid = info.egid;
guard.prio = (info.prio as usize).min(39);
if guard.pgrp == 0 {
guard.pgrp = guard.pid;
}
if guard.session == 0 {
guard.session = guard.pid;
}
Ok(size_of::<ProcSchemeAttrs>())
}
Self::Groups => {
require_zero_offset(offset)?;
const NGROUPS_MAX: usize = 65536;
if buf.len() % size_of::<u32>() != 0 {
return Err(Error::new(EINVAL));
@@ -1512,6 +1632,7 @@ impl ContextHandle {
Ok(count * size_of::<u32>())
}
ContextHandle::OpenViaDup => {
require_zero_offset(offset)?;
let mut args = buf.usizes();
let user_data = args.next().ok_or(Error::new(EINVAL))??;
@@ -1566,6 +1687,35 @@ impl ContextHandle {
token: &mut CleanLockToken,
) -> Result<usize> {
match self {
ContextHandle::Ctx => {
require_zero_offset(offset)?;
let mut regs = syscall::FullContextRegs::default();
try_stop_context(context, token, |context, _| {
match context.regs() {
None => {
assert!(!context.running, "try_stop_context is broken, clearly");
println!(
"{}:{}: Couldn't read registers from stopped process",
file!(),
line!()
);
Err(Error::new(ENOTRECOVERABLE))
}
Some(stack) => {
stack.save(&mut regs.int);
regs.float = context.get_fx_regs();
regs.env = context.read_env_regs()?;
Ok(())
}
}
})?;
buf.copy_common_bytes_from_slice(unsafe {
slice::from_raw_parts(
&regs as *const syscall::FullContextRegs as *const u8,
size_of::<syscall::FullContextRegs>(),
)
})
}
ContextHandle::Regs(kind) => {
union Output {
float: FloatRegisters,
@@ -1658,7 +1808,7 @@ impl ContextHandle {
ContextHandle::Filetable { data, .. } => read_from(buf, data, offset),
ContextHandle::ProcStat { pid } => {
let context = proc_context(*pid, token).ok_or(Error::new(ESRCH))?;
let (pid, comm, state, ppid, priority, utime, addr_space) = {
let (pid, comm, state, ppid, priority, utime, stime, pgrp, session, starttime, owner_proc_id, addr_space) = {
let context = context.read(token.token());
(
context.pid,
@@ -1666,14 +1816,45 @@ impl ContextHandle {
proc_status_char(&context.status),
context.owner_proc_id.map_or(0, NonZeroUsize::get),
context.prio,
context.cpu_time / crate::time::NANOS_PER_SEC,
context.utime / crate::time::NANOS_PER_SEC,
context.stime / crate::time::NANOS_PER_SEC,
context.pgrp,
context.session,
context.start_time / (crate::time::NANOS_PER_SEC / 100),
context.owner_proc_id,
context.addr_space.clone(),
)
};
let num_threads = {
let mut contexts = context::contexts(token.downgrade());
let (contexts, mut token2) = contexts.token_split();
contexts
.iter()
.filter(|ctx| ctx.read(token2.token()).owner_proc_id == owner_proc_id)
.count()
};
let vsize = proc_vsize(&addr_space, token);
let rss = proc_rss(&addr_space, token);
let nice = (priority as i32 / 2) - 20;
read_from(
buf,
proc_stat_line(pid, &comm, state, ppid, priority, utime, vsize).as_bytes(),
proc_stat_line(
pid,
&comm,
state,
ppid,
priority,
utime,
stime,
pgrp,
session,
nice,
num_threads,
starttime,
vsize,
rss,
)
.as_bytes(),
offset,
)
}
@@ -1691,8 +1872,26 @@ impl ContextHandle {
}
ContextHandle::ProcStatus { pid } => {
let context = proc_context(*pid, token).ok_or(Error::new(ESRCH))?;
let owner_proc_id = context.read(token.token()).owner_proc_id;
let num_threads = {
let mut contexts = context::contexts(token.downgrade());
let (contexts, mut token2) = contexts.token_split();
contexts
.iter()
.filter(|ctx| ctx.read(token2.token()).owner_proc_id == owner_proc_id)
.count()
};
let (vsize, rss) = {
let addr_space = {
let context = context.read(token.token());
context.addr_space.clone()
};
let vsize = proc_vsize(&addr_space, token);
let rss = proc_rss(&addr_space, token);
(vsize, rss)
};
let context = context.read(token.token());
read_from(buf, proc_status_text(&context).as_bytes(), offset)
read_from(buf, proc_status_text(&context, num_threads, vsize, rss).as_bytes(), offset)
}
ContextHandle::ProcRoot | ContextHandle::ProcDir { .. } => Err(Error::new(EBADF)),
ContextHandle::MmapMinAddr(addrspace) => {