Skip to content

Commit b0c16e7

Browse files
ryanbreenclaude
andcommitted
feat(devfs): implement virtual device filesystem
Add devfs support with virtual device nodes: - /dev/null: write discards data, read returns EOF - /dev/zero: read returns zeros, write discards data - /dev/console: write outputs to serial port - /dev/tty: write outputs to serial port Implementation: - New kernel/src/fs/devfs/ module with DeviceType enum - FdKind::Device variant for device file descriptors - FdKind::DevfsDirectory variant for /dev directory listing - sys_open routes /dev/* paths to devfs handler - sys_open handles /dev as a listable directory - sys_getdents64 returns virtual entries for /dev directory - sys_read/sys_write handle device operations - sys_fstat returns proper S_IFCHR/S_IFDIR mode and st_rdev - sys_access handles /dev and /dev/* paths - Poll support for device files Interactive features: - `ls /dev` works (directory listing) - `devtest` shell command for interactive device testing - Includes userspace test (devfs_test) verifying all behaviors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bf0eb38 commit b0c16e7

File tree

15 files changed

+1055
-1
lines changed

15 files changed

+1055
-1
lines changed

kernel/src/fs/devfs/mod.rs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//! Device Filesystem (devfs)
2+
//!
3+
//! Provides a virtual filesystem mounted at /dev containing device nodes.
4+
//! Unlike ext2, devfs doesn't use disk storage - all nodes are virtual.
5+
//!
6+
//! # Supported Devices
7+
//!
8+
//! - `/dev/null` - Discards all writes, reads return EOF
9+
//! - `/dev/zero` - Discards all writes, reads return zero bytes
10+
//! - `/dev/console` - System console (serial output)
11+
//! - `/dev/tty` - Current process's controlling terminal
12+
//!
13+
//! # Architecture
14+
//!
15+
//! ```text
16+
//! sys_open("/dev/null")
17+
//! |
18+
//! v
19+
//! devfs_open()
20+
//! |
21+
//! v
22+
//! DeviceFile { device_type: DevNull }
23+
//! |
24+
//! v
25+
//! sys_read/sys_write dispatch to device handlers
26+
//! ```
27+
28+
use alloc::string::String;
29+
use alloc::vec::Vec;
30+
use spin::Mutex;
31+
32+
/// Device types supported by devfs
33+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34+
pub enum DeviceType {
35+
/// /dev/null - discards writes, reads return EOF
36+
Null,
37+
/// /dev/zero - discards writes, reads return zero bytes
38+
Zero,
39+
/// /dev/console - system console (serial port)
40+
Console,
41+
/// /dev/tty - controlling terminal
42+
Tty,
43+
}
44+
45+
impl DeviceType {
46+
/// Get the device name (without /dev/ prefix)
47+
pub fn name(&self) -> &'static str {
48+
match self {
49+
DeviceType::Null => "null",
50+
DeviceType::Zero => "zero",
51+
DeviceType::Console => "console",
52+
DeviceType::Tty => "tty",
53+
}
54+
}
55+
56+
/// Get the inode number for this device
57+
/// Using fixed inode numbers for devices
58+
pub fn inode(&self) -> u64 {
59+
match self {
60+
DeviceType::Null => 1,
61+
DeviceType::Zero => 2,
62+
DeviceType::Console => 3,
63+
DeviceType::Tty => 4,
64+
}
65+
}
66+
67+
/// Check if this device is readable
68+
pub fn is_readable(&self) -> bool {
69+
true // All devices are readable
70+
}
71+
72+
/// Check if this device is writable
73+
pub fn is_writable(&self) -> bool {
74+
true // All devices are writable
75+
}
76+
}
77+
78+
/// A devfs device node
79+
#[derive(Debug, Clone)]
80+
pub struct DeviceNode {
81+
/// Device type
82+
pub device_type: DeviceType,
83+
/// Device major number (for st_rdev)
84+
pub major: u32,
85+
/// Device minor number (for st_rdev)
86+
pub minor: u32,
87+
}
88+
89+
impl DeviceNode {
90+
/// Create a new device node
91+
pub fn new(device_type: DeviceType, major: u32, minor: u32) -> Self {
92+
Self {
93+
device_type,
94+
major,
95+
minor,
96+
}
97+
}
98+
99+
/// Get the combined device number (major << 8 | minor)
100+
pub fn rdev(&self) -> u64 {
101+
((self.major as u64) << 8) | (self.minor as u64)
102+
}
103+
}
104+
105+
/// Global devfs state
106+
struct DevfsState {
107+
/// Registered devices
108+
devices: Vec<DeviceNode>,
109+
/// Whether devfs is initialized
110+
initialized: bool,
111+
}
112+
113+
impl DevfsState {
114+
const fn new() -> Self {
115+
Self {
116+
devices: Vec::new(),
117+
initialized: false,
118+
}
119+
}
120+
}
121+
122+
static DEVFS: Mutex<DevfsState> = Mutex::new(DevfsState::new());
123+
124+
/// Initialize devfs with standard devices
125+
pub fn init() {
126+
let mut devfs = DEVFS.lock();
127+
if devfs.initialized {
128+
log::warn!("devfs already initialized");
129+
return;
130+
}
131+
132+
// Register standard devices
133+
// Major 1 = memory devices (null, zero, etc.)
134+
devfs.devices.push(DeviceNode::new(DeviceType::Null, 1, 3)); // /dev/null
135+
devfs.devices.push(DeviceNode::new(DeviceType::Zero, 1, 5)); // /dev/zero
136+
137+
// Major 5 = TTY devices
138+
devfs.devices.push(DeviceNode::new(DeviceType::Console, 5, 1)); // /dev/console
139+
devfs.devices.push(DeviceNode::new(DeviceType::Tty, 5, 0)); // /dev/tty
140+
141+
devfs.initialized = true;
142+
log::info!("devfs: initialized with {} devices", devfs.devices.len());
143+
144+
// Register mount point
145+
crate::fs::vfs::mount::mount("/dev", "devfs");
146+
}
147+
148+
/// Look up a device by path (without /dev/ prefix)
149+
pub fn lookup(name: &str) -> Option<DeviceNode> {
150+
let devfs = DEVFS.lock();
151+
for device in &devfs.devices {
152+
if device.device_type.name() == name {
153+
return Some(device.clone());
154+
}
155+
}
156+
None
157+
}
158+
159+
/// Look up a device by inode number
160+
pub fn lookup_by_inode(inode: u64) -> Option<DeviceNode> {
161+
let devfs = DEVFS.lock();
162+
for device in &devfs.devices {
163+
if device.device_type.inode() == inode {
164+
return Some(device.clone());
165+
}
166+
}
167+
None
168+
}
169+
170+
/// List all device names (for /dev directory listing)
171+
pub fn list_devices() -> Vec<String> {
172+
let devfs = DEVFS.lock();
173+
devfs.devices.iter().map(|d| String::from(d.device_type.name())).collect()
174+
}
175+
176+
/// Check if devfs is initialized
177+
pub fn is_initialized() -> bool {
178+
DEVFS.lock().initialized
179+
}
180+
181+
/// Device file operations - read from device
182+
pub fn device_read(device_type: DeviceType, buf: &mut [u8]) -> Result<usize, i32> {
183+
match device_type {
184+
DeviceType::Null => {
185+
// /dev/null always returns EOF (0 bytes read)
186+
Ok(0)
187+
}
188+
DeviceType::Zero => {
189+
// /dev/zero fills buffer with zeros
190+
for byte in buf.iter_mut() {
191+
*byte = 0;
192+
}
193+
Ok(buf.len())
194+
}
195+
DeviceType::Console | DeviceType::Tty => {
196+
// Console/TTY read - for now return EAGAIN (no input available)
197+
// In the future, this would read from keyboard buffer
198+
Err(-11) // EAGAIN
199+
}
200+
}
201+
}
202+
203+
/// Device file operations - write to device
204+
pub fn device_write(device_type: DeviceType, buf: &[u8]) -> Result<usize, i32> {
205+
match device_type {
206+
DeviceType::Null | DeviceType::Zero => {
207+
// /dev/null and /dev/zero discard all writes
208+
Ok(buf.len())
209+
}
210+
DeviceType::Console | DeviceType::Tty => {
211+
// Console/TTY write - output to serial port
212+
for &byte in buf {
213+
crate::serial::write_byte(byte);
214+
}
215+
Ok(buf.len())
216+
}
217+
}
218+
}

kernel/src/fs/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Filesystem abstraction layer
22
//!
3-
//! Provides support for various filesystem types including ext2.
3+
//! Provides support for various filesystem types including ext2 and devfs.
44
//!
55
//! Note: The filesystem layer is complete but not yet integrated into
66
//! kernel initialization. Call ext2::init_root_fs() to mount the root
@@ -9,5 +9,6 @@
99
// Allow dead code for filesystem modules until they are integrated into kernel init
1010
#![allow(dead_code)]
1111

12+
pub mod devfs;
1213
pub mod ext2;
1314
pub mod vfs;

kernel/src/ipc/fd.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub struct DirectoryFile {
7272
/// - Pipes (read and write ends)
7373
/// - UDP sockets (with future support for TCP, files, etc.)
7474
/// - Regular files (filesystem files)
75+
/// - Device files (/dev/null, /dev/zero, etc.)
7576
///
7677
/// Note: Sockets use Arc<Mutex<>> like pipes because they need to be shared
7778
/// and cannot be cloned (they contain unique socket handles and rx queues).
@@ -90,6 +91,10 @@ pub enum FdKind {
9091
RegularFile(Arc<Mutex<RegularFile>>),
9192
/// Directory file descriptor (for getdents)
9293
Directory(Arc<Mutex<DirectoryFile>>),
94+
/// Device file (/dev/null, /dev/zero, /dev/console, /dev/tty)
95+
Device(crate::fs::devfs::DeviceType),
96+
/// /dev directory (virtual directory for listing devices)
97+
DevfsDirectory { position: u64 },
9398
}
9499

95100
impl core::fmt::Debug for FdKind {
@@ -101,6 +106,8 @@ impl core::fmt::Debug for FdKind {
101106
FdKind::UdpSocket(_) => write!(f, "UdpSocket"),
102107
FdKind::RegularFile(_) => write!(f, "RegularFile"),
103108
FdKind::Directory(_) => write!(f, "Directory"),
109+
FdKind::Device(dt) => write!(f, "Device({:?})", dt),
110+
FdKind::DevfsDirectory { position } => write!(f, "DevfsDirectory(pos={})", position),
104111
}
105112
}
106113
}
@@ -411,6 +418,14 @@ impl Drop for FdTable {
411418
// Directory cleanup handled by Arc refcount
412419
log::debug!("FdTable::drop() - releasing directory fd {}", i);
413420
}
421+
FdKind::Device(_) => {
422+
// Device files don't need cleanup
423+
log::debug!("FdTable::drop() - releasing device fd {}", i);
424+
}
425+
FdKind::DevfsDirectory { .. } => {
426+
// Devfs directory doesn't need cleanup
427+
log::debug!("FdTable::drop() - releasing devfs directory fd {}", i);
428+
}
414429
}
415430
}
416431
}

kernel/src/ipc/poll.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,43 @@ pub fn poll_fd(fd_entry: &FileDescriptor, events: i16) -> i16 {
112112
revents |= events::POLLIN;
113113
}
114114
}
115+
FdKind::Device(device_type) => {
116+
// Device files have different poll behavior based on type
117+
use crate::fs::devfs::DeviceType;
118+
match device_type {
119+
DeviceType::Null => {
120+
// /dev/null: always readable (returns EOF), always writable
121+
if (events & events::POLLIN) != 0 {
122+
revents |= events::POLLIN;
123+
}
124+
if (events & events::POLLOUT) != 0 {
125+
revents |= events::POLLOUT;
126+
}
127+
}
128+
DeviceType::Zero => {
129+
// /dev/zero: always readable (infinite zeros), always writable
130+
if (events & events::POLLIN) != 0 {
131+
revents |= events::POLLIN;
132+
}
133+
if (events & events::POLLOUT) != 0 {
134+
revents |= events::POLLOUT;
135+
}
136+
}
137+
DeviceType::Console | DeviceType::Tty => {
138+
// Console/TTY: always writable, not readable (no input buffer yet)
139+
if (events & events::POLLOUT) != 0 {
140+
revents |= events::POLLOUT;
141+
}
142+
// TODO: Check input buffer for POLLIN when implemented
143+
}
144+
}
145+
}
146+
FdKind::DevfsDirectory { .. } => {
147+
// Devfs directory is always "readable" for getdents purposes
148+
if (events & events::POLLIN) != 0 {
149+
revents |= events::POLLIN;
150+
}
151+
}
115152
}
116153

117154
revents

kernel/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ fn kernel_main(boot_info: &'static mut bootloader_api::BootInfo) -> ! {
189189
Err(e) => log::warn!("Failed to mount ext2 root: {:?}", e),
190190
}
191191

192+
// Initialize devfs (/dev virtual filesystem)
193+
crate::fs::devfs::init();
194+
log::info!("devfs initialized at /dev");
195+
192196
// Update IST stacks with per-CPU emergency stacks
193197
gdt::update_ist_stacks();
194198
log::info!("Updated IST stacks with per-CPU emergency and page fault stacks");
@@ -763,6 +767,10 @@ fn kernel_main_continue() -> ! {
763767
log::info!("=== FS TEST: access() syscall ===");
764768
test_exec::test_access();
765769

770+
// Test devfs device files (/dev/null, /dev/zero, /dev/console, /dev/tty)
771+
log::info!("=== FS TEST: devfs device files ===");
772+
test_exec::test_devfs();
773+
766774
// Test Rust std library support
767775
log::info!("=== STD TEST: Rust std library support ===");
768776
test_exec::test_hello_std_real();

kernel/src/process/process.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,14 @@ impl Process {
238238
// Directory cleanup handled by Arc refcount
239239
log::debug!("Process::close_all_fds() - released directory fd {}", fd);
240240
}
241+
FdKind::Device(_) => {
242+
// Device files don't need cleanup
243+
log::debug!("Process::close_all_fds() - released device fd {}", fd);
244+
}
245+
FdKind::DevfsDirectory { .. } => {
246+
// Devfs directory doesn't need cleanup
247+
log::debug!("Process::close_all_fds() - released devfs directory fd {}", fd);
248+
}
241249
}
242250
}
243251
}

0 commit comments

Comments
 (0)