Assignment Chef icon Assignment Chef
All English tutorials

Programming lesson

Building a FUSE Filesystem: From Zero to 1MB Disk Driver (Cs3650 Project 2 Guide)

Learn how to implement a custom FUSE filesystem on a 1MB disk image, covering basic file operations, directories, and large file support. Step-by-step tutorial inspired by the Cs3650 Project 2 assignment.

FUSE filesystem tutorial Cs3650 project 2 build filesystem from scratch 1MB disk image filesystem FUSE getattr implementation filesystem inode design FUSE readdir write large file support FUSE filesystem block allocation FUSE debugging tips filesystem project guide OS project filesystem filesystem design patterns FUSE C programming virtual filesystem example filesystem storage allocation

Introduction: Why Build Your Own Filesystem?

In 2026, with AI-driven apps and cloud storage dominating headlines, understanding how filesystems work under the hood is more relevant than ever. From FUSE (Filesystem in Userspace) powering virtual drives in Docker containers to custom filesystems in embedded systems, the ability to design a simple yet functional filesystem is a key skill for systems programmers. This tutorial walks you through building a FUSE-based filesystem on a 1MB disk image, similar to the Cs3650 Project 2 assignment, but with a focus on core concepts and practical implementation steps.

Understanding the 1MB Disk Image

Your filesystem lives inside a single file (e.g., data.nufs) that acts as a virtual disk. The disk is divided into blocks (typically 4KB each) and a superblock that stores metadata like the total number of blocks, free block count, and root directory location. You'll need to design an inode structure to represent files and directories, and a bitmap to track free blocks.

Key Data Structures

  • Superblock: Magic number, block size, total blocks, inode count, free block bitmap location.
  • Inode: File type (regular/directory), permissions, size, timestamps, direct block pointers (and indirect if supporting large files).
  • Directory entry: Name, inode number, next entry offset (or fixed-size entries).

For a 1MB disk with 4KB blocks, you have 256 blocks. Reserve block 0 for the superblock, block 1 for the inode table (or use a fixed inode count), and block 2 for the free block bitmap. The remaining 253 blocks store file data and directory contents.

Step 1: Setting Up FUSE and Starter Code

Install the required packages on your VM:

sudo apt-get install libfuse-dev libbsd-dev pkg-config

Clone your repository and run pkg-config --modversion fuse > fuse_version to verify FUSE is installed. The starter code in nufs.c provides empty FUSE callbacks like getattr, readdir, open, read, write, create, rename, unlink, etc. Your job is to fill them with logic that manipulates your disk image.

Step 2: Implementing Basic File Operations

getattr: The Foundation

Every FUSE operation starts with getattr. It returns file attributes (size, permissions, timestamps) for a given path. Without a correct getattr, nothing works. For the root directory "/", return a directory with mode 0755 and a fixed inode number (e.g., 1). For other files, look up the path in your directory structure and retrieve the inode.

Creating Files (create/mknod)

To create a file, allocate a free inode and a free data block (for small files), then write a directory entry in the parent directory. The create callback is called when a new file is opened with O_CREAT. Example flow:

  1. Parse the path to get parent directory and filename.
  2. Allocate an inode (from inode bitmap).
  3. Allocate a data block for the file (if size > 0).
  4. Add a directory entry in the parent directory block.
  5. Return the inode number as file handle.

Reading and Writing Small Files (<4K)

For files under 4KB, a single direct block pointer suffices. On write, copy data from the buffer to your block. On read, copy from your block to the buffer. Update the file size in the inode. Remember to return the number of bytes actually read/written.

Listing Directory Contents (readdir)

readdir must call filler for each entry: ., .., and all files/directories. Iterate over the directory entries stored in the directory's data blocks and call filler for each.

Renaming and Deleting

rename can be implemented by copying the directory entry to the new parent and removing the old entry. unlink removes the directory entry, frees the inode and data blocks, and updates the bitmap.

Step 3: Adding Directory Support

Now extend your filesystem to handle nested directories. Each directory is itself a file with a special inode type. Directory contents are stored as a list of entries. Implement mkdir, rmdir, and ensure rename works across directories. For rmdir, the directory must be empty (except . and ..).

To create a directory:

  1. Allocate an inode for the new directory.
  2. Allocate a data block for its entries.
  3. Add . (pointing to itself) and .. (pointing to parent) entries.
  4. Add an entry in the parent directory.

Step 4: Supporting Large Files (>4K)

To handle files larger than one block, you need indirect block pointers. The classic Unix inode design uses a small number of direct pointers (e.g., 12) plus a single indirect pointer that points to a block containing more pointers. With 4KB blocks and 4-byte pointers, one indirect block can point to 1024 data blocks, giving a maximum file size of (12 + 1024) * 4KB ≈ 4MB, well beyond your 1MB disk.

For this project, you only need to support files up to 500KB, so a simple indirect scheme is enough. When a file grows beyond one block, allocate an indirect block and populate it with pointers to data blocks. On read/write, calculate the logical block number and fetch the appropriate pointer.

Example: Writing to a Large File

Suppose the file is 10KB. You need 3 data blocks (0,1,2). The inode's direct[0] points to block 10, direct[1] to block 11, direct[2] to block 12. If the file grows to 20KB (5 blocks), you still use direct pointers (up to 12). To grow beyond 48KB (12*4KB), you allocate an indirect block. For a 100KB file, you'd use 25 data blocks: 12 direct + 13 through indirect.

Testing and Debugging

Run your filesystem in one terminal: make mount (which mounts under mnt/). In another terminal, perform operations like touch mnt/test.txt, ls -la mnt/, echo "hello" > mnt/test.txt, cat mnt/test.txt. Use make test to run automated tests. For debugging, use make gdb to launch GDB with your filesystem.

Common Pitfalls

  • Forgetting to update timestamps: Always set mtime, ctime, atime appropriately.
  • Incorrect error codes: Return -ENOENT, -ENOTDIR, -EIO, etc., as negative numbers.
  • Not handling . and .. correctly: Every directory must contain these entries.
  • Memory leaks: Free allocated buffers after use.
  • Block allocation race conditions: In single-threaded FUSE, this is less of an issue, but still be careful.

Conclusion

Building a filesystem from scratch is a rewarding experience that deepens your understanding of operating systems. By implementing FUSE callbacks, you create a fully functional filesystem that can be mounted and used like any other drive. The skills you learn—block management, inode design, directory traversal—are directly applicable to real-world systems programming. Good luck with your Cs3650 Project 2!