A cross-platform in-memory disk with a small ext2-like filesystem that can build as a shared library (.so/.dll). It exposes a C API with both block-level access and file-level operations, optionally persisting to a backing file.
Recent improvements: All critical bugs fixed including directory indirect block support, infinite loop prevention, race condition fixes, and proper link count management. All tests passing.
cmake -S . -B build -DRD_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure
Options (CMake cache):
RD_BUILD_SHARED(ON): build shared library.RD_BUILD_STATIC(OFF): also build static lib.RD_ENABLE_JOURNAL(OFF): reserved; no-op.RD_BUILD_TESTS(ON): build test executable.RD_BUILD_FUSE(OFF): build FUSE filesystem adapter.
Outputs:
- Library target:
ramdisc(aliasramdisc::ramdisc). - Header:
include/ramdisc.h. - Tests:
build/ramdisc_tests. - FUSE adapter:
build/ramdisc_fuse(ifRD_BUILD_FUSE=ON).
Include the header and link against the library:
#include "ramdisc.h"
int main() {
rd_device_t dev = rd_create(1u << 20, 4096, NULL, 0);
rd_mount(dev);
rd_fd fd = rd_open(dev, "/hello", RD_O_CREATE | RD_O_RDWR | RD_O_TRUNC, 0644);
const char msg[] = "hello";
rd_write(dev, fd, msg, sizeof msg);
rd_seek(dev, fd, 0, 0);
char buf[16];
rd_read(dev, fd, buf, sizeof msg);
rd_close(dev, fd);
rd_unlink(dev, "/hello");
rd_unmount(dev);
rd_destroy(dev);
return 0;
}rd_create(size_bytes, block_size, backing_path, flags): allocate a device.block_sizemust be a power of two.backing_pathoptional; when set, the device preloads from the file (unless truncated) and flushes to it. Flags:RD_BACKING_TRUNC: truncate/create the backing file tosize_bytes.RD_BACKING_CREATE: (reserved) same as TRUNC currently.RD_BACKING_SPILL: reserved, no-op.RD_JOURNAL_ENABLE: reserved, no-op.
rd_mount(dev): format if the superblock is absent; otherwise validate and mount.rd_unmount(dev): flush to backing and detach.rd_destroy(dev): free memory, close backing.
rd_block_read(dev, block_idx, buf, block_count)/rd_block_write(...): block-aligned I/O within bounds; returns blocks transferred or negative error.rd_block_flush(dev): flush memory to backing file (if any).
rd_open(dev, path, flags, mode):RD_O_CREATE,RD_O_TRUNC,RD_O_EXCL,RD_O_APPEND,RD_O_RDONLY/WRONLY/RDWRsupported.rd_read,rd_write,rd_pread,rd_pwrite,rd_seek,rd_close.rd_stat,rd_fstat(structrd_stat_info),rd_unlink,rd_mkdir,rd_rmdir,rd_readdir(callback receives names and stats).
- Names:
RD_MAX_NAME(64 bytes). Paths are POSIX-style (/separated); no relative paths. - Inode blocks: 8 direct + 1 single-indirect + 1 double-indirect. Max file size with 4KB blocks: ~4GB.
- Handles: 64 initial capacity, grows dynamically up to 1024 open file descriptors per device.
- Block size must divide device size.
- Directories support indirect blocks for large directories (tested with 100+ entries).
- Superblock + block bitmap + inode table + data blocks laid out in RAM (and mirrored to backing when present).
- Allocation: free list-based block allocation with bitmap tracking; inodes track direct, single-indirect, and double-indirect blocks.
- Directories store variable-length entries similar to ext2;
rd_readdirwalks entries and supplies a callback. - Reads of sparse holes return zeroed data.
- On
rd_unmountorrd_block_flush, dirty blocks are written to the backing file if configured. - Robust error handling prevents infinite loops, race conditions, and data corruption.
Functions return RD_OK (0) or negative rd_err codes:
RD_ERR_IO(-1): I/O error (backing file, corrupted internal structures)RD_ERR_NOENT(-2): File/directory not foundRD_ERR_EXIST(-3): File/directory already exists (with O_EXCL)RD_ERR_NOSPC(-4): No space left (blocks or inodes exhausted)RD_ERR_INVAL(-5): Invalid argument (null pointer, bad path, invalid whence)RD_ERR_PERM(-6): Operation not permitted (unlink directory, write to directory, rmdir non-empty)RD_ERR_RANGE(-7): Out of range (block index, file size beyond indirect limit)RD_ERR_NOMEM(-8): Memory allocation failedRD_ERR_NOSYS(-9): Not implemented (reserved for future features)
- rd_create: Returns NULL and sets
errnoon failure (EINVAL, ENOMEM) - rd_open: Returns RD_ERR_NOENT (not found), RD_ERR_EXIST (O_EXCL), RD_ERR_NOSPC (no handles/inodes), RD_ERR_PERM (write to directory)
- rd_read/write: Returns RD_ERR_INVAL (bad fd/type), RD_ERR_NOSPC (allocation failure), bytes transferred on success
- rd_seek: Returns RD_ERR_INVAL (bad fd/whence, negative result); whence: 0=SEEK_SET, 1=SEEK_CUR, 2=SEEK_END
- rd_stat: Returns RD_ERR_NOENT (not found), RD_ERR_INVAL (bad args)
- rd_mkdir: Returns RD_ERR_EXIST (already exists), RD_ERR_NOSPC (no inodes/blocks)
- rd_rmdir: Returns RD_ERR_PERM (non-empty or not directory), RD_ERR_NOENT (not found)
- rd_unlink: Returns RD_ERR_PERM (is directory or root), RD_ERR_NOENT (not found)
- File descriptors are integers starting from 0
- Initial capacity: 64 handles, grows dynamically up to 1024 as needed
- Each
rd_openallocates a new handle; same file can be opened multiple times - Each handle maintains independent position offset
- Closing an fd makes it available for reuse
- Using a closed fd returns RD_ERR_INVAL
- Destroying device invalidates all fds (no automatic cleanup)
- Reads beyond EOF return 0 (not an error); partial reads to EOF return actual bytes
- Writes extend file automatically; may fail with RD_ERR_NOSPC or RD_ERR_RANGE (beyond indirect limit)
- Sparse files: Unallocated blocks read as zeros; blocks allocated on write
- O_APPEND: Seeks to end before each write (position set at open and per write)
- O_TRUNC: Immediately frees all direct, indirect, and double-indirect blocks; resets size to 0; updates mtime/ctime
- pread/pwrite: Do not modify file position
- Concurrent access: Multiple handles to same file see each other's writes immediately (no buffering)
- Directories initially contain "." (self) and ".." (parent) with link count 2
- Root directory's ".." points to itself
rd_readdircalls callback for each entry including "." and ".."; callback can return non-zero to stop iteration- Empty directory check ignores "." and "..";
rd_rmdirfails on non-empty with RD_ERR_PERM - Directory link count = 2 + number of subdirectories
rd_createallocates aligned memory for entire device; fails if allocation failsrd_mountformats if no valid superblock; validates existing superblockrd_unmountflushes to backing file (if present); does not free memoryrd_destroyfrees all memory and closes backing file; does not flush (call rd_unmount first)- Thread safety: Thread-safe using pthread read-write locks and mutexes
- Filesystem operations (reads, writes, metadata) protected by
pthread_rwlock_t - Multiple concurrent readers allowed; writes are exclusive
- File handle table protected by
pthread_mutex_t - Expected overhead: 2-10% on concurrent workloads depending on contention
- Filesystem operations (reads, writes, metadata) protected by
- Max open file descriptors: 64 initially, grows dynamically up to 1024 per device
- Max filename length: 63 bytes + null terminator (RD_MAX_NAME = 64)
- Max path length: 255 bytes (temp buffer in rd_lookup)
- Max file size with 4KB blocks: ~4GB (8 direct + 1024 single-indirect + 1024×1024 double-indirect blocks)
- Direct: 8 × 4KB = 32KB
- Single indirect: 1024 × 4KB = 4MB
- Double indirect: 1024 × 1024 × 4KB = 4GB
- Max directory entries per block: varies by name length
- Block size: Must be power of 2
- Device size: Must be multiple of block_size
- Block allocation: O(1) using free list (was O(n))
- Inode allocation: O(1) using free list (was O(n))
- Path lookup: O(d × m) where d = path depth, m = entries per directory (linear search)
- Directory add/remove: O(m) where m = entries in directory
- File read/write: O(k) where k = blocks accessed; block lookup is O(1) direct, O(1) indirect
- rd_readdir: O(m) where m = entries in directory
- rd_rename: O(1) for file, O(depth) for directory cycle detection
- Superblock: 1 block (stores metadata)
- Block bitmap: ⌈block_count / 8 / block_size⌉ blocks
- Inode table: ⌈inode_count × 64 / block_size⌉ blocks (64 bytes per inode)
- Directory entries: ~16-80 bytes per entry (depends on name length, 4-byte aligned)
- Example: 1MB device, 4KB blocks → 256 blocks, 64 inodes, ~5% overhead (superblock + 1 bitmap + 1 inode block)
- Entire device held in RAM (size_bytes allocated with rd_create)
- No page cache or buffer cache (direct memory access)
- Backing file (if used) uses incremental writes via dirty block tracking
- Only modified blocks flushed on rd_block_flush/rd_unmount
- Dirty bitmap overhead: ⌈block_count / 8⌉ bytes
- Example: 1GB device with 4KB blocks = 256K blocks = 32KB dirty bitmap
- No permissions enforcement beyond simple mode checks; no users/groups.
- No journaling or crash recovery; backing flush is incremental (dirty blocks only) but not atomic.
- No hard links or symlinks.
- Single-device, in-process only; not mounted into the OS VFS.
- Handle table grows dynamically but caps at 1024 open files per device.
- O(1) allocation: Free list-based block and inode allocation for constant-time performance
- Previous O(n) linear search replaced with linked free lists
- Dramatically faster allocation on large devices
- Rename operation: Full rd_rename() implementation with cycle detection
- Supports file and directory renames
- Handles overwrites with proper checks
- Detects and prevents directory cycles
- Per-file fsync: rd_fsync() flushes specific file's blocks to backing storage
- More efficient than full rd_block_flush() for single file updates
- Flushes file data, metadata, and directory entries
- Thread safety: Full thread-safe implementation using pthread read-write locks
- Multiple concurrent readers for maximum performance
- Exclusive write locking for modifications
- Separate handle table mutex for minimal contention
- Double indirect blocks: Files can now grow up to ~4GB (with 4KB blocks) instead of ~4MB
- Incremental backing flush: Only dirty blocks written to backing file, dramatically faster for large devices
- Dynamic file handles: Handle table grows from 64 to 1024 as needed
- Dirty tracking: Backing file writes are now O(d) where d = dirty blocks
- Build and run:
ctest --test-dir build --output-on-failure. - Coverage includes: mount/format, create/read/write/seek, directory listing, unlink/rmdir rules, indirect-block I/O, ENOSPC behavior, block API sanity, backing persistence across remounts, rename operations, per-file fsync, thread safety.
- 18 tests total, all passing.
The FUSE adapter allows you to mount your ramdisc as a real directory in your filesystem, making it accessible to any application using standard POSIX file operations.
First, install FUSE development libraries:
# Debian/Ubuntu
sudo apt-get install libfuse-dev
# Fedora/RHEL
sudo dnf install fuse-devel
# macOS (using FUSE for macOS)
brew install macfuseBuild with FUSE support:
cmake -S . -B build -DRD_BUILD_FUSE=ON
cmake --build buildMount a 64 MB ramdisc (runs in background):
mkdir /tmp/myram
./build/ramdisc_fuse /tmp/myramUse it like any directory:
echo "Hello from FUSE!" > /tmp/myram/test.txt
cat /tmp/myram/test.txt
mkdir /tmp/myram/subdir
cp /etc/hosts /tmp/myram/
ls -la /tmp/myram/Unmount when done:
fusermount -u /tmp/myramMount with custom size (256 MB):
./build/ramdisc_fuse -o size=256 /tmp/myramMount with backing file for persistence:
# First mount - creates and uses backing file
./build/ramdisc_fuse -o backing=/tmp/ramdisc.img /tmp/myram
echo "persistent data" > /tmp/myram/file.txt
fusermount -u /tmp/myram
# Later mount - data persists!
./build/ramdisc_fuse -o backing=/tmp/ramdisc.img /tmp/myram
cat /tmp/myram/file.txt # Shows "persistent data"Run in foreground (useful for debugging):
./build/ramdisc_fuse -f /tmp/myram
# Press Ctrl+C to unmountEnable debug output:
./build/ramdisc_fuse -d -f /tmp/myramUse as fast tmpfs replacement:
./build/ramdisc_fuse -o size=512 /tmp/fast
cd /tmp/fast
# Compile projects, extract archives, etc.Run SQLite database in RAM:
./build/ramdisc_fuse -o size=128 /tmp/db
sqlite3 /tmp/db/test.db "CREATE TABLE users (id INT, name TEXT);"
sqlite3 /tmp/db/test.db "INSERT INTO users VALUES (1, 'Alice');"Development workspace with persistence:
./build/ramdisc_fuse -o size=1024,backing=$HOME/.ramdisc.img /tmp/workspace
# Your files persist across reboots via backing fileCheck filesystem status:
df -h /tmp/myram # Show capacity and usage
mount | grep ramdisc_fuse # Verify mount
tree /tmp/myram # View directory structure-o size=MB- Set ramdisc size in megabytes (default: 64)-o backing=PATH- Use a backing file for persistence-f- Run in foreground (blocks terminal, Ctrl+C to unmount)-d- Enable FUSE debug output (implies-f)-s- Single-threaded mode (useful for debugging)-o allow_other- Allow other users to access (requires user_allow_other in /etc/fuse.conf)
All standard POSIX file operations work:
- Files: create, read, write, truncate, delete
- Directories: create, list, delete (when empty)
- Operations: rename, stat, fsync
- Access modes: read-only, write-only, read-write
- Seek modes: SEEK_SET, SEEK_CUR, SEEK_END
Example workflow:
# Create and edit files with any tool
vim /tmp/myram/notes.txt
echo "data" > /tmp/myram/file.txt
# Standard utilities work
cp -r /etc/ssl/certs /tmp/myram/
tar xzf archive.tar.gz -C /tmp/myram/
find /tmp/myram -name "*.txt"
# Applications see it as a normal filesystem
sqlite3 /tmp/myram/database.db
gcc -o /tmp/myram/program source.cMount fails with "mountpoint is not empty":
# Clean the directory first
rm -rf /tmp/myram/*
# Or use the nonempty option (not recommended)
./build/ramdisc_fuse -o nonempty /tmp/myramCheck if mounted:
mount | grep ramdisc_fuse
df -h /tmp/myramUnmount stuck filesystem:
# Lazy unmount
fusermount -uz /tmp/myram
# Force unmount (if lazy doesn't work)
sudo umount -l /tmp/myramPermission denied errors:
# Ensure you have permission to the mount point
ls -ld /tmp/myram
# For multi-user access, add allow_other option
./build/ramdisc_fuse -o allow_other /tmp/myram- Zero code changes: Existing applications work without modification
- Standard tools: Use
ls,cp,mv,cat, etc. - Editor support: Any text editor can open files directly
- Database support: Run SQLite or other databases on ramdisc
- Mount anywhere: Integrate seamlessly into your filesystem hierarchy
- Fast tmpfs alternative: With ext2-like structure and optional persistence
- Same as C API: max file size ~4GB (with 4KB blocks and double indirect)
- 64-character filename limit
- No symbolic links or hard links yet
- No extended attributes
- FUSE adds some overhead vs. direct C API usage