Skip to content

Commit d452d5a

Browse files
author
Lars T Hansen
committed
For #94 - virtualize procfs
1 parent 4815fef commit d452d5a

File tree

5 files changed

+318
-70
lines changed

5 files changed

+318
-70
lines changed

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod jobs;
99
mod nvidia;
1010
mod process;
1111
mod procfs;
12+
mod procfsapi;
1213
mod ps;
1314
mod slurm;
1415
mod util;

src/process.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::command::{self, CmdError};
44
use crate::TIMEOUT_SECONDS;
55
use crate::util;
66

7-
#[derive(PartialEq)]
7+
#[derive(PartialEq, Debug)]
88
pub struct Process {
99
pub pid: usize,
1010
pub uid: usize,

src/procfs.rs

Lines changed: 39 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
/// Collect CPU process information without GPU information, from files in /proc.
2-
3-
extern crate libc;
4-
extern crate page_size;
5-
extern crate users;
2+
///
3+
/// The underlying system is virtualized through ProcfsAPI.
64
75
use crate::process;
6+
use crate::procfsapi::ProcfsAPI;
87

98
use std::collections::HashMap;
10-
use std::fs;
11-
use std::path;
12-
use std::os::linux::fs::MetadataExt;
13-
use std::time::{SystemTime, UNIX_EPOCH};
14-
use users::{uid_t, get_user_by_uid};
159

1610
/// Obtain process information via /proc and return a vector of structures with all the information
1711
/// we need. In the returned vector, pids uniquely tag the records.
@@ -22,50 +16,44 @@ use users::{uid_t, get_user_by_uid};
2216
/// This function uniformly uses /proc, even though in some cases there are system calls that
2317
/// provide the same information.
2418
25-
pub fn get_process_information() -> Result<Vec<process::Process>, String> {
19+
pub fn get_process_information(fs: &dyn ProcfsAPI) -> Result<Vec<process::Process>, String> {
2620

2721
// The boot time is the `btime` field of /proc/stat. It is measured in seconds since epoch. We
2822
// need this to compute the process's real time, which we need to compute ps-compatible cpu
2923
// utilization.
3024

3125
let mut boot_time = 0;
32-
if let Ok(s) = fs::read_to_string(path::Path::new("/proc/stat")) {
33-
for l in s.split('\n') {
34-
if l.starts_with("btime ") {
35-
let fields = l.split_ascii_whitespace().collect::<Vec<&str>>();
36-
boot_time = parse_usize_field(&fields, 1, &l, "stat", 0, "btime")? as u64;
37-
break;
38-
}
39-
}
40-
if boot_time == 0 {
41-
return Err(format!("Could not find btime in /proc/stat: {s}"));
26+
let stat_s = fs.read_to_string("stat")?;
27+
for l in stat_s.split('\n') {
28+
if l.starts_with("btime ") {
29+
let fields = l.split_ascii_whitespace().collect::<Vec<&str>>();
30+
boot_time = parse_usize_field(&fields, 1, &l, "stat", 0, "btime")? as u64;
31+
break;
4232
}
43-
} else {
44-
return Err(format!("Could not open or read /proc/stat"));
45-
};
33+
}
34+
if boot_time == 0 {
35+
return Err(format!("Could not find btime in /proc/stat: {stat_s}"));
36+
}
4637

4738
// The total RAM installed is in the `MemTotal` field of /proc/meminfo. We need this to compute
4839
// ps-compatible relative memory use.
4940

5041
let mut memtotal_kib = 0;
51-
if let Ok(s) = fs::read_to_string(path::Path::new("/proc/meminfo")) {
52-
for l in s.split('\n') {
53-
if l.starts_with("MemTotal: ") {
54-
// We expect "MemTotal:\s+(\d+)\s+kB", roughly
55-
let fields = l.split_ascii_whitespace().collect::<Vec<&str>>();
56-
if fields.len() != 3 || fields[2] != "kB" {
57-
return Err(format!("Unexpected MemTotal in /proc/meminfo: {l}"));
58-
}
59-
memtotal_kib = parse_usize_field(&fields, 1, &l, "meminfo", 0, "MemTotal")?;
60-
break;
42+
let meminfo_s = fs.read_to_string("meminfo")?;
43+
for l in meminfo_s.split('\n') {
44+
if l.starts_with("MemTotal: ") {
45+
// We expect "MemTotal:\s+(\d+)\s+kB", roughly
46+
let fields = l.split_ascii_whitespace().collect::<Vec<&str>>();
47+
if fields.len() != 3 || fields[2] != "kB" {
48+
return Err(format!("Unexpected MemTotal in /proc/meminfo: {l}"));
6149
}
50+
memtotal_kib = parse_usize_field(&fields, 1, &l, "meminfo", 0, "MemTotal")?;
51+
break;
6252
}
63-
if memtotal_kib == 0 {
64-
return Err(format!("Could not find MemTotal in /proc/meminfo: {s}"));
65-
}
66-
} else {
67-
return Err(format!("Could not open or read /proc/meminfo"));
68-
};
53+
}
54+
if memtotal_kib == 0 {
55+
return Err(format!("Could not find MemTotal in /proc/meminfo: {meminfo_s}"));
56+
}
6957

7058
// Enumerate all pids, and collect the uids while we're here.
7159
//
@@ -76,30 +64,14 @@ pub fn get_process_information() -> Result<Vec<process::Process>, String> {
7664
// Note that a pid may disappear between the time we see it here and the time we get around to
7765
// reading it, later, and that new pids may appear meanwhile. We should ignore both issues.
7866

79-
let mut pids = vec![];
80-
if let Ok(dir) = fs::read_dir("/proc") {
81-
for dirent in dir {
82-
if let Ok(dirent) = dirent {
83-
if let Ok(meta) = dirent.metadata() {
84-
let uid = meta.st_uid();
85-
if let Some(name) = dirent.path().file_name() {
86-
if let Ok(pid) = name.to_string_lossy().parse::<usize>() {
87-
pids.push((pid, uid));
88-
}
89-
}
90-
}
91-
}
92-
}
93-
} else {
94-
return Err(format!("Could not open /proc"));
95-
};
67+
let pids = fs.read_proc_pids()?;
9668

9769
// Collect remaining system data from /proc/{pid}/stat for the enumerated pids.
9870

99-
let kib_per_page = page_size::get() / 1024;
71+
let kib_per_page = fs.page_size();
10072
let mut result = vec![];
10173
let mut user_table = UserTable::new();
102-
let clock_ticks_per_sec: usize = unsafe { libc::sysconf(libc::_SC_CLK_TCK) as usize };
74+
let clock_ticks_per_sec = fs.clock_ticks_per_sec();
10375
for (pid, uid) in pids {
10476

10577
// Basic system variables.
@@ -111,7 +83,7 @@ pub fn get_process_information() -> Result<Vec<process::Process>, String> {
11183
let comm;
11284
let utime;
11385
let stime;
114-
if let Ok(line) = fs::read_to_string(path::Path::new(&format!("/proc/{pid}/stat"))) {
86+
if let Ok(line) = fs.read_to_string(&format!("{pid}/stat")) {
11587
// The comm field is a little tricky, it must be extracted first as the contents between
11688
// the first '(' and the last ')' in the line.
11789
let commstart = line.find('(');
@@ -132,12 +104,13 @@ pub fn get_process_information() -> Result<Vec<process::Process>, String> {
132104
let cstime = parse_usize_field(&fields, 14, &line, "stat", pid, "cstime")? / clock_ticks_per_sec;
133105
bsdtime = utime + stime + cutime + cstime;
134106
let start_time = (parse_usize_field(&fields, 19, &line, "stat", pid, "starttime")? / clock_ticks_per_sec) as u64;
135-
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
107+
let now = fs.now();
136108
realtime = now - (boot_time + start_time);
137109
} else {
138110
// This is *usually* benign - the process may have gone away since we enumerated the
139111
// /proc directory. It is *possibly* indicative of a permission problem, but that
140112
// problem would be so pervasive that diagnosing it here is not right.
113+
println!("E");
141114
continue;
142115
}
143116

@@ -152,12 +125,13 @@ pub fn get_process_information() -> Result<Vec<process::Process>, String> {
152125

153126
let size_kib;
154127
let rss_kib;
155-
if let Ok(s) = fs::read_to_string(path::Path::new(&format!("/proc/{pid}/statm"))) {
128+
if let Ok(s) = fs.read_to_string(&format!("{pid}/statm")) {
156129
let fields = s.split_ascii_whitespace().collect::<Vec<&str>>();
157130
rss_kib = parse_usize_field(&fields, 1, &s, "statm", pid, "resident set size")? * kib_per_page;
158131
size_kib = parse_usize_field(&fields, 5, &s, "statm", pid, "data size")? * kib_per_page;
159132
} else {
160133
// This is *usually* benign - see above.
134+
println!("F");
161135
continue;
162136
}
163137

@@ -172,7 +146,7 @@ pub fn get_process_information() -> Result<Vec<process::Process>, String> {
172146

173147
let pcpu = (((utime + stime) as f64 * 1000.0 / realtime as f64)).ceil() / 10.0;
174148
let pmem = f64::min(((rss_kib as f64) * 1000.0 / (memtotal_kib as f64)).ceil() / 10.0, 99.9);
175-
let user = user_table.lookup(uid);
149+
let user = user_table.lookup(fs, uid);
176150

177151
result.push(process::Process {
178152
pid,
@@ -212,7 +186,7 @@ fn parse_usize_field(fields: &[&str], ix: usize, line: &str, file: &str, pid: us
212186
// The UserTable optimizes uid -> name lookup.
213187

214188
struct UserTable {
215-
ht: HashMap<uid_t, String>,
189+
ht: HashMap<u32, String>,
216190
}
217191

218192
impl UserTable {
@@ -222,11 +196,10 @@ impl UserTable {
222196
}
223197
}
224198

225-
fn lookup(&mut self, uid: uid_t) -> String {
199+
fn lookup(&mut self, fs: &dyn ProcfsAPI, uid: u32) -> String {
226200
if let Some(name) = self.ht.get(&uid) {
227201
name.clone()
228-
} else if let Some(u) = get_user_by_uid(uid) {
229-
let name = u.name().to_string_lossy().to_string();
202+
} else if let Some(name) = fs.user_by_uid(uid) {
230203
self.ht.insert(uid, name.clone());
231204
name
232205
} else {

src/procfsapi.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
extern crate libc;
2+
extern crate page_size;
3+
extern crate users;
4+
5+
use users::get_user_by_uid;
6+
7+
use std::collections::HashMap;
8+
use std::fs;
9+
use std::path;
10+
use std::os::linux::fs::MetadataExt;
11+
use std::time::{SystemTime, UNIX_EPOCH};
12+
13+
pub trait ProcfsAPI {
14+
// Open /proc/<path> (which can have multiple path elements, eg, {PID}/filename), read it, and
15+
// return its entire contents as a string. Return a sensible error message if the file can't
16+
// be opened or read.
17+
fn read_to_string(&self, path: &str) -> Result<String, String>;
18+
19+
// Return (pid,uid) for every file /proc/{PID}. Return a sensible error message in case
20+
// something goes really, really wrong, but otherwise try to make the best of it.
21+
fn read_proc_pids(&self) -> Result<Vec<(usize,u32)>, String>;
22+
23+
// Try to figure out the user's name from system tables, this may be an expensive operation.
24+
fn user_by_uid(&self, uid: u32) -> Option<String>;
25+
26+
fn clock_ticks_per_sec(&self) -> usize;
27+
28+
fn page_size(&self) -> usize;
29+
30+
fn now(&self) -> u64;
31+
}
32+
33+
// RealFS is used to actually access /proc and other system tables
34+
35+
pub struct RealFS {
36+
}
37+
38+
impl RealFS {
39+
pub fn new() -> RealFS {
40+
RealFS{}
41+
}
42+
}
43+
44+
impl ProcfsAPI for RealFS {
45+
fn read_to_string(&self, path: &str) -> Result<String, String> {
46+
match fs::read_to_string(path::Path::new("/proc/{path}")) {
47+
Ok(s) => Ok(s),
48+
Err(_) => Err(format!("Unable to read /proc/{path}")),
49+
}
50+
}
51+
52+
fn read_proc_pids(&self) -> Result<Vec<(usize,u32)>, String> {
53+
let mut pids = vec![];
54+
if let Ok(dir) = fs::read_dir("/proc") {
55+
for dirent in dir {
56+
if let Ok(dirent) = dirent {
57+
if let Ok(meta) = dirent.metadata() {
58+
let uid = meta.st_uid();
59+
if let Some(name) = dirent.path().file_name() {
60+
if let Ok(pid) = name.to_string_lossy().parse::<usize>() {
61+
pids.push((pid, uid));
62+
}
63+
}
64+
}
65+
}
66+
}
67+
} else {
68+
return Err(format!("Could not open /proc"));
69+
};
70+
Ok(pids)
71+
}
72+
73+
fn user_by_uid(&self, uid: u32) -> Option<String> {
74+
if let Some(u) = get_user_by_uid(uid) {
75+
Some(u.name().to_string_lossy().to_string())
76+
} else {
77+
None
78+
}
79+
}
80+
81+
fn clock_ticks_per_sec(&self) -> usize {
82+
unsafe { libc::sysconf(libc::_SC_CLK_TCK) as usize }
83+
}
84+
85+
fn page_size(&self) -> usize {
86+
page_size::get() / 1024
87+
}
88+
89+
fn now(&self) -> u64 {
90+
unix_now()
91+
}
92+
}
93+
94+
pub fn unix_now() -> u64 {
95+
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
96+
}
97+
98+
// MockFS is used for testing
99+
100+
#[cfg(test)]
101+
pub struct MockFS {
102+
files: HashMap<String,String>,
103+
pids: Vec<(usize,u32)>,
104+
users: HashMap<u32,String>,
105+
ticks_per_sec: usize,
106+
pagesz: usize,
107+
now: u64,
108+
}
109+
110+
#[cfg(test)]
111+
impl MockFS {
112+
pub fn new(
113+
files: HashMap<String,String>,
114+
pids: Vec<(usize,u32)>,
115+
users: HashMap<u32,String>,
116+
now: u64,
117+
) -> MockFS {
118+
MockFS {
119+
files,
120+
pids,
121+
users,
122+
ticks_per_sec: 100,
123+
pagesz: 4,
124+
now,
125+
}
126+
}
127+
}
128+
129+
#[cfg(test)]
130+
impl ProcfsAPI for MockFS {
131+
fn read_to_string(&self, path: &str) -> Result<String, String> {
132+
match self.files.get(path) {
133+
Some(s) => Ok(s.clone()),
134+
None => Err(format!("Unable to read /proc/{path}")),
135+
}
136+
}
137+
138+
fn read_proc_pids(&self) -> Result<Vec<(usize,u32)>, String> {
139+
Ok(self.pids.clone())
140+
}
141+
142+
fn user_by_uid(&self, uid: u32) -> Option<String> {
143+
match self.users.get(&uid) {
144+
Some(s) => Some(s.clone()),
145+
None => None
146+
}
147+
}
148+
149+
fn clock_ticks_per_sec(&self) -> usize {
150+
self.ticks_per_sec
151+
}
152+
153+
fn page_size(&self) -> usize {
154+
self.pagesz
155+
}
156+
157+
fn now(&self) -> u64 {
158+
self.now
159+
}
160+
}
161+

0 commit comments

Comments
 (0)