Building a Minimal Linux Filesystem from Scratch: TinyFS Walkthrough
This article walks through the design and implementation of a tiny Linux filesystem called TinyFS, explaining the VFS layers, required POSIX interfaces, data structures, core kernel code, usage steps, current limitations, and a roadmap for extending it with proper superblocks, concurrency, and page‑cache integration.
Why Look Inside a Linux Filesystem?
Linux, like other UNIX‑like systems, presents a uniform directory tree, but the underlying implementations differ. Understanding the VFS (Virtual File System) and how a concrete filesystem plugs into it is essential for developers who want to build or modify filesystems.
VFS Interfaces Required by a Filesystem
A complete filesystem must provide three kinds of operations:
POSIX system‑call interface – implementations of open, read, write, etc.
Block‑device interface – communication with the underlying storage medium.
Internal management – handling of inodes, dentries, mount points, reference counting, etc.
When all three are present, the filesystem can be mounted and used like any other.
Design of TinyFS – A Very Small, Runnable Filesystem
TinyFS stores everything in a single contiguous memory array, avoiding any block‑device abstraction. Its on‑disk (in‑memory) format is deliberately simple:
#define MAXLEN 8
#define MAX_FILES 32
#define MAX_BLOCKSIZE 512
struct dir_entry {
char filename[MAXLEN];
uint8_t idx;
};
struct file_blk {
uint8_t busy;
mode_t mode;
uint8_t idx;
union {
uint8_t file_size;
uint8_t dir_children;
};
char data[0];
};
struct file_blk block[MAX_FILES+1];
int curr_count = 0;Key limitations of this layout are the lack of a superblock, no inode bitmap, metadata mixed with file data, and no concurrency protection.
Core Kernel Code
Below are the most important functions (all code is kept verbatim inside ... tags):
static int get_block(void) {
int i;
for (i = 2; i < MAX_FILES; i++) {
if (!block[i].busy) {
block[i].busy = 1;
return i;
}
}
return -1;
}
ssize_t tinyfs_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos) {
struct file_blk *blk = (struct file_blk *)filp->f_path.dentry->d_inode->i_private;
if (*ppos >= blk->file_size) return 0;
char *buffer = (char *)&blk->data[0] + *ppos;
len = min((size_t)blk->file_size, len);
if (copy_to_user(buf, buffer, len)) return -EFAULT;
*ppos += len;
return len;
}
ssize_t tinyfs_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos) {
struct file_blk *blk = (struct file_blk *)filp->f_path.dentry->d_inode->i_private;
char *buffer = (char *)&blk->data[0] + *ppos;
if (copy_from_user(buffer, buf, len)) return -EFAULT;
*ppos += len;
blk->file_size = *ppos;
return len;
}
static int tinyfs_do_create(struct inode *dir, struct dentry *dentry, umode_t mode) {
struct inode *inode;
struct super_block *sb = dir->i_sb;
struct dir_entry *entry;
struct file_blk *blk, *pblk;
int idx;
if (curr_count >= MAX_FILES) return -ENOSPC;
inode = new_inode(sb);
if (!inode) return -ENOMEM;
inode->i_sb = sb;
inode->i_op = &tinyfs_inode_ops;
inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
idx = get_block();
blk = &block[idx];
inode->i_ino = idx;
blk->mode = mode;
curr_count++;
if (S_ISDIR(mode)) {
blk->dir_children = 0;
inode->i_fop = &tinyfs_dir_operations;
} else if (S_ISREG(mode)) {
blk->file_size = 0;
inode->i_fop = &tinyfs_file_operations;
}
inode->i_private = blk;
pblk = (struct file_blk *)dir->i_private;
entry = (struct dir_entry *)&pblk->data[0];
entry += pblk->dir_children;
pblk->dir_children++;
entry->idx = idx;
strcpy(entry->filename, dentry->d_name.name);
inode_init_owner(inode, dir, mode);
d_add(dentry, inode);
return 0;
}
static struct dentry *tinyfs_lookup(struct inode *parent_inode, struct dentry *child_dentry, unsigned int flags) {
struct super_block *sb = parent_inode->i_sb;
struct file_blk *blk = (struct file_blk *)parent_inode->i_private;
struct dir_entry *entry = (struct dir_entry *)&blk->data[0];
int i;
for (i = 0; i < blk->dir_children; i++) {
if (!strcmp(entry[i].filename, child_dentry->d_name.name)) {
struct inode *inode = tinyfs_iget(sb, entry[i].idx);
d_add(child_dentry, inode);
return NULL;
}
}
return NULL;
}The filesystem registers its operations with VFS via inode_operations and file_operations structures, sets up a superblock in tinyfs_fill_super, and provides mount/unmount callbacks.
Using TinyFS
After building the module, the workflow is:
# insmod ./tinyfs.ko
# mount -t tinyfs none /mnt
# cd /mnt
# echo 11111 > a
# cat a
# mkdir dir1
# echo 22222 > dir1/b
# cat dir1/b
# rm -f dir1/c # (file removal works)
# rmdir dir1 # (fails – directory removal is a TODO)All operations succeed except directory removal, which is intentionally left as a future task.
What’s Next?
To turn TinyFS into a production‑ready filesystem, the author lists several TODO items:
Fix reference‑count bugs and add proper inode cleanup.
Introduce a superblock, free‑block bitmap, and a more realistic on‑disk layout.
Use the block‑layer APIs ( sb_bread, mark_buffer_dirty, etc.) instead of direct memory access.
Integrate the page‑cache for efficient data caching and implement write‑back handling.
Add concurrency control (spinlocks, mutexes) to make the filesystem thread‑safe.
Completing these steps will give a deep, hands‑on understanding of Linux kernel filesystem internals.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
