Stop treating filesystems as magic. Build one yourself and understand how operating systems actually store data.
Most programmers use a filesystem every day.
FILE *fp = fopen("notes.txt", "r");
or
int fd = open("file.txt", O_RDONLY);
But very few programmers know what happens after those function calls.
Where is the file actually stored?
How does the operating system know where its data begins?
How does deleting a file free disk space?
How does a filesystem survive a power failure?
These questions are answered by one piece of software:
The filesystem.
In this article, we'll build one from scratch—not a toy with a single array in memory, but a real userspace filesystem stored inside a disk.img file. By the end, you'll understand many of the same concepts used by Linux filesystems like ext2, ext3, and ext4.
Why Build a Filesystem?
Building a filesystem teaches far more than file storage.
You'll learn about:
- Disk layouts
- Binary file formats
- Metadata
- Block allocation
- Bitmaps
- Direct and indirect pointers
- File growth
- Directory structures
- Crash consistency
- C programming
- Systems programming
It's one of the best projects for becoming a systems programmer.
What Is a Filesystem?
A filesystem is software that organizes raw bytes on a storage device.
Imagine you have a completely empty disk.
+------------------------------------------------+
| |
| 1 GB of Random Bytes |
| |
+------------------------------------------------+
Without a filesystem, the operating system sees only bytes.
It doesn't know:
- where files begin
- where files end
- which space is free
- what filenames exist
The filesystem provides all of that information.
The Basic Idea
Instead of treating the disk as one giant byte array, we divide it into blocks.
Disk
+------+------+------+------+------+------+
|Blk 0 |Blk 1 |Blk 2 |Blk 3 |Blk 4 | ... |
+------+------+------+------+------+------+
A block is the smallest storage unit.
Typical sizes are:
- 512 bytes
- 1024 bytes
- 2048 bytes
- 4096 bytes
Linux commonly uses 4096-byte blocks.
Our Project
We'll build everything ourselves.
disk.img
+-------------------------+
| Superblock |
+-------------------------+
| Bitmap |
+-------------------------+
| Inode Table |
+-------------------------+
| Data Blocks |
+-------------------------+
Every modern filesystem has something similar.
Step 1 — Create a Virtual Disk
Instead of modifying a real hard drive, we'll use a normal file.
disk.img
Think of it as pretending this file is an SSD.
disk.img
+------------------------------------------+
| |
| Raw Bytes |
| |
+------------------------------------------+
We can open it using normal C functions.
FILE *disk = fopen("disk.img", "rb+");
or
int fd = open("disk.img", O_RDWR);
Every read and write will modify the virtual disk.
Step 2 — Design the Disk Layout
Before writing code, always design the filesystem.
Never start coding first.
Example layout:
Block 0
----------------
Superblock
Blocks 1-8
----------------
Bitmap
Blocks 9-40
----------------
Inode Table
Blocks 41+
----------------
File Data
Everything begins with a good layout.
Step 3 — Superblock
The superblock is the brain of the filesystem.
It stores global information.
Superblock
Magic Number
Version
Block Size
Total Blocks
Free Blocks
Inode Count
Free Inodes
Bitmap Start
Inode Start
Data Start
Whenever the filesystem mounts, it reads the superblock first.
Without it, nothing else can be understood.
Step 4 — Blocks
Instead of storing files continuously, we store them in blocks.
Example:
File
Hello World!
Stored as
Block 90
A larger file:
Block 90
Block 91
Block 92
Block 93
Large files simply occupy more blocks.
Step 5 — Free Space Management
How does the filesystem know which blocks are free?
Using a bitmap.
Example:
Block Number
0 1 2 3 4 5 6 7
Bitmap
1 1 0 0 1 0 0 0
Meaning:
1 = Used
0 = Free
Finding free space becomes easy.
Search bitmap
↓
00100000
↓
Found!
Allocate Block 2
This technique is used in ext2, ext3, ext4, FAT, and many other filesystems.
Step 6 — Inodes
The inode is the heart of every Unix filesystem.
It stores metadata—not the filename.
Example:
inode
File Size
Permissions
Owner
Creation Time
Modified Time
Block Pointers
Notice something missing?
The filename.
Linux stores filenames separately inside directories.
Step 7 — Direct Block Pointers
Small files are easy.
inode
Block 91
Block 92
Block 93
Block 94
Each pointer references one data block.
Reading the file is straightforward.
Read inode
↓
Read block pointers
↓
Read data
Step 8 — File Growth
Eventually a file becomes larger.
Before
inode
91
92
93
94
Now write more data.
Filesystem
↓
Find Free Block
↓
Allocate
↓
Update Bitmap
↓
Update Inode
↓
Increase File Size
Growing a file is simply allocating additional blocks and updating metadata.
Step 9 — Indirect Pointers
Direct pointers eventually run out.
Linux solves this with indirect blocks.
inode
Direct
Direct
Direct
Indirect
|
v
+--------------------+
|101|102|103|104|...|
+--------------------+
Instead of pointing to data, the inode points to another block containing many more block numbers.
This allows files to grow into gigabytes or even terabytes.
Step 10 — Directories
A directory is just another file.
Instead of storing text, it stores mappings.
Filename
↓
Inode Number
Example:
notes.txt
↓
inode 15
photo.png
↓
inode 42
movie.mp4
↓
inode 80
The operating system first finds the inode, then reads the file.
Step 11 — Reading a File
The process is surprisingly simple.
Open File
↓
Find Directory Entry
↓
Get Inode Number
↓
Read Inode
↓
Read Block Pointers
↓
Read Data Blocks
↓
Return Bytes
This sequence happens every time you open a file.
Step 12 — Deleting a File
Deleting does not erase the data immediately.
Instead:
Remove Directory Entry
↓
Free Blocks in Bitmap
↓
Free Inode
↓
Mark Space Available
The old bytes often remain until overwritten.
Step 13 — Formatting the Filesystem
Before using the disk, it must be formatted.
Formatting means:
- Create the superblock
- Initialize the bitmap
- Create empty inode table
- Reserve metadata blocks
- Mark remaining blocks as free
After formatting, the filesystem is ready.
Step 14 — Features to Add Later
Once the core filesystem works, you can implement advanced features.
Symbolic Links
myfile
↓
inode
↓
Target Path
Hard Links
Multiple filenames pointing to the same inode.
fileA
↓
inode 12
↑
fileB
Journaling
Protects against crashes.
Write Journal
↓
Write Real Data
↓
Commit
If the system crashes, unfinished operations can be replayed safely.
Permissions
Store Unix permissions.
rwxr-xr-x
Timestamps
Maintain:
- Creation time
- Modification time
- Access time
Nested Directories
Support structures like:
/
home/
home/user/
home/user/docs/
home/user/docs/file.txt
Suggested Development Order
Don't try to build everything at once.
Follow this roadmap:
- Create
disk.img - Define block size
- Implement disk read/write functions
- Create the superblock
- Implement bitmap-based free space management
- Build the inode table
- Create files
- Read files
- Write files
- Delete files
- Implement file growth
- Add directories
- Support nested directories
- Add indirect pointers
- Add journaling
- Add caching
- Optimize performance
Each milestone builds on the previous one, making debugging much easier.
Final Thoughts
Building a filesystem is one of the most rewarding systems programming projects you can undertake. It combines low-level C programming, binary data structures, memory management, and operating system concepts into a single, practical project.
You'll stop thinking of files as mysterious objects managed by the OS and start seeing them for what they really are: carefully organized blocks of bytes, tracked by metadata and allocation structures.
If you can build a filesystem from scratch, you'll gain a much deeper understanding of how Linux works internally—and you'll develop the skills needed for advanced areas like kernel development, storage engines, databases, embedded systems, and operating system design.
Every byte on a disk has a purpose. Your job as a filesystem developer is to decide what that purpose should be.
Top comments (0)