Add VFS subsystem and initrd filesystem driver (AI)

VFS subsystem (vfs.c/h):
- Mount table with up to 16 mount points, longest-prefix path matching.
- File descriptor table (256 entries, fds 0-2 reserved for std streams).
- Path resolution walks mount table then delegates to filesystem's
  finddir() for each path component.
- Operations: open, close, read, write, seek, readdir, stat.
- Each filesystem driver provides a vfs_fs_ops_t with callbacks.

Initrd filesystem driver (initrd_fs.c/h):
- Read-only VFS driver backed by the CPIO ramdisk.
- Mounted at '/initrd' during boot.
- Zero-copy reads: file data points directly into the CPIO archive
  memory, no allocation or copying needed.
- Supports readdir (flat iteration) and finddir (name lookup).

Bug fix: resolve_path was overwriting file-specific fs_data (set by
finddir, e.g. pointer to CPIO file data) with the mount's fs_data
(NULL). Fixed to preserve fs_data from finddir.

Verified in QEMU: kernel reads /initrd/README via VFS and prints its
contents. Ring 3 user process continues to work.
This commit is contained in:
AI
2026-02-23 12:23:32 +00:00
parent 3d5fb4c267
commit 0c5aa72fd3
8 changed files with 740 additions and 2 deletions

View File

@@ -51,8 +51,8 @@ Once a task is completed, it should be checked off.
- [x] Create a VGA driver. On startup, some memory statistics should be displayed, as well as boot progress.
- [x] Create subsystem for loading new processes in Ring 3.
- [x] Update the build script to generate a ramdisk containing any applications to run. This initial ramdisk is in CPIO format.
- [ ] Write a VFS subsystem.
- [ ] Write a VFS driver that provides the contents of the CPIO initial ramdisk to the VFS layer.
- [x] Write a VFS subsystem.
- [x] Write a VFS driver that provides the contents of the CPIO initial ramdisk to the VFS layer.
- [ ] Create a `hello-world` app. It should print `Hello, World` to its own stdout. The kernel should route this to Qemu and to the VGA dispaly. Ensure this work.
- [ ] Implement the fork system call.
- [ ] Implement environment variables. Apps should be able to modify this, and it should be copied to new forks of an app.

78
docs/vfs.md Normal file
View File

@@ -0,0 +1,78 @@
# Virtual Filesystem (VFS)
## Overview
The VFS provides a unified interface for file and directory operations across
different filesystem implementations. Filesystem drivers register ops structs
and are mounted at specific paths. Path resolution finds the longest-matching
mount point and delegates to that filesystem.
## Architecture
```
User/Kernel Code
vfs_open("/initrd/hello-world", 0)
VFS: find_mount("/initrd/hello-world")
│ → mount "/initrd", rel_path = "hello-world"
resolve_path → initrd_finddir("hello-world")
vfs_read(fd, buf, size)
│ → initrd_read(node, offset, size, buf)
Returns file data from CPIO archive
```
## Mount Points
Filesystems are mounted at absolute paths. The VFS supports up to 16
simultaneous mounts. Path resolution uses longest-prefix matching:
```
Mount: "/initrd" → handles /initrd/*
Mount: "/sys" → handles /sys/*
Mount: "/dev" → handles /dev/*
```
## File Operations
| Function | Description |
|----------|-------------|
| `vfs_open(path, flags)` | Open a file, returns fd |
| `vfs_close(fd)` | Close a file descriptor |
| `vfs_read(fd, buf, size)` | Read bytes, advances offset |
| `vfs_write(fd, buf, size)` | Write bytes, advances offset |
| `vfs_seek(fd, offset, whence)` | Seek within file |
| `vfs_readdir(path, idx, out)` | Read directory entry |
| `vfs_stat(path, out)` | Get file info |
## Filesystem Driver Interface
Each filesystem provides a `vfs_fs_ops_t` struct:
```c
typedef struct vfs_fs_ops {
int (*open)(vfs_node_t *node, uint32_t flags);
void (*close)(vfs_node_t *node);
int32_t (*read)(vfs_node_t *node, uint32_t offset, uint32_t size, void *buf);
int32_t (*write)(vfs_node_t *node, uint32_t offset, uint32_t size, const void *buf);
int (*readdir)(vfs_node_t *dir, uint32_t idx, vfs_dirent_t *out);
int (*finddir)(vfs_node_t *dir, const char *name, vfs_node_t *out);
} vfs_fs_ops_t;
```
## Initrd Filesystem Driver
The initrd filesystem (`initrd_fs.c`) provides read-only access to the CPIO
ramdisk. It is automatically mounted at `/initrd` during boot. Files are
accessed via zero-copy reads directly from the CPIO archive in memory.
## Files
- `src/vfs.h` / `src/vfs.c` — VFS core: mount table, fd table, path resolution
- `src/initrd_fs.h` / `src/initrd_fs.c` — CPIO ramdisk VFS driver

View File

@@ -17,6 +17,8 @@ add_executable(kernel
process.c
syscall.c
cpio.c
vfs.c
initrd_fs.c
interrupts.S
kernel.c
)

123
src/initrd_fs.c Normal file
View File

@@ -0,0 +1,123 @@
/**
* @file initrd_fs.c
* @brief CPIO initial ramdisk VFS driver implementation.
*
* Provides a read-only VFS interface to the CPIO archive loaded at boot.
* Files are accessed directly from the archive memory (zero-copy reads).
*/
#include "initrd_fs.h"
#include "vfs.h"
#include "cpio.h"
#include <string.h>
/* Debug print helpers defined in kernel.c */
extern void offset_print(const char *str);
extern void print_hex(uint32_t val);
/**
* Read from a file in the initrd.
* Data is read directly from the CPIO archive (memory-mapped).
*/
static int32_t initrd_read(vfs_node_t *node, uint32_t offset,
uint32_t size, void *buf) {
if (!node || !node->fs_data || !buf) return -1;
/* fs_data points to the file's data within the CPIO archive */
const uint8_t *data = (const uint8_t *)node->fs_data;
uint32_t file_size = node->size;
if (offset >= file_size) return 0;
uint32_t remaining = file_size - offset;
if (size > remaining) size = remaining;
memcpy(buf, data + offset, size);
return (int32_t)size;
}
/**
* Read a directory entry from the initrd root.
* The initrd is a flat archive — all files are at the root level.
*/
static int initrd_readdir(vfs_node_t *dir, uint32_t idx, vfs_dirent_t *out) {
(void)dir;
uint32_t off = 0;
uint32_t current = 0;
cpio_entry_t entry;
while (cpio_next(&off, &entry) == 0) {
/* Skip the "." directory entry if present */
if (entry.name[0] == '.' && entry.name[1] == '\0') continue;
/* Strip "./" prefix */
const char *name = entry.name;
if (name[0] == '.' && name[1] == '/') name += 2;
/* Skip empty names */
if (*name == '\0') continue;
if (current == idx) {
memset(out, 0, sizeof(vfs_dirent_t));
strncpy(out->name, name, VFS_MAX_NAME - 1);
out->inode = current;
out->type = VFS_FILE;
return 0;
}
current++;
}
return -1; /* No more entries */
}
/**
* Find a file by name within the initrd.
*/
static int initrd_finddir(vfs_node_t *dir, const char *name, vfs_node_t *out) {
(void)dir;
cpio_entry_t entry;
if (cpio_find(name, &entry) != 0) {
return -1;
}
memset(out, 0, sizeof(vfs_node_t));
/* Strip "./" prefix for the node name */
const char *display_name = entry.name;
if (display_name[0] == '.' && display_name[1] == '/') {
display_name += 2;
}
strncpy(out->name, display_name, VFS_MAX_NAME - 1);
out->type = VFS_FILE;
out->size = entry.datasize;
out->mode = entry.mode;
out->fs_data = (void *)entry.data; /* Direct pointer into CPIO archive */
return 0;
}
/** Filesystem operations for the initrd. */
static vfs_fs_ops_t initrd_ops = {
.open = NULL, /* No special open needed */
.close = NULL, /* No special close needed */
.read = initrd_read,
.write = NULL, /* Read-only */
.readdir = initrd_readdir,
.finddir = initrd_finddir,
};
int init_initrd_fs(void) {
if (cpio_count() == 0) {
offset_print(" INITRD_FS: no files in ramdisk\n");
}
int ret = vfs_mount("/initrd", &initrd_ops, NULL);
if (ret != 0) {
offset_print(" INITRD_FS: failed to mount\n");
return -1;
}
offset_print(" INITRD_FS: mounted at /initrd\n");
return 0;
}

20
src/initrd_fs.h Normal file
View File

@@ -0,0 +1,20 @@
/**
* @file initrd_fs.h
* @brief CPIO initial ramdisk VFS driver.
*
* Provides a read-only filesystem backed by the CPIO initial ramdisk.
* Mounted at "/initrd" to expose the contents of the ramdisk via the VFS.
*/
#ifndef INITRD_FS_H
#define INITRD_FS_H
/**
* Initialize the initrd filesystem driver and mount it at "/initrd".
* Must be called after init_vfs() and cpio_init().
*
* @return 0 on success, -1 on failure.
*/
int init_initrd_fs(void);
#endif /* INITRD_FS_H */

View File

@@ -14,6 +14,8 @@
#include "syscall.h"
#include "process.h"
#include "cpio.h"
#include "vfs.h"
#include "initrd_fs.h"
void offset_print(const char *str)
{
@@ -101,6 +103,27 @@ void kernel_main(uint32_t magic, uint32_t addr) {
offset_print("No initrd module found\n");
}
init_vfs();
offset_print("VFS initialized\n");
if (initrd_start != 0) {
init_initrd_fs();
offset_print("Initrd filesystem mounted\n");
/* Test VFS: read a file from the initrd */
int fd = vfs_open("/initrd/README", 0);
if (fd >= 0) {
char buf[64];
int32_t n = vfs_read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
offset_print("VFS read /initrd/README: ");
offset_print(buf);
}
vfs_close(fd);
}
}
init_tss();
offset_print("TSS initialized\n");

295
src/vfs.c Normal file
View File

@@ -0,0 +1,295 @@
/**
* @file vfs.c
* @brief Virtual Filesystem (VFS) subsystem implementation.
*
* Manages mount points and routes file operations to the appropriate
* filesystem driver. Path resolution walks the mount table to find the
* longest-matching mount point, then delegates to that fs's operations.
*/
#include "vfs.h"
#include <string.h>
/* Debug print helpers defined in kernel.c */
extern void offset_print(const char *str);
extern void print_hex(uint32_t val);
/** Mount table. */
static vfs_mount_t mounts[VFS_MAX_MOUNTS];
/** Open file descriptor table. */
static vfs_fd_t fd_table[VFS_MAX_OPEN_FILES];
/**
* Find the mount point that best matches a given path.
* Returns the index of the longest matching mount, or -1 if none.
*
* @param path The absolute path to resolve.
* @param rel_path Output: pointer within `path` past the mount prefix.
* @return Mount index, or -1.
*/
static int find_mount(const char *path, const char **rel_path) {
int best = -1;
uint32_t best_len = 0;
for (int i = 0; i < VFS_MAX_MOUNTS; i++) {
if (!mounts[i].active) continue;
uint32_t mlen = strlen(mounts[i].path);
/* Check if path starts with this mount point */
if (strncmp(path, mounts[i].path, mlen) != 0) continue;
/* Must match at a directory boundary */
if (mlen > 1 && path[mlen] != '\0' && path[mlen] != '/') continue;
if (mlen > best_len) {
best = i;
best_len = mlen;
}
}
if (best >= 0 && rel_path) {
const char *r = path + best_len;
/* Skip leading slash in relative path */
while (*r == '/') r++;
*rel_path = r;
}
return best;
}
/**
* Resolve a path to a VFS node by finding the appropriate mount
* and asking the filesystem driver.
*
* @param path Full absolute path.
* @param out Output node.
* @return 0 on success, -1 on failure.
*/
static int resolve_path(const char *path, vfs_node_t *out) {
const char *rel_path = NULL;
int mount_idx = find_mount(path, &rel_path);
if (mount_idx < 0) {
return -1;
}
vfs_mount_t *mnt = &mounts[mount_idx];
/* If rel_path is empty, we're looking at the mount root */
if (*rel_path == '\0') {
/* Return a directory node for the mount root */
memset(out, 0, sizeof(vfs_node_t));
strncpy(out->name, mnt->path, VFS_MAX_NAME - 1);
out->type = VFS_DIRECTORY;
out->ops = mnt->ops;
out->fs_data = mnt->fs_data;
out->mount_idx = mount_idx;
return 0;
}
/* Walk path components through the filesystem */
vfs_node_t current;
memset(&current, 0, sizeof(vfs_node_t));
current.type = VFS_DIRECTORY;
current.ops = mnt->ops;
current.fs_data = mnt->fs_data;
current.mount_idx = mount_idx;
/* Parse path components */
char component[VFS_MAX_NAME];
const char *p = rel_path;
while (*p) {
/* Skip slashes */
while (*p == '/') p++;
if (*p == '\0') break;
/* Extract component */
const char *end = p;
while (*end && *end != '/') end++;
uint32_t clen = (uint32_t)(end - p);
if (clen >= VFS_MAX_NAME) clen = VFS_MAX_NAME - 1;
memcpy(component, p, clen);
component[clen] = '\0';
/* Look up this component in the current directory */
if (!current.ops || !current.ops->finddir) {
return -1;
}
vfs_node_t child;
if (current.ops->finddir(&current, component, &child) != 0) {
return -1;
}
child.ops = mnt->ops;
/* Preserve fs_data set by finddir; only use mount fs_data if not set */
if (!child.fs_data) {
child.fs_data = mnt->fs_data;
}
child.mount_idx = mount_idx;
current = child;
p = end;
}
*out = current;
return 0;
}
/**
* Find a free file descriptor slot.
* @return Index (>= 0), or -1 if all slots are used.
*/
static int alloc_fd(void) {
/* Skip fds 0,1,2 (stdin, stdout, stderr) for user processes */
for (int i = 3; i < VFS_MAX_OPEN_FILES; i++) {
if (!fd_table[i].active) {
return i;
}
}
return -1;
}
void init_vfs(void) {
memset(mounts, 0, sizeof(mounts));
memset(fd_table, 0, sizeof(fd_table));
offset_print(" VFS: initialized\n");
}
int vfs_mount(const char *path, vfs_fs_ops_t *ops, void *fs_data) {
/* Find a free mount slot */
int slot = -1;
for (int i = 0; i < VFS_MAX_MOUNTS; i++) {
if (!mounts[i].active) {
slot = i;
break;
}
}
if (slot < 0) {
offset_print(" VFS: no free mount slots\n");
return -1;
}
strncpy(mounts[slot].path, path, VFS_MAX_PATH - 1);
mounts[slot].ops = ops;
mounts[slot].fs_data = fs_data;
mounts[slot].active = 1;
offset_print(" VFS: mounted at ");
offset_print(path);
offset_print("\n");
return 0;
}
int vfs_open(const char *path, uint32_t flags) {
vfs_node_t node;
if (resolve_path(path, &node) != 0) {
return -1;
}
int fd = alloc_fd();
if (fd < 0) {
return -1;
}
/* Call filesystem's open if available */
if (node.ops && node.ops->open) {
if (node.ops->open(&node, flags) != 0) {
return -1;
}
}
fd_table[fd].node = node;
fd_table[fd].offset = 0;
fd_table[fd].flags = flags;
fd_table[fd].active = 1;
return fd;
}
void vfs_close(int fd) {
if (fd < 0 || fd >= VFS_MAX_OPEN_FILES) return;
if (!fd_table[fd].active) return;
vfs_fd_t *f = &fd_table[fd];
if (f->node.ops && f->node.ops->close) {
f->node.ops->close(&f->node);
}
f->active = 0;
}
int32_t vfs_read(int fd, void *buf, uint32_t size) {
if (fd < 0 || fd >= VFS_MAX_OPEN_FILES) return -1;
if (!fd_table[fd].active) return -1;
vfs_fd_t *f = &fd_table[fd];
if (!f->node.ops || !f->node.ops->read) return -1;
int32_t bytes = f->node.ops->read(&f->node, f->offset, size, buf);
if (bytes > 0) {
f->offset += (uint32_t)bytes;
}
return bytes;
}
int32_t vfs_write(int fd, const void *buf, uint32_t size) {
if (fd < 0 || fd >= VFS_MAX_OPEN_FILES) return -1;
if (!fd_table[fd].active) return -1;
vfs_fd_t *f = &fd_table[fd];
if (!f->node.ops || !f->node.ops->write) return -1;
int32_t bytes = f->node.ops->write(&f->node, f->offset, size, buf);
if (bytes > 0) {
f->offset += (uint32_t)bytes;
}
return bytes;
}
int32_t vfs_seek(int fd, int32_t offset, int whence) {
if (fd < 0 || fd >= VFS_MAX_OPEN_FILES) return -1;
if (!fd_table[fd].active) return -1;
vfs_fd_t *f = &fd_table[fd];
int32_t new_offset;
switch (whence) {
case VFS_SEEK_SET:
new_offset = offset;
break;
case VFS_SEEK_CUR:
new_offset = (int32_t)f->offset + offset;
break;
case VFS_SEEK_END:
new_offset = (int32_t)f->node.size + offset;
break;
default:
return -1;
}
if (new_offset < 0) return -1;
f->offset = (uint32_t)new_offset;
return new_offset;
}
int vfs_readdir(const char *path, uint32_t idx, vfs_dirent_t *out) {
vfs_node_t node;
if (resolve_path(path, &node) != 0) {
return -1;
}
if (node.type != VFS_DIRECTORY) return -1;
if (!node.ops || !node.ops->readdir) return -1;
return node.ops->readdir(&node, idx, out);
}
int vfs_stat(const char *path, vfs_node_t *out) {
return resolve_path(path, out);
}

197
src/vfs.h Normal file
View File

@@ -0,0 +1,197 @@
/**
* @file vfs.h
* @brief Virtual Filesystem (VFS) subsystem.
*
* Provides a unified interface for file and directory operations across
* different filesystem implementations. Filesystem drivers register
* themselves and can be mounted at specific paths.
*/
#ifndef VFS_H
#define VFS_H
#include <stdint.h>
#include <stddef.h>
/** Maximum number of open files across all processes. */
#define VFS_MAX_OPEN_FILES 256
/** Maximum number of mounted filesystems. */
#define VFS_MAX_MOUNTS 16
/** Maximum path length. */
#define VFS_MAX_PATH 256
/** Maximum filename length. */
#define VFS_MAX_NAME 128
/** File types. */
#define VFS_FILE 0x01
#define VFS_DIRECTORY 0x02
#define VFS_CHARDEV 0x03
#define VFS_BLOCKDEV 0x04
#define VFS_SYMLINK 0x06
/** Seek origins. */
#define VFS_SEEK_SET 0
#define VFS_SEEK_CUR 1
#define VFS_SEEK_END 2
/** Forward declarations. */
struct vfs_node;
struct vfs_dirent;
struct vfs_fs_ops;
/**
* Directory entry, returned by readdir.
*/
typedef struct vfs_dirent {
char name[VFS_MAX_NAME]; /**< Entry name. */
uint32_t inode; /**< Inode number (fs-specific). */
uint8_t type; /**< VFS_FILE, VFS_DIRECTORY, etc. */
} vfs_dirent_t;
/**
* VFS node representing a file or directory.
*/
typedef struct vfs_node {
char name[VFS_MAX_NAME]; /**< Node name. */
uint8_t type; /**< VFS_FILE, VFS_DIRECTORY, etc. */
uint32_t size; /**< File size in bytes. */
uint32_t inode; /**< Inode number (fs-specific). */
uint32_t mode; /**< Permissions/mode. */
/** Filesystem-specific operations. */
struct vfs_fs_ops *ops;
/** Opaque pointer for the filesystem driver. */
void *fs_data;
/** Mount index (which mount this node belongs to). */
int mount_idx;
} vfs_node_t;
/**
* Filesystem operations provided by each filesystem driver.
*/
typedef struct vfs_fs_ops {
/** Open a file. Returns 0 on success. */
int (*open)(vfs_node_t *node, uint32_t flags);
/** Close a file. */
void (*close)(vfs_node_t *node);
/** Read up to `size` bytes at `offset`. Returns bytes read, or -1. */
int32_t (*read)(vfs_node_t *node, uint32_t offset, uint32_t size, void *buf);
/** Write up to `size` bytes at `offset`. Returns bytes written, or -1. */
int32_t (*write)(vfs_node_t *node, uint32_t offset, uint32_t size, const void *buf);
/** Read directory entry at index `idx`. Returns 0 on success, -1 at end. */
int (*readdir)(vfs_node_t *dir, uint32_t idx, vfs_dirent_t *out);
/** Look up a child by name within a directory. Returns 0 on success. */
int (*finddir)(vfs_node_t *dir, const char *name, vfs_node_t *out);
} vfs_fs_ops_t;
/**
* Mount point entry.
*/
typedef struct vfs_mount {
char path[VFS_MAX_PATH]; /**< Mount path (e.g., "/initrd"). */
vfs_fs_ops_t *ops; /**< Filesystem operations. */
void *fs_data; /**< Filesystem-specific data. */
int active; /**< 1 if mounted, 0 if free. */
} vfs_mount_t;
/**
* Open file descriptor.
*/
typedef struct vfs_fd {
vfs_node_t node; /**< The file node. */
uint32_t offset; /**< Current read/write offset. */
uint32_t flags; /**< Open flags. */
int active; /**< 1 if in use, 0 if free. */
} vfs_fd_t;
/**
* Initialize the VFS subsystem.
*/
void init_vfs(void);
/**
* Mount a filesystem at the given path.
*
* @param path Mount point path (e.g., "/initrd").
* @param ops Filesystem operations.
* @param fs_data Filesystem-specific data pointer.
* @return 0 on success, -1 on failure.
*/
int vfs_mount(const char *path, vfs_fs_ops_t *ops, void *fs_data);
/**
* Open a file by path.
*
* @param path Absolute path to the file.
* @param flags Open flags (currently unused).
* @return File descriptor (>= 0), or -1 on failure.
*/
int vfs_open(const char *path, uint32_t flags);
/**
* Close an open file descriptor.
*
* @param fd File descriptor.
*/
void vfs_close(int fd);
/**
* Read from an open file.
*
* @param fd File descriptor.
* @param buf Buffer to read into.
* @param size Maximum bytes to read.
* @return Bytes read, or -1 on error.
*/
int32_t vfs_read(int fd, void *buf, uint32_t size);
/**
* Write to an open file.
*
* @param fd File descriptor.
* @param buf Buffer to write from.
* @param size Bytes to write.
* @return Bytes written, or -1 on error.
*/
int32_t vfs_write(int fd, const void *buf, uint32_t size);
/**
* Seek within an open file.
*
* @param fd File descriptor.
* @param offset Offset to seek to.
* @param whence VFS_SEEK_SET, VFS_SEEK_CUR, or VFS_SEEK_END.
* @return New offset, or -1 on error.
*/
int32_t vfs_seek(int fd, int32_t offset, int whence);
/**
* Read a directory entry.
*
* @param path Path to the directory.
* @param idx Entry index (0-based).
* @param out Output directory entry.
* @return 0 on success, -1 at end or on error.
*/
int vfs_readdir(const char *path, uint32_t idx, vfs_dirent_t *out);
/**
* Stat a file (get its node info).
*
* @param path Path to the file.
* @param out Output node.
* @return 0 on success, -1 on failure.
*/
int vfs_stat(const char *path, vfs_node_t *out);
#endif /* VFS_H */