Since the EuraliOS file system is the main way to provide and control access to resources, we’re going to need plenty of user-space servers to handle messages as the RAMdisk server does. To avoid repeating code, we’ll generalise the RAMdisk code and turn the message handling code into a standard library module. It’s also a good excuse to learn about Rust traits.
The functions to handle messages for files currently allows reading
and writing data, and writing is simplified to just appending. A third
function is to truncate (empty) a file in the open
function. The
interface to something that is like a file is therefore defined (in
euralios_std::server
) as
pub trait FileLike {
/// Number of bytes in the file
fn len(&self) -> usize;
/// Read data starting at a given offset, storing in pre-allocated slice
fn read(&self, start: usize, buffer: &mut [u8]) -> Result<usize, syscalls::SyscallError>;
/// Write data starting at given offset
fn write(&mut self, start: usize, buffer: &[u8]) -> Result<usize, syscalls::SyscallError>;
/// Delete contents
fn clear(&mut self) -> Result<(), syscalls::SyscallError>;
}
The read
function takes a pre-allocated mutable slice as input, so
that the caller can control memory allocation. In our case we are allocating
memory chunks for sending in messages.
Not all files may implement all of these functions, so read
, write
and clear
have default implementations that return
Err(syscalls::SYSCALL_ERROR_NOT_IMPLEMENTED)
. Errors should be
handled by the standard library code, and returned as an ERROR
message to the caller. The len
function in most cases is simple, but
less clear for streams where the length isn’t known. Perhaps that
should also return a Result
(or Option
) to indicate where length
isn’t known. The clear
function should probably also be generalised
at some point to truncate(length:usize)
but that isn’t needed yet.
Directories need to look up files and subdirectories, and list their contents. Both files and directories could implement a common trait and be treated the same by directories, and my C++ instinct was to create more complicated types and use inheritance. Rust, or perhaps my lack of experience with it, seems to discourage this, with all my attempts at adding abstraction leading to more code and more complicated code. On balance I think a simpler approach is better for now, so files and directories are looked up separately:
pub trait DirLike {
/// Lookup and return shared reference to a directory
fn get_dir(&self, name: &str) -> Result<Arc<RwLock<dyn DirLike + Sync + Send>>, syscalls::SyscallError>;
/// Lookup and return shared reference to a file
fn get_file(&self, name: &str) -> Result<Arc<RwLock<dyn FileLike + Sync + Send>>, syscalls::SyscallError>;
/// Return a JSON string describing the directory and its contents
fn query(&self) -> String;
The pci
program handles messages and behaves a bit like a file
server. We can simplify it by removing most of the message handling
code for pci-specific functions like reading device BAR registers, and
replacing them with file read/writes.
The root directory of the pci
server represents a collection of devices:
struct DeviceCollection {
devices: BTreeMap<String, Arc<RwLock<Device>>>
}
impl DirLike for DeviceCollection {
/// Each subdirectory is a PCI Device
fn get_dir(&self, name: &str) -> Result<Arc<RwLock<dyn DirLike + Send + Sync>>, syscalls::SyscallError> {
match self.devices.get(name) {
Some(device) => Ok(device.clone()),
None => Err(syscalls::SYSCALL_ERROR_NOTFOUND)
}
}
/// No files; always returns not found error
fn get_file(&self, name: &str) -> Result<Arc<RwLock<dyn FileLike + Send + Sync>>, syscalls::SyscallError> {
Err(syscalls::SYSCALL_ERROR_NOTFOUND)
}
fn query(&self) -> String {
// Create JSON description of devices
}
}
For this to work the Device
struct should also implement the DirLike
trait:
impl DirLike for Device {
fn get_dir(&self, name: &str) -> Result<Arc<RwLock<dyn DirLike + Send + Sync>>, syscalls::SyscallError> {
Err(syscalls::SYSCALL_ERROR_NOTFOUND)
}
fn get_file(&self, name: &str) -> Result<Arc<RwLock<dyn FileLike + Send + Sync>>, syscalls::SyscallError> {
Err(syscalls::SYSCALL_ERROR_NOTFOUND)
}
fn query(&self) -> String {
// Put device information into JSON string
}
}
We don’t yet have any files under each PCI device directory, but we can add those later to provide ways to access and configure devices.
Since pci
is now multi-threaded, we need to make sure that the
different threads can’t interfere with each other. Unfortunately all
access to PCI data involves writing to the CONFIG_ADDRESS
port
0xCF8
and then reading or writing port CONFIG_DATA
, 0xCFC
.
To control access we will use a mutex and the lazy_static crate that was used in parts of the kernel.
Putting the functions to read and write data to the PCI configuration
address and data ports into a struct
implementation:
struct PciPorts {}
impl PciPorts {
const CONFIG_ADDRESS: u16 = 0xCF8;
const CONFIG_DATA: u16 = 0xCFC;
/// Write to Address and Data ports
fn write(&mut self, address: u32, value: u32) {
unsafe {
asm!("out dx, eax",
in("dx") Self::CONFIG_ADDRESS,
in("eax") address,
options(nomem, nostack));
asm!("out dx, eax",
in("dx") Self::CONFIG_DATA,
in("eax") value,
options(nomem, nostack));
}
}
/// Write to Address port, read from Data port
/// Note: Mutates ports values so needs mut self
fn read(&mut self, address: u32) -> u32 {
let value: u32;
unsafe {
asm!("out dx, eax",
in("dx") Self::CONFIG_ADDRESS,
in("eax") address,
options(nomem, nostack));
asm!("in eax, dx",
in("dx") Self::CONFIG_DATA,
lateout("eax") value,
options(nomem, nostack));
}
value
}
}
we can then put one of these behind a mutex:
lazy_static! {
static ref PORTS: Mutex<PciPorts> = Mutex::new(PciPorts{});
}
There’s nothing to prevent us from modifying the ports in another part of the code but if all access is through this then we should avoid many problems. Race conditions can still occur however if a caller releases the mutex lock between reading and writing values to device registers e.g. to set or clear a bit.
In the open
function there was a chain of else if
clauses, including one like:
} else if let Ok(file) = dir.read().get_file(key) { // <- Locked
// Opening an existing file
} else if path_iter.peek().is_some() {
// Missing a directory
} else if (flags & message::O_CREATE) == message::O_CREATE {
// Create a file
let new_file = dir.write().make_file(key)?; // <- Hangs
}
This code locked up because the lock created by dir.read()
was not released
by the time dir.write()
is called. A solution is to define an intermediate
variable:
} else {
let result_file = dir.read().get_file(key); // <- Locks and releases
if let Ok(file) = result_file {
// Opening an existing file
} else if path_iter.peek().is_some() {
// Missing a directory
} else if (flags & message::O_CREATE) == message::O_CREATE {
// Create a file
let new_file = dir.write().make_file(key)?;
}
}