Add support for the ps command on FreeBSD, NetBSD, and OpenBSD (#12892)

# Description

I feel like it's a little sad that BSDs get to enjoy almost everything
other than the `ps` command, and there are some tests that rely on this
command, so I figured it would be fun to patch that and make it work.

The different BSDs have diverged from each other somewhat, but generally
have a similar enough API for reading process information via
`sysctl()`, with some slightly different args.

This supports FreeBSD with the `freebsd` module, and NetBSD and OpenBSD
with the `netbsd` module. OpenBSD is a fork of NetBSD and the interface
has some minor differences but many things are the same.

I had wanted to try to support DragonFlyBSD too, but their Rust version
in the latest release is only 1.72.0, which is too old for me to want to
try to compile rustc up to 1.77.2... but I will revisit this whenever
they do update it. Dragonfly is a fork of FreeBSD, so it's likely to be
more or less the same - I just don't want to enable it without testing
it.

Fixes #6862 (partially, we probably won't be adding `zfs list`)

# User-Facing Changes
`ps` added for FreeBSD, NetBSD, and OpenBSD.

# Tests + Formatting
The CI doesn't run tests for BSDs, so I'm not entirely sure if
everything was already passing before. (Frankly, it's unlikely.) But
nothing appears to be broken.

# After Submitting
- [ ] release notes?
- [ ] DragonflyBSD, whenever they do update Rust to something close
enough for me to try it
This commit is contained in:
Devyn Cairns 2024-05-22 08:13:45 -07:00 committed by GitHub
parent d7e75c0b70
commit 758c5d447a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 662 additions and 15 deletions

1
Cargo.lock generated
View File

@ -3264,6 +3264,7 @@ name = "nu-system"
version = "0.93.1"
dependencies = [
"chrono",
"itertools 0.12.1",
"libc",
"libproc",
"log",

View File

@ -164,6 +164,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
#[cfg(any(
target_os = "android",
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "windows"
))]

View File

@ -5,6 +5,8 @@ mod nu_check;
target_os = "android",
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "windows"
))]
@ -23,6 +25,8 @@ pub use nu_check::NuCheck;
target_os = "android",
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "windows"
))]

View File

@ -2,20 +2,13 @@
use itertools::Itertools;
use nu_engine::command_prelude::*;
#[cfg(all(
unix,
not(target_os = "freebsd"),
not(target_os = "macos"),
not(target_os = "windows"),
not(target_os = "android"),
))]
#[cfg(target_os = "linux")]
use procfs::WithCurrentSystemInfo;
use std::time::Duration;
#[derive(Clone)]
pub struct Ps;
#[cfg(not(target_os = "freebsd"))]
impl Command for Ps {
fn name(&self) -> &str {
"ps"
@ -82,7 +75,6 @@ impl Command for Ps {
}
}
#[cfg(not(target_os = "freebsd"))]
fn run_ps(
engine_state: &EngineState,
stack: &mut Stack,
@ -111,12 +103,7 @@ fn run_ps(
if long {
record.push("command", Value::string(proc.command(), span));
#[cfg(all(
unix,
not(target_os = "macos"),
not(target_os = "windows"),
not(target_os = "android"),
))]
#[cfg(target_os = "linux")]
{
let proc_stat = proc
.curr_proc

View File

@ -16,6 +16,7 @@ bench = false
libc = { workspace = true }
log = { workspace = true }
sysinfo = { workspace = true }
itertools = { workspace = true }
[target.'cfg(target_family = "unix")'.dependencies]
nix = { workspace = true, default-features = false, features = ["fs", "term", "process", "signal"] }

View File

@ -0,0 +1,305 @@
use itertools::{EitherOrBoth, Itertools};
use libc::{
kinfo_proc, sysctl, CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS, TDF_IDLETD,
};
use std::{
ffi::CStr,
io,
mem::{self, MaybeUninit},
ptr,
time::{Duration, Instant},
};
#[derive(Debug)]
pub struct ProcessInfo {
pub pid: i32,
pub ppid: i32,
pub name: String,
pub argv: Vec<u8>,
pub stat: i8,
pub percent_cpu: f64,
pub mem_resident: u64, // in bytes
pub mem_virtual: u64, // in bytes
}
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
compare_procs(interval).unwrap_or_else(|err| {
log::warn!("Failed to get processes: {}", err);
vec![]
})
}
fn compare_procs(interval: Duration) -> io::Result<Vec<ProcessInfo>> {
let pagesize = get_pagesize()? as u64;
// Compare two full snapshots of all of the processes over the interval
let now = Instant::now();
let procs_a = get_procs()?;
std::thread::sleep(interval);
let procs_b = get_procs()?;
let true_interval = Instant::now().saturating_duration_since(now);
let true_interval_sec = true_interval.as_secs_f64();
// Group all of the threads in each process together
let a_grouped = procs_a.into_iter().group_by(|proc| proc.ki_pid);
let b_grouped = procs_b.into_iter().group_by(|proc| proc.ki_pid);
// Join the processes between the two snapshots
Ok(a_grouped
.into_iter()
.merge_join_by(b_grouped.into_iter(), |(pid_a, _), (pid_b, _)| {
pid_a.cmp(pid_b)
})
.map(|threads| {
// Join the threads between the two snapshots for the process
let mut threads = {
let (left, right) = threads.left_and_right();
left.into_iter()
.flat_map(|(_, threads)| threads)
.merge_join_by(
right.into_iter().flat_map(|(_, threads)| threads),
|thread_a, thread_b| thread_a.ki_tid.cmp(&thread_b.ki_tid),
)
.peekable()
};
// Pick the later process entry of the first thread to use for basic process information
let proc = match threads.peek().ok_or(io::ErrorKind::NotFound)? {
EitherOrBoth::Both(_, b) => b,
EitherOrBoth::Left(a) => a,
EitherOrBoth::Right(b) => b,
}
.clone();
// Skip over the idle process. It always appears with high CPU usage when the
// system is idle
if proc.ki_tdflags as u64 & TDF_IDLETD as u64 != 0 {
return Err(io::ErrorKind::NotFound.into());
}
// Aggregate all of the threads that exist in both snapshots and sum their runtime.
let (runtime_a, runtime_b) =
threads
.flat_map(|t| t.both())
.fold((0., 0.), |(runtime_a, runtime_b), (a, b)| {
let runtime_in_seconds =
|proc: &kinfo_proc| proc.ki_runtime as f64 /* µsec */ / 1_000_000.0;
(
runtime_a + runtime_in_seconds(&a),
runtime_b + runtime_in_seconds(&b),
)
});
// The percentage CPU is the ratio of how much runtime occurred for the process out of
// the true measured interval that occurred.
let percent_cpu = 100. * (runtime_b - runtime_a).max(0.) / true_interval_sec;
let info = ProcessInfo {
pid: proc.ki_pid,
ppid: proc.ki_ppid,
name: read_cstr(&proc.ki_comm).to_string_lossy().into_owned(),
argv: get_proc_args(proc.ki_pid)?,
stat: proc.ki_stat,
percent_cpu,
mem_resident: proc.ki_rssize.max(0) as u64 * pagesize,
mem_virtual: proc.ki_size.max(0) as u64,
};
Ok(info)
})
// Remove errors from the list - probably just processes that are gone now
.flat_map(|result: io::Result<_>| result.ok())
.collect())
}
fn check(err: libc::c_int) -> std::io::Result<()> {
if err < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
/// This is a bounds-checked way to read a `CStr` from a slice of `c_char`
fn read_cstr(slice: &[libc::c_char]) -> &CStr {
unsafe {
// SAFETY: ensure that c_char and u8 are the same size
mem::transmute::<libc::c_char, u8>(0);
let slice: &[u8] = mem::transmute(slice);
CStr::from_bytes_until_nul(slice).unwrap_or_default()
}
}
fn get_procs() -> io::Result<Vec<libc::kinfo_proc>> {
// To understand what's going on here, see the sysctl(3) manpage for FreeBSD.
unsafe {
const STRUCT_SIZE: usize = mem::size_of::<libc::kinfo_proc>();
let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ALL];
// First, try to figure out how large a buffer we need to allocate
// (calling with NULL just tells us that)
let mut data_len = 0;
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
ptr::null(),
0,
))?;
// data_len will be set in bytes, so divide by the size of the structure
let expected_len = data_len.div_ceil(STRUCT_SIZE);
// Now allocate the Vec and set data_len to the real number of bytes allocated
let mut vec: Vec<libc::kinfo_proc> = Vec::with_capacity(expected_len);
data_len = vec.capacity() * STRUCT_SIZE;
// Call sysctl() again to put the result in the vec
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
ptr::null(),
0,
))?;
// If that was ok, we can set the actual length of the vec to whatever
// data_len was changed to, since that should now all be properly initialized data.
let true_len = data_len.div_ceil(STRUCT_SIZE);
vec.set_len(true_len);
// Sort the procs by pid and then tid before using them
vec.sort_by_key(|p| (p.ki_pid, p.ki_tid));
Ok(vec)
}
}
fn get_proc_args(pid: i32) -> io::Result<Vec<u8>> {
unsafe {
let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ARGS, pid];
// First, try to figure out how large a buffer we need to allocate
// (calling with NULL just tells us that)
let mut data_len = 0;
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
ptr::null(),
0,
))?;
// Now allocate the Vec and set data_len to the real number of bytes allocated
let mut vec: Vec<u8> = Vec::with_capacity(data_len);
data_len = vec.capacity();
// Call sysctl() again to put the result in the vec
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
ptr::null(),
0,
))?;
// If that was ok, we can set the actual length of the vec to whatever
// data_len was changed to, since that should now all be properly initialized data.
vec.set_len(data_len);
Ok(vec)
}
}
// For getting simple values from the sysctl interface
unsafe fn get_ctl<T>(ctl_name: &[i32]) -> io::Result<T> {
let mut value: MaybeUninit<T> = MaybeUninit::uninit();
let mut value_len = mem::size_of_val(&value);
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
value.as_mut_ptr() as *mut libc::c_void,
&mut value_len,
ptr::null(),
0,
))?;
Ok(value.assume_init())
}
fn get_pagesize() -> io::Result<libc::c_int> {
// not in libc for some reason
const HW_PAGESIZE: i32 = 7;
unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) }
}
impl ProcessInfo {
/// PID of process
pub fn pid(&self) -> i32 {
self.pid
}
/// Parent PID of process
pub fn ppid(&self) -> i32 {
self.ppid
}
/// Name of command
pub fn name(&self) -> String {
let argv_name = self
.argv
.split(|b| *b == 0)
.next()
.map(String::from_utf8_lossy)
.unwrap_or_default()
.into_owned();
if !argv_name.is_empty() {
argv_name
} else {
// Just use the command name alone.
self.name.clone()
}
}
/// Full name of command, with arguments
pub fn command(&self) -> String {
if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) {
// The command string is NUL separated
// Take the string up to the last NUL, then replace the NULs with spaces
String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ")
} else {
// The argv is empty, so use the name instead
self.name()
}
}
/// Get the status of the process
pub fn status(&self) -> String {
match self.stat {
libc::SIDL | libc::SRUN => "Running",
libc::SSLEEP => "Sleeping",
libc::SSTOP => "Stopped",
libc::SWAIT => "Waiting",
libc::SLOCK => "Locked",
libc::SZOMB => "Zombie",
_ => "Unknown",
}
.into()
}
/// CPU usage as a percent of total
pub fn cpu_usage(&self) -> f64 {
self.percent_cpu
}
/// Memory size in number of bytes
pub fn mem_size(&self) -> u64 {
self.mem_resident
}
/// Virtual memory size in bytes
pub fn virtual_size(&self) -> u64 {
self.mem_virtual
}
}

View File

@ -1,8 +1,13 @@
mod foreground;
#[cfg(target_os = "freebsd")]
mod freebsd;
#[cfg(any(target_os = "android", target_os = "linux"))]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(any(target_os = "netbsd", target_os = "openbsd"))]
mod netbsd;
pub mod os_info;
#[cfg(target_os = "windows")]
mod windows;
@ -10,9 +15,14 @@ mod windows;
#[cfg(unix)]
pub use self::foreground::stdin_fd;
pub use self::foreground::{ForegroundChild, ForegroundGuard};
#[cfg(target_os = "freebsd")]
pub use self::freebsd::*;
#[cfg(any(target_os = "android", target_os = "linux"))]
pub use self::linux::*;
#[cfg(target_os = "macos")]
pub use self::macos::*;
#[cfg(any(target_os = "netbsd", target_os = "openbsd"))]
pub use self::netbsd::*;
#[cfg(target_os = "windows")]
pub use self::windows::*;

View File

@ -0,0 +1,336 @@
//! This is used for both NetBSD and OpenBSD, because they are fairly similar.
use itertools::{EitherOrBoth, Itertools};
use libc::{sysctl, CTL_HW, CTL_KERN, KERN_PROC_ALL, KERN_PROC_ARGS, KERN_PROC_ARGV};
use std::{
io,
mem::{self, MaybeUninit},
ptr,
time::{Duration, Instant},
};
#[cfg(target_os = "netbsd")]
type KInfoProc = libc::kinfo_proc2;
#[cfg(target_os = "openbsd")]
type KInfoProc = libc::kinfo_proc;
#[derive(Debug)]
pub struct ProcessInfo {
pub pid: i32,
pub ppid: i32,
pub argv: Vec<u8>,
pub stat: i8,
pub percent_cpu: f64,
pub mem_resident: u64, // in bytes
pub mem_virtual: u64, // in bytes
}
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
compare_procs(interval).unwrap_or_else(|err| {
log::warn!("Failed to get processes: {}", err);
vec![]
})
}
fn compare_procs(interval: Duration) -> io::Result<Vec<ProcessInfo>> {
let pagesize = get_pagesize()? as u64;
// Compare two full snapshots of all of the processes over the interval
let now = Instant::now();
let procs_a = get_procs()?;
std::thread::sleep(interval);
let procs_b = get_procs()?;
let true_interval = Instant::now().saturating_duration_since(now);
let true_interval_sec = true_interval.as_secs_f64();
// Join the processes between the two snapshots
Ok(procs_a
.into_iter()
.merge_join_by(procs_b.into_iter(), |a, b| a.p_pid.cmp(&b.p_pid))
.map(|proc| {
// Take both snapshotted processes if we can, but if not then just keep the one that
// exists and set prev_proc to None
let (prev_proc, proc) = match proc {
EitherOrBoth::Both(a, b) => (Some(a), b),
EitherOrBoth::Left(a) => (None, a),
EitherOrBoth::Right(b) => (None, b),
};
// The percentage CPU is the ratio of how much runtime occurred for the process out of
// the true measured interval that occurred.
let percent_cpu = if let Some(prev_proc) = prev_proc {
let prev_rtime =
prev_proc.p_rtime_sec as f64 + prev_proc.p_rtime_usec as f64 / 1_000_000.0;
let rtime = proc.p_rtime_sec as f64 + proc.p_rtime_usec as f64 / 1_000_000.0;
100. * (rtime - prev_rtime).max(0.) / true_interval_sec
} else {
0.0
};
Ok(ProcessInfo {
pid: proc.p_pid,
ppid: proc.p_ppid,
argv: get_proc_args(proc.p_pid, KERN_PROC_ARGV)?,
stat: proc.p_stat,
percent_cpu,
mem_resident: proc.p_vm_rssize.max(0) as u64 * pagesize,
#[cfg(target_os = "netbsd")]
mem_virtual: proc.p_vm_msize.max(0) as u64 * pagesize,
#[cfg(target_os = "openbsd")]
mem_virtual: proc.p_vm_map_size.max(0) as u64 * pagesize,
})
})
// Remove errors from the list - probably just processes that are gone now
.flat_map(|result: io::Result<_>| result.ok())
.collect())
}
fn check(err: libc::c_int) -> std::io::Result<()> {
if err < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
/// Call `sysctl()` in read mode (i.e. the last two arguments are NULL and zero)
unsafe fn sysctl_get(
name: *const i32,
name_len: u32,
data: *mut libc::c_void,
data_len: *mut usize,
) -> i32 {
sysctl(
name,
name_len,
data,
data_len,
// NetBSD and OpenBSD differ in mutability for this pointer, but it's null anyway
#[cfg(target_os = "netbsd")]
ptr::null(),
#[cfg(target_os = "openbsd")]
ptr::null_mut(),
0,
)
}
fn get_procs() -> io::Result<Vec<KInfoProc>> {
// To understand what's going on here, see the sysctl(3) and sysctl(7) manpages for NetBSD.
unsafe {
const STRUCT_SIZE: usize = mem::size_of::<KInfoProc>();
#[cfg(target_os = "netbsd")]
const TGT_KERN_PROC: i32 = libc::KERN_PROC2;
#[cfg(target_os = "openbsd")]
const TGT_KERN_PROC: i32 = libc::KERN_PROC;
let mut ctl_name = [
CTL_KERN,
TGT_KERN_PROC,
KERN_PROC_ALL,
0,
STRUCT_SIZE as i32,
0,
];
// First, try to figure out how large a buffer we need to allocate
// (calling with NULL just tells us that)
let mut data_len = 0;
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
))?;
// data_len will be set in bytes, so divide by the size of the structure
let expected_len = data_len.div_ceil(STRUCT_SIZE);
// Now allocate the Vec and set data_len to the real number of bytes allocated
let mut vec: Vec<KInfoProc> = Vec::with_capacity(expected_len);
data_len = vec.capacity() * STRUCT_SIZE;
// We are also supposed to set ctl_name[5] to the number of structures we want
ctl_name[5] = expected_len.try_into().expect("expected_len too big");
// Call sysctl() again to put the result in the vec
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
))?;
// If that was ok, we can set the actual length of the vec to whatever
// data_len was changed to, since that should now all be properly initialized data.
let true_len = data_len.div_ceil(STRUCT_SIZE);
vec.set_len(true_len);
// Sort the procs by pid before using them
vec.sort_by_key(|p| p.p_pid);
Ok(vec)
}
}
fn get_proc_args(pid: i32, what: i32) -> io::Result<Vec<u8>> {
unsafe {
let ctl_name = [CTL_KERN, KERN_PROC_ARGS, pid, what];
// First, try to figure out how large a buffer we need to allocate
// (calling with NULL just tells us that)
let mut data_len = 0;
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
))?;
// Now allocate the Vec and set data_len to the real number of bytes allocated
let mut vec: Vec<u8> = Vec::with_capacity(data_len);
data_len = vec.capacity();
// Call sysctl() again to put the result in the vec
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
))?;
// If that was ok, we can set the actual length of the vec to whatever
// data_len was changed to, since that should now all be properly initialized data.
vec.set_len(data_len);
// On OpenBSD we have to do an extra step, because it fills the buffer with pointers to the
// strings first, even though the strings are within the buffer as well.
#[cfg(target_os = "openbsd")]
let vec = {
use std::ffi::CStr;
// Set up some bounds checking. We assume there will be some pointers at the base until
// we reach NULL, but we want to make sure we only ever read data within the range of
// min_ptr..max_ptr.
let ptrs = vec.as_ptr() as *const *const u8;
let min_ptr = vec.as_ptr() as *const u8;
let max_ptr = vec.as_ptr().add(vec.len()) as *const u8;
let max_index: isize = (vec.len() / mem::size_of::<*const u8>())
.try_into()
.expect("too big for isize");
let mut new_vec = Vec::with_capacity(vec.len());
for index in 0..max_index {
let ptr = ptrs.offset(index);
if *ptr == ptr::null() {
break;
} else {
// Make sure it's within the bounds of the buffer
assert!(
*ptr >= min_ptr && *ptr < max_ptr,
"pointer out of bounds of the buffer returned by sysctl()"
);
// Also bounds-check the C strings, to make sure we don't overrun the buffer
new_vec.extend(
CStr::from_bytes_until_nul(std::slice::from_raw_parts(
*ptr,
max_ptr.offset_from(*ptr) as usize,
))
.expect("invalid C string")
.to_bytes_with_nul(),
);
}
}
new_vec
};
Ok(vec)
}
}
// For getting simple values from the sysctl interface
unsafe fn get_ctl<T>(ctl_name: &[i32]) -> io::Result<T> {
let mut value: MaybeUninit<T> = MaybeUninit::uninit();
let mut value_len = mem::size_of_val(&value);
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
value.as_mut_ptr() as *mut libc::c_void,
&mut value_len,
))?;
Ok(value.assume_init())
}
fn get_pagesize() -> io::Result<libc::c_int> {
// not in libc for some reason
const HW_PAGESIZE: i32 = 7;
unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) }
}
impl ProcessInfo {
/// PID of process
pub fn pid(&self) -> i32 {
self.pid
}
/// Parent PID of process
pub fn ppid(&self) -> i32 {
self.ppid
}
/// Name of command
pub fn name(&self) -> String {
self.argv
.split(|b| *b == 0)
.next()
.map(String::from_utf8_lossy)
.unwrap_or_default()
.into_owned()
}
/// Full name of command, with arguments
pub fn command(&self) -> String {
if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) {
// The command string is NUL separated
// Take the string up to the last NUL, then replace the NULs with spaces
String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ")
} else {
"".into()
}
}
/// Get the status of the process
pub fn status(&self) -> String {
// see sys/proc.h (OpenBSD), sys/lwp.h (NetBSD)
// the names given here are the NetBSD ones, starting with LS*, but the OpenBSD ones are
// the same, just starting with S* instead
match self.stat {
1 /* LSIDL */ => "",
2 /* LSRUN */ => "Waiting",
3 /* LSSLEEP */ => "Sleeping",
4 /* LSSTOP */ => "Stopped",
5 /* LSZOMB */ => "Zombie",
#[cfg(target_os = "openbsd")] // removed in NetBSD
6 /* LSDEAD */ => "Dead",
7 /* LSONPROC */ => "Running",
#[cfg(target_os = "netbsd")] // doesn't exist in OpenBSD
8 /* LSSUSPENDED */ => "Suspended",
_ => "Unknown",
}
.into()
}
/// CPU usage as a percent of total
pub fn cpu_usage(&self) -> f64 {
self.percent_cpu
}
/// Memory size in number of bytes
pub fn mem_size(&self) -> u64 {
self.mem_resident
}
/// Virtual memory size in bytes
pub fn virtual_size(&self) -> u64 {
self.mem_virtual
}
}