⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/fs/
procfs.rs

1//! Process Filesystem (/proc)
2//!
3//! Provides information about running processes and system state.
4//! Populated with real data from CPUID, frame allocator, timer, and
5//! process table.
6
7#![allow(clippy::useless_format)]
8
9use alloc::{format, string::String, sync::Arc, vec::Vec};
10
11use super::{DirEntry, Filesystem, Metadata, NodeType, Permissions, VfsNode};
12use crate::error::{FsError, KernelError};
13
14/// ProcFS node types
15enum ProcNodeType {
16    Root,
17    ProcessDir(u64),
18    ProcessFile(u64, String),
19    SystemFile(String),
20}
21
22/// ProcFS node
23struct ProcNode {
24    node_type: ProcNodeType,
25}
26
27impl ProcNode {
28    fn new_root() -> Self {
29        Self {
30            node_type: ProcNodeType::Root,
31        }
32    }
33
34    fn new_process_dir(pid: u64) -> Self {
35        Self {
36            node_type: ProcNodeType::ProcessDir(pid),
37        }
38    }
39
40    fn new_process_file(pid: u64, name: String) -> Self {
41        Self {
42            node_type: ProcNodeType::ProcessFile(pid, name),
43        }
44    }
45
46    fn new_system_file(name: String) -> Self {
47        Self {
48            node_type: ProcNodeType::SystemFile(name),
49        }
50    }
51}
52
53impl VfsNode for ProcNode {
54    fn node_type(&self) -> NodeType {
55        match &self.node_type {
56            ProcNodeType::Root | ProcNodeType::ProcessDir(_) => NodeType::Directory,
57            ProcNodeType::ProcessFile(_, _) | ProcNodeType::SystemFile(_) => NodeType::File,
58        }
59    }
60
61    fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<usize, KernelError> {
62        let content = match &self.node_type {
63            ProcNodeType::SystemFile(name) => {
64                match name.as_str() {
65                    "version" => {
66                        format!(
67                            "VeridianOS version 0.5.0 (gcc 14.2.0) #1 SMP {}\n",
68                            env!("CARGO_PKG_VERSION")
69                        )
70                    }
71                    "uptime" => {
72                        let secs = crate::arch::timer::get_timestamp_secs();
73                        let frac_ms = crate::arch::timer::get_timestamp_ms() % 1000;
74                        // Linux format: uptime_secs idle_secs
75                        format!("{}.{:02} 0.00\n", secs, frac_ms / 10)
76                    }
77                    "meminfo" => {
78                        let stats = crate::mm::get_memory_stats();
79                        let total_kb = stats.total_frames * 4; // 4KB per frame
80                        let free_kb = stats.free_frames * 4;
81                        let used_kb = total_kb.saturating_sub(free_kb);
82                        let cached_kb = stats.cached_frames * 4;
83                        let available_kb = free_kb + cached_kb;
84                        let buffers_kb = 0usize;
85                        let slab_kb = 0usize;
86
87                        format!(
88                            "MemTotal:       {:>8} kB\n\
89                             MemFree:        {:>8} kB\n\
90                             MemAvailable:   {:>8} kB\n\
91                             Buffers:        {:>8} kB\n\
92                             Cached:         {:>8} kB\n\
93                             Slab:           {:>8} kB\n\
94                             MemUsed:        {:>8} kB\n",
95                            total_kb,
96                            free_kb,
97                            available_kb,
98                            buffers_kb,
99                            cached_kb,
100                            slab_kb,
101                            used_kb,
102                        )
103                    }
104                    "cpuinfo" => generate_cpuinfo(),
105                    "loadavg" => generate_loadavg(),
106                    _ => String::new(),
107                }
108            }
109            ProcNodeType::ProcessFile(pid, name) => {
110                match name.as_str() {
111                    "status" => {
112                        // Get actual process information
113                        if let Some(process) =
114                            crate::process::get_process(crate::process::ProcessId(*pid))
115                        {
116                            let state = match process.get_state() {
117                                crate::process::ProcessState::Creating => "N (new)",
118                                crate::process::ProcessState::Ready => "R (running)",
119                                crate::process::ProcessState::Running => "R (running)",
120                                crate::process::ProcessState::Blocked => "S (sleeping)",
121                                crate::process::ProcessState::Sleeping => "S (sleeping)",
122                                crate::process::ProcessState::Zombie => "Z (zombie)",
123                                crate::process::ProcessState::Dead => "X (dead)",
124                            };
125
126                            #[cfg(feature = "alloc")]
127                            let name = &process.name;
128                            #[cfg(not(feature = "alloc"))]
129                            let name = "process";
130
131                            let parent = process.parent.unwrap_or(crate::process::ProcessId(0));
132
133                            format!(
134                                "Name:\t{}\nPid:\t{}\nPPid:\t{}\nState:\t{}\n",
135                                name, pid, parent.0, state
136                            )
137                        } else {
138                            format!("Name:\tProcess\nPid:\t{}\nState:\tR (running)\n", pid)
139                        }
140                    }
141                    "cmdline" => {
142                        // Get actual command line
143                        if let Some(process) =
144                            crate::process::get_process(crate::process::ProcessId(*pid))
145                        {
146                            #[cfg(feature = "alloc")]
147                            let name = &process.name;
148                            #[cfg(not(feature = "alloc"))]
149                            let name = "process";
150                            format!("{}\0", name)
151                        } else {
152                            format!("init\0")
153                        }
154                    }
155                    _ => String::new(),
156                }
157            }
158            _ => return Err(KernelError::FsError(FsError::NotAFile)),
159        };
160
161        let bytes = content.as_bytes();
162        if offset >= bytes.len() {
163            return Ok(0);
164        }
165
166        let bytes_to_read = core::cmp::min(buffer.len(), bytes.len() - offset);
167        buffer[..bytes_to_read].copy_from_slice(&bytes[offset..offset + bytes_to_read]);
168        Ok(bytes_to_read)
169    }
170
171    fn write(&self, _offset: usize, _data: &[u8]) -> Result<usize, KernelError> {
172        Err(KernelError::FsError(FsError::ReadOnly))
173    }
174
175    fn metadata(&self) -> Result<Metadata, KernelError> {
176        let node_type = match &self.node_type {
177            ProcNodeType::Root | ProcNodeType::ProcessDir(_) => NodeType::Directory,
178            _ => NodeType::File,
179        };
180
181        Ok(Metadata {
182            node_type,
183            size: 0,
184            permissions: Permissions::read_only(),
185            uid: 0,
186            gid: 0,
187            created: 0,
188            modified: 0,
189            accessed: 0,
190            inode: 0,
191        })
192    }
193
194    fn readdir(&self) -> Result<Vec<DirEntry>, KernelError> {
195        let mut entries = Vec::new();
196
197        entries.push(DirEntry {
198            name: String::from("."),
199            node_type: NodeType::Directory,
200            inode: 0,
201        });
202
203        entries.push(DirEntry {
204            name: String::from(".."),
205            node_type: NodeType::Directory,
206            inode: 0,
207        });
208
209        match &self.node_type {
210            ProcNodeType::Root => {
211                // System files
212                entries.push(DirEntry {
213                    name: String::from("version"),
214                    node_type: NodeType::File,
215                    inode: 0,
216                });
217
218                entries.push(DirEntry {
219                    name: String::from("uptime"),
220                    node_type: NodeType::File,
221                    inode: 0,
222                });
223
224                entries.push(DirEntry {
225                    name: String::from("meminfo"),
226                    node_type: NodeType::File,
227                    inode: 0,
228                });
229
230                entries.push(DirEntry {
231                    name: String::from("cpuinfo"),
232                    node_type: NodeType::File,
233                    inode: 0,
234                });
235
236                entries.push(DirEntry {
237                    name: String::from("loadavg"),
238                    node_type: NodeType::File,
239                    inode: 0,
240                });
241
242                // Add process directories for all running processes
243                if let Some(process_list) = crate::process::get_process_list() {
244                    for pid in process_list {
245                        entries.push(DirEntry {
246                            name: format!("{}", pid),
247                            node_type: NodeType::Directory,
248                            inode: pid,
249                        });
250                    }
251                } else {
252                    // Fallback: just show init process
253                    entries.push(DirEntry {
254                        name: String::from("1"),
255                        node_type: NodeType::Directory,
256                        inode: 1,
257                    });
258                }
259            }
260            ProcNodeType::ProcessDir(_pid) => {
261                entries.push(DirEntry {
262                    name: String::from("status"),
263                    node_type: NodeType::File,
264                    inode: 0,
265                });
266
267                entries.push(DirEntry {
268                    name: String::from("cmdline"),
269                    node_type: NodeType::File,
270                    inode: 0,
271                });
272            }
273            _ => return Err(KernelError::FsError(FsError::NotADirectory)),
274        }
275
276        Ok(entries)
277    }
278
279    fn lookup(&self, name: &str) -> Result<Arc<dyn VfsNode>, KernelError> {
280        match &self.node_type {
281            ProcNodeType::Root => {
282                // Check for system files
283                match name {
284                    "version" | "uptime" | "meminfo" | "cpuinfo" | "loadavg" => {
285                        Ok(Arc::new(ProcNode::new_system_file(String::from(name)))
286                            as Arc<dyn VfsNode>)
287                    }
288                    _ => {
289                        // Try to parse as PID
290                        if let Ok(pid) = name.parse::<u64>() {
291                            // Validate PID exists in process table
292                            if crate::process::get_process(crate::process::ProcessId(pid)).is_some()
293                            {
294                                Ok(Arc::new(ProcNode::new_process_dir(pid)) as Arc<dyn VfsNode>)
295                            } else {
296                                Err(KernelError::FsError(FsError::NotFound))
297                            }
298                        } else {
299                            Err(KernelError::FsError(FsError::NotFound))
300                        }
301                    }
302                }
303            }
304            ProcNodeType::ProcessDir(pid) => match name {
305                "status" | "cmdline" => Ok(Arc::new(ProcNode::new_process_file(
306                    *pid,
307                    String::from(name),
308                )) as Arc<dyn VfsNode>),
309                _ => Err(KernelError::FsError(FsError::NotFound)),
310            },
311            _ => Err(KernelError::FsError(FsError::NotADirectory)),
312        }
313    }
314
315    fn create(
316        &self,
317        _name: &str,
318        _permissions: Permissions,
319    ) -> Result<Arc<dyn VfsNode>, KernelError> {
320        Err(KernelError::FsError(FsError::ReadOnly))
321    }
322
323    fn mkdir(
324        &self,
325        _name: &str,
326        _permissions: Permissions,
327    ) -> Result<Arc<dyn VfsNode>, KernelError> {
328        Err(KernelError::FsError(FsError::ReadOnly))
329    }
330
331    fn unlink(&self, _name: &str) -> Result<(), KernelError> {
332        Err(KernelError::FsError(FsError::ReadOnly))
333    }
334
335    fn truncate(&self, _size: usize) -> Result<(), KernelError> {
336        Err(KernelError::FsError(FsError::ReadOnly))
337    }
338}
339
340/// Generate /proc/cpuinfo content with real CPUID data on x86_64.
341fn generate_cpuinfo() -> String {
342    #[cfg(target_arch = "x86_64")]
343    {
344        let brand = cpuid_brand_string();
345        let freq_mhz = crate::arch::timer::hw_ticks_per_second() / 1_000_000;
346
347        format!(
348            "processor\t: 0\nvendor_id\t: {}\nmodel name\t: {}\ncpu MHz\t\t: {}\ncache size\t: 0 \
349             KB\nbogomips\t: {}\n",
350            cpuid_vendor_id(),
351            brand,
352            freq_mhz,
353            freq_mhz * 2,
354        )
355    }
356
357    #[cfg(target_arch = "aarch64")]
358    {
359        let freq_mhz = crate::arch::timer::hw_ticks_per_second() / 1_000_000;
360        format!(
361            "processor\t: 0\narchitecture\t: aarch64\nmodel name\t: ARMv8 Processor\nBogoMIPS\t: \
362             {}\nFeatures\t: fp asimd\n",
363            freq_mhz * 2,
364        )
365    }
366
367    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
368    {
369        format!("processor\t: 0\narchitecture\t: riscv64\nmodel name\t: RISC-V Processor\n")
370    }
371}
372
373/// Generate /proc/loadavg content.
374fn generate_loadavg() -> String {
375    // Count running/total tasks from process table
376    let (running, total) = if let Some(pids) = crate::process::get_process_list() {
377        let total = pids.len();
378        let mut running = 0usize;
379        for pid in &pids {
380            if let Some(p) = crate::process::get_process(crate::process::ProcessId(*pid)) {
381                match p.get_state() {
382                    crate::process::ProcessState::Running | crate::process::ProcessState::Ready => {
383                        running += 1;
384                    }
385                    _ => {}
386                }
387            }
388        }
389        (running, total)
390    } else {
391        (1, 1)
392    };
393
394    // Find highest PID for last field
395    let last_pid = crate::process::get_process_list()
396        .and_then(|pids| pids.iter().copied().max())
397        .unwrap_or(1);
398
399    // Linux format: 1min 5min 15min running/total last_pid
400    format!("0.00 0.00 0.00 {}/{} {}\n", running, total, last_pid)
401}
402
403/// Read CPUID vendor ID string (x86_64 only).
404#[cfg(target_arch = "x86_64")]
405fn cpuid_vendor_id() -> String {
406    let ebx: u32;
407    let ecx: u32;
408    let edx: u32;
409    // SAFETY: CPUID leaf 0 returns vendor string in EBX:EDX:ECX.
410    // push/pop RBX required because CPUID clobbers it.
411    unsafe {
412        core::arch::asm!(
413            "push rbx",
414            "xor eax, eax",
415            "cpuid",
416            "mov {0:e}, ebx",
417            "mov {1:e}, ecx",
418            "mov {2:e}, edx",
419            "pop rbx",
420            out(reg) ebx,
421            out(reg) ecx,
422            out(reg) edx,
423            out("eax") _,
424        );
425    }
426    let mut buf = [0u8; 12];
427    buf[0..4].copy_from_slice(&ebx.to_le_bytes());
428    buf[4..8].copy_from_slice(&edx.to_le_bytes());
429    buf[8..12].copy_from_slice(&ecx.to_le_bytes());
430    String::from_utf8_lossy(&buf).trim_end_matches('\0').into()
431}
432
433/// Read CPUID brand string (x86_64 only, leaves 0x80000002-0x80000004).
434#[cfg(target_arch = "x86_64")]
435fn cpuid_brand_string() -> String {
436    // Check if extended CPUID is supported
437    let max_ext: u32;
438    // SAFETY: CPUID leaf 0x80000000 returns max extended leaf in EAX.
439    unsafe {
440        core::arch::asm!(
441            "push rbx",
442            "mov eax, 0x80000000",
443            "cpuid",
444            "mov {0:e}, eax",
445            "pop rbx",
446            out(reg) max_ext,
447            out("eax") _,
448        );
449    }
450
451    if max_ext < 0x80000004 {
452        return String::from("Unknown CPU");
453    }
454
455    let mut buf = [0u8; 48];
456
457    for (i, leaf) in [0x80000002u32, 0x80000003, 0x80000004].iter().enumerate() {
458        let eax: u32;
459        let ebx: u32;
460        let ecx: u32;
461        let edx: u32;
462        // SAFETY: CPUID leaves 0x80000002-4 return the CPU brand string.
463        unsafe {
464            core::arch::asm!(
465                "push rbx",
466                "mov eax, {leaf:e}",
467                "cpuid",
468                "mov {0:e}, eax",
469                "mov {1:e}, ebx",
470                "mov {2:e}, ecx",
471                "mov {3:e}, edx",
472                "pop rbx",
473                out(reg) eax,
474                out(reg) ebx,
475                out(reg) ecx,
476                out(reg) edx,
477                leaf = in(reg) *leaf,
478                out("eax") _,
479            );
480        }
481        let off = i * 16;
482        buf[off..off + 4].copy_from_slice(&eax.to_le_bytes());
483        buf[off + 4..off + 8].copy_from_slice(&ebx.to_le_bytes());
484        buf[off + 8..off + 12].copy_from_slice(&ecx.to_le_bytes());
485        buf[off + 12..off + 16].copy_from_slice(&edx.to_le_bytes());
486    }
487
488    String::from_utf8_lossy(&buf)
489        .trim_end_matches('\0')
490        .trim()
491        .into()
492}
493
494/// Process filesystem
495pub struct ProcFs {
496    root: Arc<ProcNode>,
497}
498
499impl ProcFs {
500    pub fn new() -> Self {
501        Self {
502            root: Arc::new(ProcNode::new_root()),
503        }
504    }
505}
506
507impl Default for ProcFs {
508    fn default() -> Self {
509        Self::new()
510    }
511}
512
513impl Filesystem for ProcFs {
514    fn root(&self) -> Arc<dyn VfsNode> {
515        self.root.clone() as Arc<dyn VfsNode>
516    }
517
518    fn name(&self) -> &str {
519        "procfs"
520    }
521
522    fn is_readonly(&self) -> bool {
523        true
524    }
525
526    fn sync(&self) -> Result<(), KernelError> {
527        Ok(())
528    }
529}