feat: vfs - add dynamic filesystem mounts and safe unmount lifecycle

This commit is contained in:
2026-08-04 08:58:50 +02:00
parent b1c55073c7
commit ca5331c9db
12 changed files with 520 additions and 259 deletions
+150 -59
View File
@@ -10,6 +10,7 @@
#include <Terminal/Terminal.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <CppLib/Vector.hpp>
using namespace Kt;
@@ -19,7 +20,6 @@ namespace Fs::Fat32 {
// Constants
// =========================================================================
static constexpr int MaxInstances = 8;
static constexpr int MaxFilesPerInstance = 16;
static constexpr int MaxDirEntries = 128;
static constexpr int MaxNameLen = 256;
@@ -60,6 +60,10 @@ namespace Fs::Fat32 {
int blockDevIndex;
uint64_t partStartLba;
// The vtable handed to the VFS for this mount; owned by the instance
// and freed alongside it in DrvUnmount.
Vfs::FsDriver* driver;
// BPB fields
uint16_t bytesPerSector;
uint8_t sectorsPerCluster;
@@ -87,8 +91,11 @@ namespace Fs::Fat32 {
// Open file handles
Fat32File files[MaxFilesPerInstance];
// ReadDir name cache
char dirNames[MaxDirEntries][MaxNameLen];
// ReadDir name cache. 32 KiB, allocated on the first listing rather
// than at mount, so volumes that are never enumerated don't pay for
// it. Released on unmount.
char (*dirNames)[MaxNameLen];
int dirNamesPages;
int dirNameCount;
};
@@ -106,8 +113,28 @@ namespace Fs::Fat32 {
// Instance table
// =========================================================================
static Fat32Instance g_instances[MaxInstances] = {};
static int g_instanceCount = 0;
// One heap allocation per mounted volume. The table holds pointers rather
// than values so growing it never moves a live instance, and an index stays
// valid for the lifetime of its mount. Unmounting nulls the slot; Mount
// reuses the lowest free one, so hot-plug cycles don't grow the table.
static kcp::vector<Fat32Instance*> g_instanceSlots;
// Resolve an instance index, or nullptr if the slot is out of range, freed,
// or inactive. Every FsDriver entry point validates through this; internal
// helpers may then assume the instance is live, because the VFS holds
// vfsLock across the whole driver call and Unmount runs under that lock.
static Fat32Instance* InstanceAt(int inst) {
if (inst < 0 || (std::size_t)inst >= g_instanceSlots.size()) return nullptr;
Fat32Instance* self = g_instanceSlots[(std::size_t)inst];
return (self != nullptr && self->active) ? self : nullptr;
}
// Adapter keeping the `g_instances[inst]` spelling at the ~40 call sites
// that index the table by instance number.
struct InstanceTable {
Fat32Instance& operator[](int inst) { return *g_instanceSlots[(std::size_t)inst]; }
};
static InstanceTable g_instances;
// =========================================================================
// Low-level helpers
@@ -902,7 +929,7 @@ namespace Fs::Fat32 {
// =========================================================================
static int OpenImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
if (InstanceAt(inst) == nullptr) return -1;
ParsedEntry entry;
if (!TraversePath(inst, path, &entry)) return -1;
@@ -928,7 +955,7 @@ namespace Fs::Fat32 {
static int ReadImpl(int inst, int handle, uint8_t* buffer,
uint64_t offset, uint64_t size) {
if (inst < 0 || inst >= g_instanceCount) return -1;
if (InstanceAt(inst) == nullptr) return -1;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1;
@@ -988,28 +1015,44 @@ namespace Fs::Fat32 {
}
static uint64_t GetSizeImpl(int inst, int handle) {
if (inst < 0 || inst >= g_instanceCount) return 0;
if (InstanceAt(inst) == nullptr) return 0;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return 0;
return self.files[handle].fileSize;
}
static void CloseImpl(int inst, int handle) {
if (inst < 0 || inst >= g_instanceCount) return;
if (InstanceAt(inst) == nullptr) return;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance) return;
self.files[handle].inUse = false;
}
static constexpr int DirNamesPages =
(MaxDirEntries * MaxNameLen + 0xFFF) / 0x1000;
static bool EnsureDirNames(Fat32Instance& self) {
if (self.dirNames != nullptr) return true;
self.dirNames = (char(*)[MaxNameLen])Memory::g_pfa->ReallocConsecutive(
nullptr, DirNamesPages);
if (self.dirNames == nullptr) return false;
self.dirNamesPages = DirNamesPages;
return true;
}
static int ReadDirImpl(int inst, const char* path,
const char** outNames, int maxEntries, int startIndex = 0) {
if (inst < 0 || inst >= g_instanceCount) return -1;
if (InstanceAt(inst) == nullptr) return -1;
auto& self = g_instances[inst];
ParsedEntry dirEntry;
if (!TraversePath(inst, path, &dirEntry)) return -1;
if (!(dirEntry.attributes & ATTR_DIRECTORY)) return -1;
if (!EnsureDirNames(self)) return -1;
int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries;
int count = ReadDirectoryNames(inst, dirEntry.firstCluster, self.dirNames, limit, startIndex);
@@ -1023,7 +1066,7 @@ namespace Fs::Fat32 {
static int WriteImpl(int inst, int handle, const uint8_t* buffer,
uint64_t offset, uint64_t size) {
if (inst < 0 || inst >= g_instanceCount) return -1;
if (InstanceAt(inst) == nullptr) return -1;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1;
@@ -1126,7 +1169,7 @@ namespace Fs::Fat32 {
}
static int CreateImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
if (InstanceAt(inst) == nullptr) return -1;
auto& self = g_instances[inst];
// Split path into parent directory and filename
@@ -1274,7 +1317,7 @@ namespace Fs::Fat32 {
}
static int DeleteImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
if (InstanceAt(inst) == nullptr) return -1;
auto& self = g_instances[inst];
// Split path into parent directory and filename
@@ -1410,7 +1453,7 @@ namespace Fs::Fat32 {
// =========================================================================
static int MkdirImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
if (InstanceAt(inst) == nullptr) return -1;
auto& self = g_instances[inst];
// Split path into parent directory and new dir name
@@ -1540,7 +1583,7 @@ namespace Fs::Fat32 {
// =========================================================================
static int RenameImpl(int inst, const char* oldPath, const char* newPath) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
if (InstanceAt(inst) == nullptr) return -1;
auto& self = g_instances[inst];
// Split old path
@@ -1754,58 +1797,72 @@ namespace Fs::Fat32 {
// =========================================================================
static const char* GetLabelImpl(int inst) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return nullptr;
if (InstanceAt(inst) == nullptr) return nullptr;
const char* label = g_instances[inst].volumeLabel;
if (label[0] == '\0' || StrEqualNoCase(label, "NO NAME")) return nullptr;
return label;
}
template<int N> struct Thunks {
static int Open(const char* p) { return OpenImpl(N, p); }
static int Read(int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(N, h, b, o, s); }
static uint64_t GetSize(int h) { return GetSizeImpl(N, h); }
static void Close(int h) { CloseImpl(N, h); }
static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); }
static int ReadDirAt(const char* p, const char** o, int m, int s) { return ReadDirImpl(N, p, o, m, s); }
static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); }
static int Create(const char* p) { return CreateImpl(N, p); }
static int Delete(const char* p) { return DeleteImpl(N, p); }
static int Mkdir(const char* p) { return MkdirImpl(N, p); }
static int Rename(const char* o, const char* n) { return RenameImpl(N, o, n); }
static const char* GetLabel() { return GetLabelImpl(N); }
};
// ctx carries the instance index. Slots are never compacted, so the index
// stays valid until the mount is torn down.
static int CtxToInst(void* ctx) { return (int)(uintptr_t)ctx; }
static void* InstToCtx(int inst) { return (void*)(uintptr_t)inst; }
template<int N>
static Vfs::FsDriver MakeDriver() {
return {
Thunks<N>::Open,
Thunks<N>::Read,
Thunks<N>::GetSize,
Thunks<N>::Close,
Thunks<N>::ReadDir,
Thunks<N>::Write,
Thunks<N>::Create,
Thunks<N>::Delete,
Thunks<N>::Mkdir,
Thunks<N>::Rename,
Thunks<N>::GetLabel,
Thunks<N>::ReadDirAt,
};
static int DrvOpen(void* c, const char* p) { return OpenImpl(CtxToInst(c), p); }
static int DrvRead(void* c, int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(CtxToInst(c), h, b, o, s); }
static uint64_t DrvGetSize(void* c, int h) { return GetSizeImpl(CtxToInst(c), h); }
static void DrvClose(void* c, int h) { CloseImpl(CtxToInst(c), h); }
static int DrvReadDir(void* c, const char* p, const char** o, int m) { return ReadDirImpl(CtxToInst(c), p, o, m); }
static int DrvReadDirAt(void* c, const char* p, const char** o, int m, int s) { return ReadDirImpl(CtxToInst(c), p, o, m, s); }
static int DrvWrite(void* c, int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(CtxToInst(c), h, b, o, s); }
static int DrvCreate(void* c, const char* p) { return CreateImpl(CtxToInst(c), p); }
static int DrvDelete(void* c, const char* p) { return DeleteImpl(CtxToInst(c), p); }
static int DrvMkdir(void* c, const char* p) { return MkdirImpl(CtxToInst(c), p); }
static int DrvRename(void* c, const char* o, const char* n) { return RenameImpl(CtxToInst(c), o, n); }
static const char* DrvGetLabel(void* c) { return GetLabelImpl(CtxToInst(c)); }
// Release everything the mount owns. The driver is either not registered
// yet, or the VFS has deactivated its drive and drained dispatches.
static void DrvUnmount(void* c) {
int inst = CtxToInst(c);
Fat32Instance* self = InstanceAt(inst);
if (self == nullptr) return;
if (self->clusterBuf != nullptr && self->clusterBufPages > 0) {
Memory::g_pfa->Free(self->clusterBuf, self->clusterBufPages);
}
if (self->fatCache != nullptr && self->fatCachePages > 0) {
Memory::g_pfa->Free(self->fatCache, self->fatCachePages);
}
if (self->dirNames != nullptr && self->dirNamesPages > 0) {
Memory::g_pfa->Free(self->dirNames, self->dirNamesPages);
}
self->active = false;
g_instanceSlots[(std::size_t)inst] = nullptr;
Vfs::FsDriver* driver = self->driver;
Memory::g_heap->Free(self);
if (driver != nullptr) Memory::g_heap->Free(driver);
}
static Vfs::FsDriver g_drivers[] = {
MakeDriver<0>(), MakeDriver<1>(), MakeDriver<2>(), MakeDriver<3>(),
MakeDriver<4>(), MakeDriver<5>(), MakeDriver<6>(), MakeDriver<7>(),
};
// =========================================================================
// BPB validation and mount
// =========================================================================
Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) {
if (g_instanceCount >= MaxInstances) return nullptr;
// Claim the lowest free instance slot, extending the table if all are in
// use. Returns -1 only if the heap is exhausted.
static int AllocateInstanceSlot() {
for (std::size_t i = 0; i < g_instanceSlots.size(); i++) {
if (g_instanceSlots[i] == nullptr) return (int)i;
}
g_instanceSlots.push_back(nullptr);
return (int)g_instanceSlots.size() - 1;
}
Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) {
auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex);
if (!dev) return nullptr;
@@ -1873,9 +1930,23 @@ namespace Fs::Fat32 {
// At least one of: valid cluster count or FS type string
if (!hasFat32Str && clusterCount < 65525) return nullptr;
// Success — initialize instance
int idx = g_instanceCount;
auto& inst = g_instances[idx];
// Success — allocate and initialize the instance
int idx = AllocateInstanceSlot();
if (idx < 0) return nullptr;
auto* instPtr = (Fat32Instance*)Memory::g_heap->Request(sizeof(Fat32Instance));
if (instPtr == nullptr) return nullptr;
memset(instPtr, 0, sizeof(Fat32Instance));
auto* driver = (Vfs::FsDriver*)Memory::g_heap->Request(sizeof(Vfs::FsDriver));
if (driver == nullptr) {
Memory::g_heap->Free(instPtr);
return nullptr;
}
g_instanceSlots[(std::size_t)idx] = instPtr;
auto& inst = *instPtr;
inst.driver = driver;
inst.active = true;
inst.blockDevIndex = blockDevIndex;
@@ -1924,8 +1995,12 @@ namespace Fs::Fat32 {
uint32_t chunk = (remaining > 4096) ? 4096 : (uint32_t)remaining;
uint32_t secs = (chunk + bytesPerSector - 1) / bytesPerSector;
if (!ReadPartSectors(inst, fatPartSector, secs, dst)) {
// If read fails, disable cache and fall back to per-lookup reads
// If read fails, disable cache and fall back to per-lookup
// reads. Release the pages rather than orphaning them.
Memory::g_pfa->Free(inst.fatCache, inst.fatCachePages);
inst.fatCache = nullptr;
inst.fatCachePages = 0;
inst.fatCacheEntries = 0;
break;
}
dst += secs * bytesPerSector;
@@ -1939,13 +2014,29 @@ namespace Fs::Fat32 {
inst.files[i].inUse = false;
}
g_instanceCount++;
*driver = Vfs::FsDriver{
.ctx = InstToCtx(idx),
.Open = DrvOpen,
.Read = DrvRead,
.GetSize = DrvGetSize,
.Close = DrvClose,
.ReadDir = DrvReadDir,
.Write = DrvWrite,
.Create = DrvCreate,
.Delete = DrvDelete,
.Mkdir = DrvMkdir,
.Rename = DrvRename,
.GetLabel = DrvGetLabel,
.ReadDirAt = DrvReadDirAt,
.Stat = nullptr,
.Unmount = DrvUnmount,
};
KernelLogStream(OK, "FAT32") << "Mounted volume \""
<< inst.volumeLabel << "\" (" << clusterCount << " clusters, "
<< (uint64_t)inst.clusterSize << " bytes/cluster)";
return &g_drivers[idx];
return driver;
}
void RegisterProbe() {