A from-scratch implementation of a Unix-style Virtual File System in C/C++, running entirely in RAM with a custom interactive shell — built to understand exactly how the Linux kernel manages files at the data structure level.
- What Is This?
- Memory Layout & Disk Structure
- System Architecture
- Data Structures (Deep Dive)
- How Each System Call Works
- Initialization Flow
- Command Reference
- Error Code Reference
- Configuration Reference
- Getting Started
- Full Session Walkthrough
- Known Limitations
- Future Enhancements
- Author
When you call open() or write() in a Linux program, the kernel runs through a chain of data structures — inodes, file tables, file descriptor tables — before a single byte touches the disk. This project reimplements that entire chain from scratch in user space, with RAM acting as the virtual disk.
Why is this interesting?
- There is no OS support. No
fopen, nofwrite. Everything — allocation, permission checks, offset tracking — is implemented manually. - The data structure design mirrors the actual Unix File Subsystem (
inode → file table → UFDT). - It ships with a working interactive shell that accepts real commands (
creat,read,write,stat,unlink,ls).
A real disk is divided into zones. This project mirrors that layout in RAM:
┌─────────────────────────────────────────────────────────────────────┐
│ VIRTUAL DISK (RAM) │
├──────────────┬─────────────────┬──────────────────┬────────────────┤
│ Boot Block │ Super Block │ Inode Table │ Data Blocks │
│ (1 KB) │ │ (DILB) │ │
│ │ TotalInodes: 5 │ Linked List of │ char* Buffer │
│ "Boot OS │ FreeInodes: 5 │ Inode structs │ per file │
│ process │ │ (heap-allocated)│ (100 bytes) │
│ done" │ │ │ │
└──────────────┴─────────────────┴──────────────────┴────────────────┘
This maps directly to the hand-drawn architecture diagram: Boot Block → Super Block → DILB (Data Inode List Block) → Data Block.
Each data block is a heap-allocated char* buffer of MAXFILESIZE bytes, assigned to an inode when a file is created via malloc() and reclaimed when deleted via free().
┌─────────────────────────────────────────────────────────────────────┐
│ USER INPUT │
│ CVFS > creat Notes.txt 3 │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ CUSTOM SHELL (main) │
│ • Reads input via fgets() │
│ • Tokenizes via sscanf() │
│ • Routes to system call by arg count (iCount = 1 / 2 / 3) │
└───────┬─────────────┬────────────────┬──────────────┬──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
CreateFile UnlinkFile write_file read_file
stat_file ls_file ManPage DisplayHelp
│ │ │ │
└─────────────┴────────────────┴──────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ UAREA (per-process context) │
│ │
│ UFDT[0] UFDT[1] UFDT[2] ... UFDT[MAXOPENEDFILES - 1] │
│ │ │
│ └──► FILETABLE ──► INODE ──► char* Buffer (Data Block) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ DILB — Inode Linked List │
│ │
│ [Inode 1]──►[Inode 2]──►[Inode 3]──►[Inode 4]──►[Inode 5]──►NULL │
│ (free) (in use) (free) (in use) (free) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ SUPER BLOCK │
│ TotalInodes = 5 FreeInodes = 2 │
└─────────────────────────────────────────────────────────────────────┘
This is the core pointer chain that every file operation traverses. This mirrors the hand-drawn diagram (UAREA → File Table → Inode Table → Data Block):
Integer FD (e.g., 0)
│
▼
uareaobj.UFDT[0] ← pointer to FileTable
│
├─ ReadOffset = 0
├─ WriteOffset = 11
├─ Count = 1
├─ Mode = READ+WRITE (3)
│
└─ ptrinode ─────────────────────────► INODE struct
│
├─ FileName: "Notes.txt"
├─ InodeNumber: 1
├─ FileSize: 100
├─ ActualFileSize: 11
├─ FileType: REGULARFILE (1)
├─ Permission: 3 (R+W)
├─ LinkCount: 1
├─ ReferenceCount: 1
│
└─ Buffer ──────► [H][e][l][l][o][ ][W][o][r][l][d]
Data Block on heap (char*)
typedef struct Inode {
char FileName[50]; // Name of the file
int InodeNumber; // Unique ID (1 to MAXINODE)
int FileSize; // Allocated size = MAXFILESIZE (100 bytes)
int ActualFileSize; // Bytes actually written so far
int FileType; // REGULARFILE(1) or SPECIALFILE(2); 0 = free slot
int ReferenceCount; // Number of processes referencing this inode
int LinkCount; // Hard link count (currently always 1)
int Permission; // READ(1), WRITE(2), READ+WRITE(3)
char *Buffer; // Pointer to heap-allocated data block
struct Inode *next; // Next inode in DILB linked list
} INODE, *PINODE, **PPINODE;FileType == 0 is the free-slot marker.
CreateFilescans the DILB forFileType == 0to find an available inode.UnlinkFileresetsFileTypeback to0to return it to the pool.
typedef struct FileTable {
int ReadOffset; // Current read cursor (bytes from start of buffer)
int WriteOffset; // Current write cursor (bytes from start of buffer)
int Count; // Reference count for this file table entry
int Mode; // Permission mode file was opened/created with
PINODE ptrinode; // Pointer to the associated Inode
} FILETABLE, *PFILETABLE;Each open file gets its own
FileTable. Two processes opening the same file would get two separateFileTableentries pointing to the sameInode— exactly how Linux handles shared file access.
struct SuperBlock {
int TotalInodes; // = MAXINODE — constant, never changes after init
int FreeInodes; // Decrements on creat, increments on unlink
};struct UAREA {
char ProcessName[50]; // = "Myexe"
PFILETABLE UFDT[MAXOPENEDFILES]; // Array of FileTable pointers (size 20)
};
UFDTis the User File Descriptor Table. The array index is the file descriptor.UFDT[0]= FD 0,UFDT[3]= FD 3. ANULLentry means that FD slot is unused and available.
struct BootBlock {
char Information[100]; // "Boot process of Operating System done"
};START
│
├─► name != NULL ? else → ERR_INVALID_PARAMETER
├─► permission in [1, 3] ? else → ERR_INVALID_PARAMETER
├─► SuperBlock.FreeInodes > 0 ? else → ERR_NO_INODES
├─► IsFileExists(name) == false ? else → ERR_FILE_ALREADY_EXIST
│
├─► Scan DILB linked list for first Inode with FileType == 0
├─► Scan UFDT array for first NULL slot → that index becomes the FD
│
├─► malloc(sizeof(FILETABLE)) → allocate FileTable on heap
├─► malloc(MAXFILESIZE) → allocate data buffer on heap
│
├─► Populate FileTable (ReadOffset=0, WriteOffset=0, Mode=permission)
├─► Populate Inode (name, FileSize=MAXFILESIZE, FileType=REGULAR)
├─► Link: UFDT[fd]->ptrinode = &inode
│
├─► SuperBlock.FreeInodes--
│
└─► return fd
START
│
├─► fd in [0, MAXOPENEDFILES] ? else → ERR_INVALID_PARAMETER
├─► UFDT[fd] != NULL ? else → ERR_FILE_NOT_EXIST
├─► Permission >= WRITE (2) ? else → ERR_PERMISSION_DENIED
├─► (MAXFILESIZE - WriteOffset) >= size? else → ERR_INSUFFICIENT_SPACE
│
├─► strncpy(Buffer + WriteOffset, data, size)
├─► WriteOffset += size
├─► ActualFileSize += size
│
└─► return size
START
│
├─► fd in [0, MAXOPENEDFILES] ? else → ERR_INVALID_PARAMETER
├─► UFDT[fd] != NULL ? else → ERR_FILE_NOT_EXIST
├─► Permission >= READ (1) ? else → ERR_PERMISSION_DENIED
├─► (MAXFILESIZE - ReadOffset) >= size ? else → ERR_INSUFFICIENT_DATA
│
├─► strncpy(buffer, Buffer + ReadOffset, size)
├─► ReadOffset += size
│
└─► return size
START
│
├─► name != NULL ? else → ERR_INVALID_PARAMETER
├─► IsFileExists(name) == true? else → ERR_FILE_NOT_EXIST
│
├─► Scan UFDT for entry matching FileName
│
├─► free(ptrinode->Buffer) ← release data block
├─► Reset all Inode fields = 0 ← FileType=0 marks it free again
├─► free(UFDT[i]) ← release FileTable
├─► UFDT[i] = NULL ← FD slot is now available
├─► SuperBlock.FreeInodes++
│
└─► return EXECUTE_SUCCESS (0)
Everything that happens before the shell prompt appears:
main()
│
└─► StartAuxilaryDataInitialisation()
│
├─► BootBlock initialized
│ └─► prints "Boot process of Operating System done"
│
├─► InitialiseSuperblock()
│ ├─► TotalInodes = MAXINODE (5)
│ └─► FreeInodes = MAXINODE (5)
│
├─► CreateDILB()
│ └─► Loop i = 1 to MAXINODE:
│ Allocate new INODE on heap
│ Set all fields to 0 / NULL
│ Link into list:
│ head → [1] → [2] → [3] → [4] → [5] → NULL
│
└─► InitialiseUAREA()
├─► ProcessName = "Myexe"
└─► UFDT[0 .. 19] = NULL
After this, the while(1) shell loop begins and CVFS is ready.
| Command | Syntax | Description |
|---|---|---|
help |
help |
Display all available commands |
man |
man <command> |
Show manual page for a specific command |
creat |
creat <filename> <perm> |
Create a new regular file. Returns an integer FD. |
write |
write <fd> |
Prompt for data and write it to the file |
read |
read <fd> <size> |
Read size bytes from the file at current offset |
stat |
stat <filename> |
Display metadata (size, permissions, type, link count) |
ls |
ls |
List all files currently in the virtual file system |
unlink |
unlink <filename> |
Delete a file and free its inode and data buffer |
clear |
clear |
Clear the terminal screen |
exit |
exit |
Shut down CVFS |
| Value | Meaning |
|---|---|
1 |
Read only |
2 |
Write only |
3 |
Read + Write |
| Code | Macro | When It Fires |
|---|---|---|
-1 |
ERR_INVALID_PARAMETER |
NULL filename, out-of-range FD, invalid permission value |
-2 |
ERR_NO_INODES |
FreeInodes == 0 when creat is called |
-3 |
ERR_FILE_ALREADY_EXIST |
creat with a name already present in the DILB |
-4 |
ERR_FILE_NOT_EXIST |
stat, unlink, or read/write on unknown file/FD |
-5 |
ERR_PERMISSION_DENIED |
Writing to read-only, or reading from write-only file |
-6 |
ERR_INSUFFICIENT_SPACE |
WriteOffset + size > MAXFILESIZE |
-7 |
ERR_INSUFFICIENT_DATA |
ReadOffset + size > MAXFILESIZE |
All system limits are macros at the top of CVFS.cpp. Change them before compiling to resize the system:
#define MAXFILESIZE 100 // Max bytes per file (data block size)
#define MAXOPENEDFILES 20 // UFDT size = max simultaneously open files
#define MAXINODE 5 // Max total files in the system
#define READ 1 // Permission bit: read
#define WRITE 2 // Permission bit: write
#define EXECUTE 4 // Permission bit: execute (defined, not yet enforced)
#define REGULARFILE 1 // FileType value for a regular file
#define SPECIALFILE 2 // FileType value for a special/device file
#define START 0 // lseek whence: from start (defined, not yet used)
#define CURRENT 1 // lseek whence: from current position
#define END 2 // lseek whence: from endA C++ compiler (g++). Verify with:
g++ --versionLinux / macOS: GCC or Clang are typically pre-installed or available via your package manager.
Windows: Install WSL and follow the Linux steps, or use MinGW-w64.
git clone https://github.com/Rohan13253/CustomisedVirtualFileSystem.git
cd CustomisedVirtualFileSystemg++ CVFS.cpp -o MyexeNo external libraries. No build system. No dependencies.
./MyexeBoot process of Operating System done
CVFS : Superblock initialised succesfully
CVFS : DILB created succesfully
CVFS : UAREA initialised succesfully
---------------------------------------------------------
----------------CVFS Started Succesfully ----------------
---------------------------------------------------------
CVFS > help
(displays command list)
CVFS > creat Notes.txt 3
Current Inodes remaining : 5
File is succesfully created with FD : 0
CVFS > write 0
Please enter the data that you want to write into the file :
Hello World
11 bytes gets succesfully written into the file
CVFS > read 0 5
Read operation is successfull
Data from file is : Hello
CVFS > stat Notes.txt
------------ Statistical Information of file -----------
File name : Notes.txt
File size on Disk : 100
Actual File size : 11
Link count : 1
File permission : Read + Write
File type : Regular file
--------------------------------------------------------
CVFS > creat Config.txt 1
Current Inodes remaining : 4
File is succesfully created with FD : 1
CVFS > write 1
Please enter the data that you want to write into the file :
test data
Error : Unable to write as there is no write permission
CVFS > ls
Notes.txt
Config.txt
CVFS > unlink Notes.txt
Unlink Opertaion is succesfully performed
CVFS > ls
Config.txt
CVFS > exit
Thank you for using CVFS
Deallocating all resources...
Documented honestly for contributors and interviewers:
| # | Issue | Severity | Location |
|---|---|---|---|
| 1 | No persistence. All data lives in RAM and is lost when the process exits. | High | Entire system |
| 2 | Read permission check is incorrect. read_file checks Permission < READ (i.e., < 1), but a write-only file has Permission = 2, which passes the check. Write-only files can be read. |
High | read_file() |
| 3 | malloc size bug in read command. malloc(sizeof(atoi(Command[2]))) allocates sizeof(int) = 4 bytes instead of the requested read size. Reading more than 4 bytes causes a heap buffer overflow. |
High | main() |
| 4 | write success message always prints from FD 0. The buffer display after a successful write hardcodes UFDT[0] instead of using the actual FD. |
Medium | main() write handler |
| 5 | No open system call. creat simultaneously allocates the inode and returns an FD. There is no way to re-open an existing file once its FD is forgotten. |
Medium | Design |
| 6 | No close system call. Files cannot be closed without deleting them. All FD slots stay occupied until unlink. |
Medium | Design |
| 7 | lseek macros defined but function not implemented. START, CURRENT, END are defined but never used. |
Low | CVFS.cpp |
| 8 | fflush(stdin) is undefined behavior on input streams in the C standard. |
Low | main() |
| Priority | Enhancement | Notes |
|---|---|---|
| 🔴 High | Fix malloc size bug in read |
Change to malloc(atoi(Command[2]) + 1) |
| 🔴 High | Fix read permission check | Use !(Permission & READ) bitwise check instead of < READ |
| 🔴 High | Sync README with actual macro values | MAXINODE and MAXFILESIZE are wrong in current README |
| 🔴 High | Persistence | Serialize DILB + data buffers to a .cvfs file on exit, reload on startup |
| 🟠 Medium | Implement open and close |
Files should be creatable once and openable many times independently |
| 🟠 Medium | Implement lseek |
Macros already defined — add the function and shell command |
| 🟡 Low | Directory support | Add DirectoryInode type; implement mkdir, cd, pwd |
| 🟡 Low | Multi-user simulation | Multiple UAREA instances with simulated user switching |
| 🟡 Low | Replace fflush(stdin) |
Use while(getchar() != '\n') for portable input flushing |
Rohan Murlidhar Pawar
- GitHub: @Rohan13253
- Project started: August 2025
This project was built to understand the Unix File Subsystem at the kernel data structure level. The design intentionally mirrors the real Linux implementation — UAREA, UFDT, File Table, and Inode chain — rather than using simplified abstractions.