DEV Community

Cover image for How to Build Your Own Filesystem From Scratch in C
Farhad Rahimi Klie
Farhad Rahimi Klie

Posted on

How to Build Your Own Filesystem From Scratch in C

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");
Enter fullscreen mode Exit fullscreen mode

or

int fd = open("file.txt", O_RDONLY);
Enter fullscreen mode Exit fullscreen mode

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               |
|                                                |
+------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

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 | ... |
+------+------+------+------+------+------+
Enter fullscreen mode Exit fullscreen mode

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             |
+-------------------------+
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Think of it as pretending this file is an SSD.

disk.img

+------------------------------------------+
|                                          |
|             Raw Bytes                    |
|                                          |
+------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

We can open it using normal C functions.

FILE *disk = fopen("disk.img", "rb+");
Enter fullscreen mode Exit fullscreen mode

or

int fd = open("disk.img", O_RDWR);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A larger file:

Block 90
Block 91
Block 92
Block 93
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Meaning:

1 = Used
0 = Free
Enter fullscreen mode Exit fullscreen mode

Finding free space becomes easy.

Search bitmap

↓

00100000

↓

Found!

Allocate Block 2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Each pointer references one data block.

Reading the file is straightforward.

Read inode

↓

Read block pointers

↓

Read data
Enter fullscreen mode Exit fullscreen mode

Step 8 — File Growth

Eventually a file becomes larger.

Before

inode

91

92

93

94
Enter fullscreen mode Exit fullscreen mode

Now write more data.

Filesystem

↓

Find Free Block

↓

Allocate

↓

Update Bitmap

↓

Update Inode

↓

Increase File Size
Enter fullscreen mode Exit fullscreen mode

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|...|
+--------------------+
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Example:

notes.txt

↓

inode 15

photo.png

↓

inode 42

movie.mp4

↓

inode 80
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Hard Links

Multiple filenames pointing to the same inode.

fileA

↓

inode 12

↑

fileB
Enter fullscreen mode Exit fullscreen mode

Journaling

Protects against crashes.

Write Journal

↓

Write Real Data

↓

Commit
Enter fullscreen mode Exit fullscreen mode

If the system crashes, unfinished operations can be replayed safely.


Permissions

Store Unix permissions.

rwxr-xr-x
Enter fullscreen mode Exit fullscreen mode

Timestamps

Maintain:

  • Creation time
  • Modification time
  • Access time

Nested Directories

Support structures like:

/

home/

home/user/

home/user/docs/

home/user/docs/file.txt
Enter fullscreen mode Exit fullscreen mode

Suggested Development Order

Don't try to build everything at once.

Follow this roadmap:

  1. Create disk.img
  2. Define block size
  3. Implement disk read/write functions
  4. Create the superblock
  5. Implement bitmap-based free space management
  6. Build the inode table
  7. Create files
  8. Read files
  9. Write files
  10. Delete files
  11. Implement file growth
  12. Add directories
  13. Support nested directories
  14. Add indirect pointers
  15. Add journaling
  16. Add caching
  17. 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)