Project 2: In-Memory Filesystem with FUSE
Build a complete, mountable filesystem that stores files in RAM using FUSE (Filesystem in Userspace)
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Intermediate (Level 3) |
| Time Estimate | 2-3 weeks |
| Language | C (Alternatives: C++, Rust with fuse-rs) |
| Prerequisites | C pointers/structs, Project 1 or understanding of filesystem structures |
| Key Topics | VFS operations, FUSE API, inode management, directory traversal |
| Main Book | “The Linux Programming Interface” by Michael Kerrisk |
1. Learning Objectives
By completing this project, you will:
- Understand the Virtual Filesystem (VFS) abstraction and how Linux presents a unified interface to different filesystems
- Implement core file operations (getattr, open, read, write, create, unlink) that handle real system calls
- Design in-memory data structures for inodes, directory entries, and file data storage
- Work with the FUSE API to create mountable filesystems without writing kernel code
- Handle file metadata correctly including permissions, timestamps, link counts, and ownership
- Debug userspace filesystems using FUSE’s debugging features and strace
- Understand path resolution and how the kernel translates paths to filesystem operations
2. Theoretical Foundation
2.1 Core Concepts
The Virtual Filesystem (VFS) Layer
Linux supports dozens of filesystems (ext4, XFS, Btrfs, NFS, FUSE, etc.), yet applications use identical system calls for all of them. The VFS layer makes this possible by defining a common interface that all filesystems must implement.
User Application
│
│ open("/mnt/myfs/file.txt", O_RDONLY)
▼
┌─────────────────────────────────────────────────────────────┐
│ System Call Layer │
│ (sys_open, sys_read, sys_write) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ VFS (Virtual Filesystem) │
│ Provides unified interface for all filesystems │
│ │
│ Key abstractions: │
│ - superblock: filesystem-wide metadata │
│ - inode: per-file metadata (not the name!) │
│ - dentry: directory entry cache (name → inode) │
│ - file: open file instance (position, flags) │
└─────────────────────────────────────────────────────────────┘
│
│ Dispatches to appropriate filesystem driver
▼
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ ext4 │ XFS │ NFS │ FUSE │ tmpfs │
│ driver │ driver │ client │ bridge │ driver │
└──────────┴──────────┴──────────┴────┬─────┴──────────┘
│
▼
┌──────────────────────┐
│ Your Userspace │
│ Filesystem (memfs) │
└──────────────────────┘
VFS Key Abstractions:
| Abstraction | Purpose | Your Implementation |
|---|---|---|
| superblock | Filesystem-wide metadata | Global state struct with inode table |
| inode | Per-file metadata (NOT the filename) | struct my_inode with mode, size, data pointer |
| dentry | Name-to-inode mapping | Directory entries stored in directory inodes |
| file | Open file instance | Handled by FUSE; you track via path |
FUSE Architecture
FUSE (Filesystem in Userspace) provides a bridge between the kernel VFS and your regular C program:
┌─────────────────────────────────────────────────────────────┐
│ User Application │
│ (ls, cat, echo, mkdir, rm, etc.) │
└──────────────────────────┬──────────────────────────────────┘
│ System calls (open, read, write)
▼
┌─────────────────────────────────────────────────────────────┐
│ Linux Kernel VFS │
└──────────────────────────┬──────────────────────────────────┘
│ Routes to FUSE kernel module
▼
┌─────────────────────────────────────────────────────────────┐
│ FUSE Kernel Module │
│ (fuse.ko) │
│ │
│ Translates VFS operations to /dev/fuse protocol │
└──────────────────────────┬──────────────────────────────────┘
│ Reads/writes to /dev/fuse
▼
┌─────────────────────────────────────────────────────────────┐
│ /dev/fuse │
│ (character device for IPC) │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ libfuse Library │
│ (userspace) │
│ │
│ - Reads requests from /dev/fuse │
│ - Calls YOUR callback functions │
│ - Writes responses back to /dev/fuse │
└──────────────────────────┬──────────────────────────────────┘
│ Your callbacks
▼
┌─────────────────────────────────────────────────────────────┐
│ YOUR FILESYSTEM CODE │
│ │
│ myfs_getattr() - Return file metadata │
│ myfs_readdir() - List directory contents │
│ myfs_open() - Open a file │
│ myfs_read() - Read file data │
│ myfs_write() - Write file data │
│ myfs_create() - Create new file │
│ myfs_mkdir() - Create directory │
│ myfs_unlink() - Delete file │
│ myfs_rmdir() - Remove directory │
└─────────────────────────────────────────────────────────────┘
How a read() syscall flows through FUSE:
- Application calls
read(fd, buf, 100) - Kernel VFS dispatches to FUSE kernel module
- FUSE module writes request to
/dev/fuse - libfuse reads request from
/dev/fuse - libfuse calls your
myfs_read()callback - Your code returns data
- libfuse writes response to
/dev/fuse - FUSE module returns data to VFS
- Application receives data
The fuse_operations Structure
FUSE filesystems implement callbacks in a struct fuse_operations:
struct fuse_operations {
// Metadata operations
int (*getattr)(const char *path, struct stat *stbuf,
struct fuse_file_info *fi);
int (*chmod)(const char *path, mode_t mode,
struct fuse_file_info *fi);
int (*chown)(const char *path, uid_t uid, gid_t gid,
struct fuse_file_info *fi);
int (*utimens)(const char *path, const struct timespec tv[2],
struct fuse_file_info *fi);
// Directory operations
int (*readdir)(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi,
enum fuse_readdir_flags flags);
int (*mkdir)(const char *path, mode_t mode);
int (*rmdir)(const char *path);
// File operations
int (*create)(const char *path, mode_t mode,
struct fuse_file_info *fi);
int (*open)(const char *path, struct fuse_file_info *fi);
int (*read)(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi);
int (*write)(const char *path, const char *buf, size_t size,
off_t offset, struct fuse_file_info *fi);
int (*truncate)(const char *path, off_t size,
struct fuse_file_info *fi);
int (*unlink)(const char *path);
int (*rename)(const char *from, const char *to, unsigned int flags);
// ... many more optional operations
};
Critical operations you MUST implement:
| Operation | When Called | What You Return |
|---|---|---|
getattr |
EVERY file access (ls, stat, open) | Fill struct stat with metadata |
readdir |
ls, find, directory iteration |
Call filler() for each entry |
create |
Creating new files | Allocate inode, add to parent dir |
open |
Opening existing files | Verify file exists |
read |
Reading file content | Copy data to buffer, return bytes read |
write |
Writing file content | Store data, return bytes written |
unlink |
Deleting files (rm) |
Remove dir entry, decrement nlink |
mkdir |
Creating directories | Allocate inode, add . and .. |
rmdir |
Removing directories | Check empty, remove |
The stat Structure
Every filesystem must provide file metadata via struct stat:
struct stat {
dev_t st_dev; // Device ID (your FS ID)
ino_t st_ino; // Inode number (unique per file)
mode_t st_mode; // File type + permissions
nlink_t st_nlink; // Number of hard links
uid_t st_uid; // Owner user ID
gid_t st_gid; // Owner group ID
off_t st_size; // Total size in bytes
blksize_t st_blksize; // Block size for I/O (use 4096)
blkcnt_t st_blocks; // Number of 512B blocks allocated
struct timespec st_atim; // Last access time
struct timespec st_mtim; // Last modification time
struct timespec st_ctim; // Last status change time
};
st_mode encoding (CRITICAL to get right):
// File type bits (upper 4 bits of mode) - MUTUALLY EXCLUSIVE
S_IFREG 0100000 // Regular file
S_IFDIR 0040000 // Directory
S_IFLNK 0120000 // Symbolic link
S_IFCHR 0020000 // Character device
S_IFBLK 0060000 // Block device
S_IFIFO 0010000 // FIFO (named pipe)
S_IFSOCK 0140000 // Socket
// Permission bits (lower 9 bits)
S_IRUSR 00400 // Owner read
S_IWUSR 00200 // Owner write
S_IXUSR 00100 // Owner execute
S_IRGRP 00040 // Group read
S_IWGRP 00020 // Group write
S_IXGRP 00010 // Group execute
S_IROTH 00004 // Others read
S_IWOTH 00002 // Others write
S_IXOTH 00001 // Others execute
// Example: Regular file with rw-r--r-- (0644)
mode_t file_mode = S_IFREG | 0644;
// Example: Directory with rwxr-xr-x (0755)
mode_t dir_mode = S_IFDIR | 0755;
// COMMON BUG: Forgetting file type bits!
// WRONG: stbuf->st_mode = 0755; // ls shows "??????"
// RIGHT: stbuf->st_mode = S_IFDIR | 0755;
Hard Links and nlink Count
Every file has a link count (st_nlink) that tracks how many directory entries point to it:
File link count rules:
┌──────────────────────────────────────────────────────────┐
│ Regular file: │
│ - Starts with nlink=1 │
│ - Each hard link adds 1 │
│ - File deleted when nlink reaches 0 (and no open fds) │
│ │
│ Directory: │
│ - Starts with nlink=2 (parent's entry + own ".") │
│ - Each subdirectory adds 1 (for its ".." entry) │
│ │
│ Example: │
│ │
│ /mydir nlink=4 │
│ ├── . (points to /mydir, counted in 2) │
│ ├── .. (points to parent) │
│ ├── file.txt nlink=1 │
│ ├── subdir1/ nlink=2 (adds 1 to mydir) │
│ │ ├── . │
│ │ └── .. (points to /mydir) │
│ └── subdir2/ nlink=2 (adds 1 to mydir) │
│ ├── . │
│ └── .. (points to /mydir) │
└──────────────────────────────────────────────────────────┘
Why nlink matters:
rmdecrements nlink; actual deletion happens when nlink=0ls -luses nlink to show link count- Directories with wrong nlink confuse
findand other tools
2.2 Why This Matters
Understanding VFS unlocks:
- Debugging file issues: When
lsshows strange output or permissions seem wrong, you’ll know which operations are failing - Building custom storage: Network filesystems, encrypted filesystems, database-backed filesystems all use FUSE
- Interview preparation: “How does a filesystem work?” is a common systems interview question
- Operating system internals: VFS is fundamental to understanding how Unix works
Real-world FUSE filesystems:
- s3fs: Mount Amazon S3 buckets as local directories
- sshfs: Mount remote servers via SSH
- encfs: Encrypted overlay filesystem
- rclone mount: Multi-cloud filesystem (Google Drive, Dropbox, etc.)
- gcsfuse: Google Cloud Storage filesystem
2.3 Historical Context
The Problem Before FUSE (pre-2005):
- Filesystem = kernel code = crashes bring down entire system
- Requires kernel recompilation or module loading
- Debugging with printk, no gdb
- One bug = kernel panic = reboot
FUSE Solved This:
- 2001: Miklos Szeredi begins development
- 2005: Merged into Linux kernel 2.6.14
- Enabled explosion of userspace filesystems
- Made filesystem development accessible to application developers
The Trade-off:
- Performance: Each operation requires kernel ↔ userspace context switch
- Typical overhead: 10-30% compared to kernel filesystems
- For many use cases (cloud storage, archives, debugging), this is acceptable
2.4 Common Misconceptions
Misconception 1: “getattr is only called for stat“
- Reality:
getattris called for EVERY file access lscalls getattr on every file in the directoryopencalls getattr to check if file exists- Keep getattr FAST
Misconception 2: “I can store filenames in inodes”
- Reality: Filenames belong in directory entries, not inodes
- An inode can have multiple names (hard links)
- This is why you can rename a file without changing its inode
Misconception 3: “read() and write() handle files”
- Reality: They handle paths! FUSE gives you the path, not a file descriptor
- You must resolve the path to an inode in every callback
Misconception 4: “Directories are just special files”
- Reality: Yes, but they need special handling:
- Must contain
.(self) and..(parent) - nlink counts subdirectories
- Cannot be opened with O_RDONLY for reading bytes
- Must contain
3. Project Specification
3.1 What You Will Build
A complete in-memory filesystem called memfs that:
- Mounts as a real directory on Linux
- Supports files and directories
- Handles all basic file operations
- Stores everything in RAM (data lost on unmount)
- Works with standard Unix tools (ls, cat, mkdir, rm, etc.)
3.2 Functional Requirements
- Mount and Unmount
- Mount filesystem to any empty directory
- Unmount cleanly with
fusermount -u - Support foreground mode (
-f) and debug mode (-d)
- File Operations
- Create files (
touch,echo > file) - Read files (
cat,head,tail) - Write files (
echo >>, text editors) - Delete files (
rm) - Truncate files (
truncate -s,>)
- Create files (
- Directory Operations
- Create directories (
mkdir) - List directories (
ls,ls -la) - Remove empty directories (
rmdir) - Navigate directories (
cd)
- Create directories (
- Metadata Operations
- Get file attributes (
stat,ls -l) - Update timestamps (
touch) - Change permissions (
chmod) - Change ownership (
chown) (as root)
- Get file attributes (
- Path Operations
- Resolve absolute paths
- Handle
.and..correctly - Support nested paths (
/a/b/c/file.txt)
3.3 Non-Functional Requirements
- Capacity: Support files up to 100MB
- File count: Support up to 1000 files/directories
- Concurrency: Handle sequential access (single-threaded is fine)
- Errors: Return appropriate errno codes (ENOENT, EACCES, EISDIR, etc.)
- Performance: Operations complete in reasonable time (< 100ms)
3.4 Example Usage / Output
# Terminal 1: Run the filesystem
$ mkdir /tmp/myfs
$ ./memfs /tmp/myfs
# (Filesystem is now mounted and running)
# Terminal 2: Use the filesystem
$ cd /tmp/myfs
$ pwd
/tmp/myfs
$ ls -la
total 0
drwxr-xr-x 2 user user 0 Dec 20 10:00 .
drwxr-xr-x 3 user user 4096 Dec 20 10:00 ..
$ echo "Hello, FUSE!" > greeting.txt
$ cat greeting.txt
Hello, FUSE!
$ ls -la
total 0
drwxr-xr-x 2 user user 0 Dec 20 10:00 .
drwxr-xr-x 3 user user 4096 Dec 20 10:00 ..
-rw-r--r-- 1 user user 13 Dec 20 10:01 greeting.txt
$ mkdir subdir
$ echo "Nested content" > subdir/nested.txt
$ ls -R
.:
greeting.txt subdir
./subdir:
nested.txt
$ cat subdir/nested.txt
Nested content
$ stat greeting.txt
File: greeting.txt
Size: 13 Blocks: 0 IO Block: 4096 regular file
Device: 0,0 Inode: 2 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ user) Gid: ( 1000/ user)
Access: 2024-12-20 10:01:00.000000000 +0000
Modify: 2024-12-20 10:01:00.000000000 +0000
Change: 2024-12-20 10:01:00.000000000 +0000
$ rm greeting.txt
$ ls
subdir
$ rmdir subdir
rmdir: failed to remove 'subdir': Directory not empty
$ rm subdir/nested.txt
$ rmdir subdir
$ ls
(empty)
# Terminal 1: Unmount
$ fusermount -u /tmp/myfs
# (All data is gone - it was in RAM)
3.5 Real World Outcome
When you complete this project:
- You have a mountable filesystem - You can
mountit just like ext4 or NTFS - Standard tools just work -
ls,cat,vim,cpall work transparently - You understand VFS - You can explain what happens for any file operation
- Debug any filesystem issue - You know where to look when file operations fail
Concrete verification:
# Your filesystem behaves like any other:
$ mount | grep myfs
memfs on /tmp/myfs type fuse.memfs (rw,nosuid,nodev,relatime,user_id=1000)
# File operations work:
$ dd if=/dev/urandom of=/tmp/myfs/random.bin bs=1K count=100
100+0 records in
100+0 records out
102400 bytes (102 kB) copied, 0.01 s, 10.2 MB/s
$ md5sum /tmp/myfs/random.bin
a1b2c3d4e5f6... /tmp/myfs/random.bin
# Editors work:
$ vim /tmp/myfs/document.txt
# (create, edit, save - all work)
# You can copy to/from the filesystem:
$ cp /etc/passwd /tmp/myfs/
$ diff /etc/passwd /tmp/myfs/passwd
(no differences)
4. Solution Architecture
4.1 High-Level Design
┌─────────────────────────────────────────────────────────────────┐
│ FUSE Callbacks Layer │
│ myfs_getattr() myfs_readdir() myfs_read() myfs_write() │
│ myfs_create() myfs_mkdir() myfs_unlink() myfs_rmdir() │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Path Resolution Layer │
│ resolve_path(path) → inode_num │
│ resolve_parent(path) → (parent_ino, name) │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Inode Manager │
│ inode_alloc() inode_get() inode_free() │
│ Manages array of struct my_inode │
└─────────────────┬───────────────────────────────┬───────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐ ┌───────────────────────────┐
│ Directory Manager │ │ Data Manager │
│ dir_add_entry() │ │ file_read() │
│ dir_remove_entry() │ │ file_write() │
│ dir_lookup() │ │ file_truncate() │
│ dir_list() │ │ (malloc/realloc buffers) │
└─────────────────────────────────┘ └───────────────────────────┘
│ │
└───────────────┬───────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ In-Memory Storage │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Inode Table: struct my_inode inodes[MAX_INODES] │ │
│ │ │ │
│ │ [0]: unused (inode 0 is invalid) │ │
│ │ [1]: root directory (ino=1) │ │
│ │ └── entries: [{ino=2, "file.txt"}, ...] │ │
│ │ [2]: regular file (ino=2) │ │
│ │ └── data: "Hello, World!\n" │ │
│ │ [3]: free │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Inode Bitmap: int bitmap[MAX_INODES] │ │
│ │ [0]=0, [1]=1, [2]=1, [3]=0, [4]=0, ... │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| FUSE Callbacks | Interface with libfuse, call internal functions | Thin layer: validate args, call managers, return errors |
| Path Resolution | Convert path string to inode number | Iterative traversal, handle . and .. |
| Inode Manager | Allocate, lookup, free inodes | Fixed array vs dynamic; reuse freed inodes |
| Directory Manager | Manage directory entries | Store entries in inode struct vs separate |
| Data Manager | Store file contents | Dynamic allocation with realloc |
4.3 Data Structures
#define MAX_INODES 1024
#define MAX_NAME_LEN 255
#define MAX_DIR_ENTRIES 256
// File types
typedef enum {
MY_FILE,
MY_DIR,
MY_SYMLINK // Extension
} my_file_type;
// Directory entry
struct dir_entry {
uint32_t ino; // Inode number (0 = unused)
char name[MAX_NAME_LEN + 1]; // Null-terminated filename
};
// Inode structure
struct my_inode {
uint32_t ino; // Inode number
my_file_type type; // File type
mode_t mode; // Permissions (not including file type)
uid_t uid; // Owner user ID
gid_t gid; // Owner group ID
nlink_t nlink; // Link count
off_t size; // Size in bytes
struct timespec atime; // Access time
struct timespec mtime; // Modification time
struct timespec ctime; // Status change time
union {
// For regular files
struct {
char *data; // File content (malloc'd)
size_t capacity; // Allocated size
} file;
// For directories
struct {
struct dir_entry *entries; // Array of entries
size_t count; // Number of entries
size_t capacity; // Allocated slots
} dir;
// For symlinks (extension)
struct {
char *target; // Symlink target path
} symlink;
};
};
// Global filesystem state
struct myfs_state {
struct my_inode inodes[MAX_INODES];
int inode_bitmap[MAX_INODES]; // 1 = used, 0 = free
uint32_t next_ino; // Next inode to try
pthread_mutex_t lock; // For thread safety (optional)
};
// Global instance
static struct myfs_state *fs_state;
4.4 Algorithm Overview
Key Algorithm: Path Resolution
resolve_path("/home/user/file.txt") → inode_number
1. If path == "/", return ROOT_INODE (1)
2. Start at current_ino = ROOT_INODE
3. Split path by "/" → ["home", "user", "file.txt"]
4. For each component:
a. Get inode for current_ino
b. If not a directory, return -ENOTDIR
c. Search entries for component name
d. If not found, return -ENOENT
e. current_ino = found entry's inode
5. Return current_ino
Key Algorithm: File Write with Sparse Handling
myfs_write(path, buf, size, offset)
1. Resolve path to inode
2. If offset + size > capacity:
- Reallocate data buffer to offset + size
- If offset > current_size:
- Zero-fill gap from current_size to offset
3. memcpy(data + offset, buf, size)
4. If offset + size > size, update size
5. Update mtime and ctime
6. Return bytes written
Complexity Analysis:
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Path resolution | O(d * e) where d=depth, e=entries | O(1) |
| getattr | O(d * e) | O(1) |
| readdir | O(e) entries | O(1) |
| read/write | O(d * e + n) where n=bytes | O(n) for realloc |
| create/unlink | O(d * e) | O(1) |
5. Implementation Guide
5.1 Development Environment Setup
# Install FUSE development libraries
# Ubuntu/Debian:
sudo apt update
sudo apt install libfuse3-dev pkg-config build-essential
# Fedora/RHEL:
sudo dnf install fuse3-devel pkg-config gcc
# Arch Linux:
sudo pacman -S fuse3 pkg-config base-devel
# Verify installation
pkg-config --cflags --libs fuse3
# Should output: -I/usr/include/fuse3 -lfuse3 -lpthread
# Create project directory
mkdir memfs && cd memfs
5.2 Project Structure
memfs/
├── Makefile # Build configuration
├── memfs.c # Main file with FUSE callbacks
├── inode.h # Inode structure definitions
├── inode.c # Inode management functions
├── dir.h # Directory operation declarations
├── dir.c # Directory operations
├── path.h # Path resolution declarations
├── path.c # Path resolution implementation
├── tests/
│ ├── test_basic.sh # Basic functionality tests
│ ├── test_stress.sh # Stress tests
│ └── test_unit.c # Unit tests for internal functions
└── README.md # Usage instructions
Simple single-file structure (recommended to start):
memfs/
├── Makefile
├── memfs.c # Everything in one file initially
└── tests/
└── test_basic.sh
5.3 The Core Question You’re Answering
“What actually happens when a program calls open(), read(), or write()? How does the kernel translate those syscalls into filesystem operations?”
When you type cat file.txt:
catcallsopen("file.txt", O_RDONLY)- Who handles this?catcallsread(fd, buf, 4096)- Where does data come from?catcallsclose(fd)- What gets cleaned up?
Most developers can’t answer these questions beyond “the OS handles it.” After this project, you’ll know EXACTLY what happens because YOU implemented the filesystem that handles these calls.
5.4 Concepts You Must Understand First
Stop and research these before coding:
1. FUSE API Basics
- What is
fuse_main()and what does it do? - What is
struct fuse_operationsand how do you populate it? - How do FUSE callbacks receive the path vs. the file descriptor?
- What do the return values of FUSE callbacks mean?
- Book Reference: “The Linux Programming Interface” Ch. 14 (File Systems)
2. The stat Structure
- What is every field in
struct stat? - How do you encode file type in
st_mode? - What’s the difference between
atime,mtime, andctime? - Why does
st_nlinkmatter? - Book Reference: “The Linux Programming Interface” Ch. 15 (File Attributes)
3. Directory Entry Handling
- How are
.and..special? - Why are filenames in directory entries, not inodes?
- What is
fuse_fill_dir_tand how do you use it? - How do you iterate through directory entries?
4. Memory Management for File Data
- How do you dynamically grow a buffer with
realloc()? - What happens when
write()is called at an offset past current EOF? - How do you handle file truncation?
- Book Reference: “C Programming: A Modern Approach” Ch. 17 - K.N. King
5. Error Handling with errno
- What is
errnoand how does FUSE use it? - What do common errors mean: ENOENT, EACCES, EEXIST, EISDIR, ENOTDIR, ENOTEMPTY?
- How do you return errors from FUSE callbacks?
- Book Reference: “The Linux Programming Interface” Ch. 3 (System Programming Concepts)
5.5 Questions to Guide Your Design
Before implementing, think through these:
Phase 1: Minimal Skeleton
- What headers do you need for FUSE 3?
- How do you define
FUSE_USE_VERSION? - What’s the minimal
fuse_operationsthat lets you mount? - How do you compile and link with libfuse?
Phase 2: getattr and readdir
- When
getattr("/")is called, what should you return? - How do you fill
struct statfor a directory vs. a file? - What is
fuse_fill_dir_t fillerand how do you call it? - Should you include
.and..in readdir output?
Phase 3: Inode Management
- How do you allocate a new inode number?
- How do you initialize the root directory at mount time?
- When should you reuse freed inode numbers?
- How do you map inode numbers to inode structs?
Phase 4: File Creation and Data
- What happens when
create()is called? - How do you add an entry to the parent directory?
- When
write()is called, how do you store the data? - What if
write()is called at offset 1000 in a new file?
Phase 5: Directory Operations
- How does
mkdir()differ fromcreate()? - Why must new directories contain
.and..? - When can
rmdir()succeed vs. fail with ENOTEMPTY? - How do you update parent’s nlink when creating subdirectories?
5.6 Thinking Exercise
Trace a File Creation by Hand
Before coding, trace what happens when you run echo "hello" > /mnt/myfs/test.txt:
Shell: echo "hello" > /mnt/myfs/test.txt
1. Shell calls open("/mnt/myfs/test.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644)
→ Kernel: What's at /mnt/myfs/test.txt?
→ FUSE: getattr("/mnt/myfs/test.txt")
→ Your code: Look up path... not found!
→ Return: -ENOENT
2. Since O_CREAT is set and file doesn't exist:
→ FUSE: create("/mnt/myfs/test.txt", 0644, fi)
→ Your code:
a. Find parent directory inode for "/mnt/myfs"
b. Allocate new inode for the file
c. Initialize inode: type=FILE, mode=0644, size=0, data=NULL
d. Add entry "test.txt" to parent directory
e. Update parent's mtime
→ Return: 0 (success)
3. Shell calls write(fd, "hello\n", 6)
→ FUSE: write("/mnt/myfs/test.txt", "hello\n", 6, offset=0, fi)
→ Your code:
a. Resolve path to inode
b. Allocate data buffer (6 bytes or more)
c. Copy "hello\n" to buffer
d. Update size to 6
e. Update mtime
→ Return: 6 (bytes written)
4. Shell calls close(fd)
→ FUSE: release() (if implemented)
→ Your code: Nothing to do for in-memory FS
→ Return: 0
Questions while tracing:
- What inode number does the new file get?
- What’s in the root directory’s entries after creation?
- What are the atime, mtime, ctime values after write()?
- What happens if the parent directory doesn’t exist?
5.7 Hints in Layers
Hint 1: Start with the Minimal Skeleton
Get this compiling and mounting before adding functionality:
#define FUSE_USE_VERSION 31
#include <fuse.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
static int myfs_getattr(const char *path, struct stat *stbuf,
struct fuse_file_info *fi) {
(void)fi;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
stbuf->st_uid = getuid();
stbuf->st_gid = getgid();
return 0;
}
return -ENOENT;
}
static int myfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi,
enum fuse_readdir_flags flags) {
(void)offset;
(void)fi;
(void)flags;
if (strcmp(path, "/") != 0)
return -ENOENT;
filler(buf, ".", NULL, 0, 0);
filler(buf, "..", NULL, 0, 0);
return 0;
}
static struct fuse_operations myfs_ops = {
.getattr = myfs_getattr,
.readdir = myfs_readdir,
};
int main(int argc, char *argv[]) {
return fuse_main(argc, argv, &myfs_ops, NULL);
}
Compile and test:
gcc -Wall myfs.c -o myfs $(pkg-config fuse3 --cflags --libs)
mkdir /tmp/myfs
./myfs /tmp/myfs
ls /tmp/myfs # Should show empty directory
ls -la /tmp/myfs # Should show . and ..
fusermount -u /tmp/myfs
Hint 2: Add Inode Infrastructure
Before adding file operations, build your inode table:
#define MAX_INODES 1024
#define ROOT_INO 1
struct my_inode {
int used;
mode_t mode;
nlink_t nlink;
off_t size;
uid_t uid;
gid_t gid;
// ... other fields
};
static struct my_inode inodes[MAX_INODES];
void init_fs() {
memset(inodes, 0, sizeof(inodes));
// Initialize root directory
inodes[ROOT_INO].used = 1;
inodes[ROOT_INO].mode = S_IFDIR | 0755;
inodes[ROOT_INO].nlink = 2;
inodes[ROOT_INO].uid = getuid();
inodes[ROOT_INO].gid = getgid();
}
Hint 3: Implement Path Resolution
This is the core of your filesystem:
// Returns inode number or 0 if not found
uint32_t resolve_path(const char *path) {
if (strcmp(path, "/") == 0)
return ROOT_INO;
char *path_copy = strdup(path);
char *token = strtok(path_copy + 1, "/"); // Skip leading /
uint32_t current = ROOT_INO;
while (token != NULL) {
struct my_inode *dir = &inodes[current];
// Find entry with matching name
uint32_t found = 0;
for (size_t i = 0; i < dir->dir.count; i++) {
if (strcmp(dir->dir.entries[i].name, token) == 0) {
found = dir->dir.entries[i].ino;
break;
}
}
if (found == 0) {
free(path_copy);
return 0; // Not found
}
current = found;
token = strtok(NULL, "/");
}
free(path_copy);
return current;
}
Hint 4: Debug with FUSE Flags
Run in foreground with debug output:
./myfs -f -d /tmp/myfs
This shows every FUSE operation as it happens:
unique: 2, opcode: LOOKUP (1), nodeid: 1, insize: 48, pid: 1234
LOOKUP /test.txt
unique: 2, success, outsize: 144
Use strace on client programs:
strace ls /tmp/myfs 2>&1 | grep -E "(stat|getdents|open)"
5.8 The Interview Questions They’ll Ask
Prepare to answer these:
- “What is FUSE and how does it work?”
- Expected: Userspace filesystem through /dev/fuse, kernel module bridges VFS to user process
- “What is getattr, and why is it called so frequently?”
- Expected: It’s stat(). Called for every file access. Must be fast.
- “How does a filesystem resolve a path like ‘/home/user/file.txt’?”
- Expected: Start at root inode, read directory, find “home”, get its inode, repeat
- “What’s the difference between an inode and a directory entry?”
- Expected: Inode stores metadata (not name). Directory entry maps name → inode.
- “Why do directories have link count >= 2?”
- Expected: Parent’s entry + own “.” entry. Each subdirectory adds 1 for its “..”.
- “What should read() return if offset is past EOF?”
- Expected: Return 0 (EOF), not an error.
- “How would you implement hard links?”
- Expected: Multiple directory entries pointing to same inode. Increment nlink.
- “What happens when you delete a file that’s still open?”
- Expected: nlink goes to 0, but data preserved until last fd closed (FUSE handles this).
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| VFS and filesystem concepts | “The Linux Programming Interface” by Kerrisk | Ch. 14-15 |
| Directory and link operations | “The Linux Programming Interface” by Kerrisk | Ch. 18 |
| Filesystem implementation | “Operating Systems: Three Easy Pieces” | Ch. 39-40 |
| Linux kernel VFS internals | “Understanding the Linux Kernel” by Bovet & Cesati | Ch. 12 |
| FUSE tutorial | FUSE Tutorial | Full |
| System programming in C | “Advanced Programming in the UNIX Environment” | Ch. 4-5 |
5.10 Implementation Phases
Phase 1: FUSE Skeleton (Days 1-2)
Goals:
- Set up build environment with libfuse3
- Create minimal mountable filesystem
- Implement getattr for root directory only
- Implement readdir for root (empty)
Tasks:
- Install libfuse3-dev
- Create Makefile with correct flags
- Write minimal memfs.c with getattr and readdir
- Test: mount, ls, unmount
Checkpoint:
$ ./memfs /tmp/myfs
$ ls /tmp/myfs
$ ls -la /tmp/myfs
drwxr-xr-x 2 user user 0 Dec 20 10:00 .
drwxr-xr-x ... ..
$ fusermount -u /tmp/myfs
Phase 2: Inode Infrastructure (Days 2-4)
Goals:
- Define inode structure with all necessary fields
- Initialize root directory inode
- Create inode allocation/deallocation functions
- Update getattr to use inode data
Tasks:
- Define
struct my_inodewith mode, size, uid, gid, times - Create
inode_alloc()returning next free inode number - Create
inode_free()marking inode as available - Initialize root inode (ino=1) at startup
- Modify getattr to read from inode table
Checkpoint:
# Should still work, now backed by real inode structure
$ ./memfs /tmp/myfs
$ stat /tmp/myfs
# Shows real uid, gid, timestamps
Phase 3: Path Resolution (Days 4-5)
Goals:
- Tokenize path strings
- Traverse directory tree
- Return inode for any valid path
Tasks:
- Implement
resolve_path()for simple paths - Handle “/” specially
- Implement
resolve_parent()to get parent inode and basename - Test with paths like “/file.txt”, “/dir/file.txt”
Checkpoint:
// Unit tests:
assert(resolve_path("/") == ROOT_INO);
// After creating /test.txt:
assert(resolve_path("/test.txt") == some_ino);
assert(resolve_path("/nonexistent") == 0);
Phase 4: File Creation and Deletion (Days 5-7)
Goals:
- Implement create() callback
- Implement unlink() callback
- Manage directory entries
Tasks:
- Implement
dir_add_entry()to add to directory - Implement
dir_remove_entry()to remove from directory - Implement
myfs_create()- allocate inode, add entry - Implement
myfs_unlink()- remove entry, free if nlink=0 - Handle parent directory mtime updates
Checkpoint:
$ touch /tmp/myfs/test.txt
$ ls /tmp/myfs
test.txt
$ rm /tmp/myfs/test.txt
$ ls /tmp/myfs
(empty)
Phase 5: Read and Write (Days 7-10)
Goals:
- Store file data in memory
- Handle read with offset and size
- Handle write with growing buffers
- Update timestamps correctly
Tasks:
- Add data pointer and capacity to inode struct
- Implement
myfs_open()- just verify file exists - Implement
myfs_read()- copy data to buffer - Implement
myfs_write()- grow buffer, copy from buffer - Handle sparse writes (offset > current size)
- Update atime on read, mtime/ctime on write
Checkpoint:
$ echo "Hello World" > /tmp/myfs/test.txt
$ cat /tmp/myfs/test.txt
Hello World
$ echo "More text" >> /tmp/myfs/test.txt
$ cat /tmp/myfs/test.txt
Hello World
More text
Phase 6: Directory Operations (Days 10-12)
Goals:
- Implement mkdir and rmdir
- Handle nlink correctly for directories
- Ensure directories contain . and ..
Tasks:
- Implement
myfs_mkdir()- create dir inode with . and .. - Implement
myfs_rmdir()- check empty, remove - Update parent’s nlink on mkdir (increment) and rmdir (decrement)
- Handle nested directories
Checkpoint:
$ mkdir /tmp/myfs/subdir
$ ls -la /tmp/myfs
drwxr-xr-x 3 user user 0 ... .
...
drwxr-xr-x 2 user user 0 ... subdir
$ echo "nested" > /tmp/myfs/subdir/file.txt
$ cat /tmp/myfs/subdir/file.txt
nested
$ rmdir /tmp/myfs/subdir
rmdir: failed to remove 'subdir': Directory not empty
$ rm /tmp/myfs/subdir/file.txt
$ rmdir /tmp/myfs/subdir
$ ls /tmp/myfs
(empty)
Phase 7: Metadata Operations (Days 12-14)
Goals:
- Implement chmod, chown, utimens
- Implement truncate
- Handle all edge cases
Tasks:
- Implement
myfs_chmod()- update mode (preserve type bits) - Implement
myfs_chown()- update uid/gid - Implement
myfs_utimens()- update atime/mtime - Implement
myfs_truncate()- resize file
Checkpoint:
$ echo "test" > /tmp/myfs/file.txt
$ chmod 600 /tmp/myfs/file.txt
$ ls -l /tmp/myfs/file.txt
-rw------- 1 user user 5 ... file.txt
$ truncate -s 0 /tmp/myfs/file.txt
$ cat /tmp/myfs/file.txt
(empty)
$ truncate -s 1000 /tmp/myfs/file.txt
$ ls -l /tmp/myfs/file.txt
-rw------- 1 user user 1000 ... file.txt
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Inode storage | Dynamic allocation vs Fixed array | Fixed array | Simpler, predictable memory, fine for 1000 files |
| Directory entries | In inode struct vs Separate table | In inode struct (union) | Keeps related data together |
| Path resolution | Recursive vs Iterative | Iterative | Avoids stack overflow on deep paths |
| File data storage | Fixed size vs Dynamic realloc | Dynamic realloc | Memory efficient, handles any file size |
| Thread safety | None vs Mutex | Start without, add if needed | Simpler to debug single-threaded first |
| FUSE version | FUSE 2 vs FUSE 3 | FUSE 3 | Modern API, better maintained |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Smoke Tests | Basic functionality works | mount, ls, unmount |
| Unit Tests | Individual functions correct | path_resolve, inode_alloc |
| Integration Tests | Components work together | create + write + read |
| Edge Cases | Boundary conditions handled | empty files, long names, deep paths |
| Stress Tests | System handles load | 1000 files, large files |
6.2 Critical Test Cases
- Mount and Unmount
- Mount to empty directory
- Unmount cleanly
- Remount to same directory
- File Lifecycle
- Create file, verify exists
- Write data, read back same data
- Append data, read complete file
- Delete file, verify gone
- Directory Lifecycle
- Create directory
- Create nested directory
- Create file in nested directory
- Delete files and directories in order
- Metadata
- Correct permissions after chmod
- Correct ownership after chown (as root)
- Timestamps update appropriately
- Edge Cases
- Empty file (size 0)
- Large file (10MB)
- Long filename (255 chars)
- Deep nesting (/a/b/c/d/e/f/g/h/i/j/file.txt)
- Many files in one directory (100+)
6.3 Test Script
#!/bin/bash
# test_memfs.sh
set -e # Exit on first error
MOUNT=/tmp/myfs_test_$$
BINARY=./memfs
cleanup() {
fusermount -u $MOUNT 2>/dev/null || true
rmdir $MOUNT 2>/dev/null || true
}
trap cleanup EXIT
# Setup
mkdir -p $MOUNT
$BINARY $MOUNT
echo "=== Basic Operations ==="
# Test 1: Empty directory listing
echo "Test 1: Empty listing..."
[ "$(ls $MOUNT | wc -l)" -eq 0 ] && echo "PASS" || echo "FAIL"
# Test 2: File creation and read
echo "Test 2: File creation..."
echo "hello" > $MOUNT/test.txt
[ "$(cat $MOUNT/test.txt)" = "hello" ] && echo "PASS" || echo "FAIL"
# Test 3: File append
echo "Test 3: File append..."
echo "world" >> $MOUNT/test.txt
[ "$(cat $MOUNT/test.txt)" = $'hello\nworld' ] && echo "PASS" || echo "FAIL"
# Test 4: Directory creation
echo "Test 4: Directory creation..."
mkdir $MOUNT/subdir
[ -d $MOUNT/subdir ] && echo "PASS" || echo "FAIL"
# Test 5: Nested file
echo "Test 5: Nested file..."
echo "nested" > $MOUNT/subdir/nested.txt
[ "$(cat $MOUNT/subdir/nested.txt)" = "nested" ] && echo "PASS" || echo "FAIL"
# Test 6: File deletion
echo "Test 6: File deletion..."
rm $MOUNT/test.txt
[ ! -f $MOUNT/test.txt ] && echo "PASS" || echo "FAIL"
# Test 7: Non-empty rmdir fails
echo "Test 7: Non-empty rmdir..."
rmdir $MOUNT/subdir 2>/dev/null && echo "FAIL" || echo "PASS"
# Test 8: Proper cleanup
echo "Test 8: Cleanup..."
rm $MOUNT/subdir/nested.txt
rmdir $MOUNT/subdir
[ ! -d $MOUNT/subdir ] && echo "PASS" || echo "FAIL"
# Test 9: Permissions
echo "Test 9: Permissions..."
echo "test" > $MOUNT/perm.txt
chmod 000 $MOUNT/perm.txt
ls -l $MOUNT/perm.txt | grep -q "^----------" && echo "PASS" || echo "FAIL"
chmod 644 $MOUNT/perm.txt
# Test 10: Large file
echo "Test 10: Large file..."
dd if=/dev/urandom of=$MOUNT/large.bin bs=1M count=1 2>/dev/null
[ "$(stat -c%s $MOUNT/large.bin)" -eq 1048576 ] && echo "PASS" || echo "FAIL"
# Cleanup
rm $MOUNT/perm.txt $MOUNT/large.bin
echo "=== All tests completed ==="
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Missing file type in st_mode | ls shows ??????? instead of permissions |
Always OR file type: S_IFREG \| 0644 |
| getattr returns wrong value | Files not found that exist | Check return value: 0 for success, -errno for error |
| readdir missing . and .. | Some tools behave strangely | Always include . and .. entries |
| Wrong nlink for directories | find doesn’t descend into directories |
Directories start with nlink=2, add 1 per subdirectory |
| Not handling offset in read | Only first 4KB of file works | Use offset parameter: data + offset |
| Not growing buffer on write | Write past current size fails or corrupts | realloc when offset + size > capacity |
| Stale mount point | “Transport endpoint not connected” | fusermount -u /path or sudo umount -f /path |
| Not updating timestamps | make and other tools confused |
Update mtime on write, atime on read, ctime on metadata change |
7.2 Debugging Strategies
1. Run in Foreground with Debug
./memfs -f -d /tmp/myfs 2>&1 | tee debug.log
Shows every FUSE operation:
unique: 1, opcode: LOOKUP (1), nodeid: 1, insize: 48
LOOKUP /test.txt
unique: 1, error: -2 (No such file or directory), outsize: 16
2. Add Extensive Logging
#include <syslog.h>
void init_logging() {
openlog("memfs", LOG_PID | LOG_PERROR, LOG_USER);
}
// In each callback:
static int myfs_getattr(const char *path, struct stat *stbuf, ...) {
syslog(LOG_DEBUG, "getattr: path=%s", path);
// ...
syslog(LOG_DEBUG, "getattr: returning %d, mode=%o, size=%ld",
ret, stbuf->st_mode, stbuf->st_size);
return ret;
}
View logs: journalctl -f -t memfs
3. Use strace on Client Programs
strace -e trace=file ls /tmp/myfs 2>&1
Shows exactly what syscalls are made:
openat(AT_FDCWD, "/tmp/myfs", O_RDONLY|O_DIRECTORY) = 3
getdents64(3, [...], 32768) = 72
close(3) = 0
4. Check /proc/mounts
cat /proc/mounts | grep myfs
# memfs /tmp/myfs fuse.memfs rw,nosuid,nodev,relatime,user_id=1000 0 0
7.3 Performance Traps
| Trap | Impact | Solution |
|---|---|---|
| Slow path resolution | Every operation slow | Cache recently resolved paths |
| Reallocating on every write | Many small writes slow | Allocate in chunks (e.g., 4KB minimum) |
| Linear directory search | Large directories slow | Use hash table for entries (advanced) |
| Copying data in read/write | Large files slow | Minimize copies, consider mmap (advanced) |
8. Extensions & Challenges
8.1 Beginner Extensions
- Symbolic links: Implement
symlink()andreadlink() - rename(): Move/rename files and directories
- statfs(): Report filesystem statistics (for
df) - access(): Permission checking
8.2 Intermediate Extensions
- Hard links: Multiple names pointing to same inode
- Extended attributes:
setxattr(),getxattr() - File locking:
flock()support - Direct I/O: O_DIRECT support for large files
8.3 Advanced Extensions
- Persistence: Save/load filesystem to disk file
- Compression: Transparent file compression
- Encryption: Encrypt file data at rest
- Multi-threading: Handle concurrent operations safely
- Quotas: Per-user storage limits
9. Real-World Connections
9.1 Industry Applications
Cloud Storage Filesystems:
- s3fs-fuse: Mount Amazon S3 buckets as local directories
- gcsfuse: Google Cloud Storage access
- rclone mount: Multi-cloud filesystem (Dropbox, Google Drive, OneDrive)
Your project teaches the same fundamentals these use.
Encryption Filesystems:
- EncFS: Encrypted overlay filesystem
- gocryptfs: Modern encrypted FUSE filesystem
- CryFS: Cloud-storage-optimized encryption
All implement the same FUSE operations you’re implementing.
Development Tools:
- sshfs: Mount remote directories via SSH
- unionfs-fuse: Overlay multiple directories (used in Docker)
- bindfs: Remount with different permissions
9.2 Related Open Source Projects
- libfuse: github.com/libfuse/libfuse - The library you’re using
- fuse-rs: Rust bindings for FUSE
- go-fuse: Go bindings for FUSE
- python-fuse: Python bindings
9.3 Interview Relevance
Systems engineering interviews often ask:
- “Design a filesystem” - You can now describe inode structures, directory entries, block allocation
- “How does
lswork?” - You implemented the readdir() that ls calls - “Explain the difference between hard and soft links” - You understand inodes vs directory entries
- “What happens when you delete an open file?” - You know about nlink and reference counting
10. Resources
10.1 Essential Reading
- “The Linux Programming Interface” by Michael Kerrisk - Chapters 14-15 (Files), 18 (Directories)
- “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau - Chapters 39-40 (Filesystems)
- FUSE documentation - libfuse.github.io/doxygen
10.2 Video Resources
- FUSE Tutorial Video - Search “FUSE filesystem tutorial” on YouTube
- MIT 6.S081 Operating Systems - Filesystem lectures (free on YouTube)
10.3 Tools & Documentation
- libfuse GitHub: github.com/libfuse/libfuse
- FUSE examples:
libfuse/example/directory has reference implementations - fusermount: Unmount FUSE filesystems
- strace: Trace system calls for debugging
10.4 Related Projects in This Series
- Previous: P01 - Hex Dump Disk Explorer: Understand on-disk layout before building VFS
- Next: P03 - Persistent Block-Based Filesystem: Add persistence to disk
11. Self-Assessment Checklist
11.1 Understanding
- I can explain what VFS is and why it exists
- I can describe the flow of a read() system call through FUSE
- I can explain the difference between an inode and a directory entry
- I understand why getattr is called so frequently
- I can explain how path resolution works step by step
- I know why directories have nlink >= 2
11.2 Implementation
- Filesystem mounts and unmounts cleanly
- Files can be created, read, written, and deleted
- Directories can be created and removed
- Nested paths work correctly
- Permissions are handled correctly
- Timestamps update appropriately
- All test cases pass
11.3 Growth
- I can debug FUSE filesystems using -d flag and strace
- I can extend this filesystem with new operations
- I can explain my design decisions and trade-offs
- I could implement this again without looking at my code
- I can answer common interview questions about filesystems
12. Submission / Completion Criteria
Minimum Viable Completion:
- Filesystem mounts and unmounts
lsandls -lawork on root and subdirectoriestouch,echo >,catwork for filesmkdirandrmdirwork for directories- Basic test script passes
Full Completion:
- All minimum criteria plus:
- Permissions work (chmod)
- Truncate works
- Nested directories to depth 5+
- Files up to 10MB supported
- All edge case tests pass
- Timestamps update correctly
Excellence (Going Above & Beyond):
- Symbolic links implemented
- rename() implemented
- Hard links implemented
- Performance benchmarks documented
- Multi-threaded operation with proper locking
- Comprehensive documentation
This guide was generated from FILESYSTEM_INTERNALS_LEARNING_PROJECTS.md. For the complete learning path, see the parent directory.