Skip to content

File Systems

Enrico Fraccaroli (Galfurian) edited this page Jan 28, 2026 · 10 revisions

File Systems in MentOS

This page teaches you how MentOS manages files and directories on disk.

Important: All code references point to the actual MentOS source code. Explore these files in kernel/src/fs/ and kernel/inc/fs/ to understand how MentOS implements file systems.

The Problem: How Do We Store Files?

Scenario: You write a file myfile.txt to disk. Then you turn off the computer. When you turn it back on, the file is still there.

Question: How does the computer remember where myfile.txt is? What's the structure on disk?

Answer: A filesystem - a way to organize data on disk into files and folders.

The Three Layers of File I/O

MentOS uses three layers (like a sandwich):

┌─────────────────────────────────────┐
│  User Programs (ring 3)             │
│  printf("Hello");                   │
│  read(fd, buf, 10);                 │
│  write(fd, "data", 4);              │
├─────────────────────────────────────┤
│  VFS Layer (kernel)                 │
│  "I don't care HOW you store files" │
│  Just provide: open, read, write    │
│  Works with ANY filesystem          │
├─────────────────────────────────────┤
│  Filesystem Drivers                 │
│  EXT2: "Here's how WE do it"        │
│  ProcFS: "And here's how WE do it"  │
└─────────────────────────────────────┘
        ↓
    Hard Disk

Key Idea: The VFS (Virtual File System) is a plug-and-play interface. Add a new filesystem? Just implement the VFS interface!

1. The VFS Layer - "The Adapter"

What is the VFS?

The VFS is a layer of abstraction that says: "I don't care if you're using EXT2, FAT32, or a USB drive. You all must support the same interface."

Implementation: kernel/src/fs/vfs.c and kernel/inc/fs/vfs.h

Key VFS Data Structures

Think of these as "handles" to files and directories:

File Descriptor (fd)

When you do: fd = open("/home/user/file.txt", O_RDONLY)

The kernel creates:
┌─────────────────────────────────┐
│ File Descriptor (fd = 3)        │
├─────────────────────────────────┤
│ • File position: 0              │
│ • Open flags: O_RDONLY          │
│ • Points to: inode of file.txt  │
│ • Points to: directory entry    │
└─────────────────────────────────┘
     ↓
   Inode (in-memory representation of file metadata)
     ↓
   Disk (where actual data lives)

Real analogy: Like a bookmark in a book. The fd tells you WHERE YOU ARE in the file (position = page 5).

Inode (index node)

Every file/directory has an INODE that stores:
  • File type (regular file, directory, symlink, device)
  • Permissions (rwxrwxrwx)
  • Owner (user ID, group ID)
  • Size in bytes
  • Timestamps (created, modified, accessed)
  • WHERE ON DISK is the file data?

Inode does NOT store the filename!

Real analogy: Like a library card catalog entry. It describes WHAT a file is, but not its name or location.

Directory Entry (dentry)

Maps FILENAME to INODE:
  "file.txt" → inode #12345
  "folder/" → inode #12346
  ".." → inode #12340 (parent)

Real analogy: Like page 5 in the phone book: "Smith, John" → phone number "555-1234"

How open() Works Under the Hood

fd = open("/home/user/file.txt", O_RDONLY)

1. PARSE PATH: /home/user/file.txt
   Start at root inode (inode #2)
   │
   ├─ Directory lookup: "home"
   │  └─ Read root inode → find "home" entry → get inode #1000
   │
   ├─ Directory lookup: "user"  
   │  └─ Read inode #1000 → find "user" entry → get inode #1001
   │
   └─ Directory lookup: "file.txt"
      └─ Read inode #1001 → find "file.txt" entry → get inode #1002

2. GET INODE METADATA from disk (inode #1002)
   • Size: 150 bytes
   • Permissions: rw-r--r--
   • Owner: uid 1000
   • Data blocks on disk: 256, 257, 258

3. CREATE FILE DESCRIPTOR
   allocate fd = 3 (first free slot)
   fd[3].position = 0
   fd[3].inode = inode #1002
   fd[3].flags = O_RDONLY

4. RETURN 3 to user

Result: When user calls read(3, buf, 10), kernel knows exactly which file to read!

How read() Works

ssize_t n = read(3, buf, 10);  // Read 10 bytes from fd #3
1. Kernel looks up fd #3
   → Gets inode #1002
   → Knows position = 0

2. Kernel calculates which DISK BLOCK to read
   • File position = 0
   • Block size = 4096 bytes
   • Need byte 0-9
   → Those are in block #256 on disk

3. Kernel reads block #256 from disk
   → Gets 4096 bytes

4. Kernel copies bytes 0-9 to user buffer
   buf = "Hello file"  ← user gets this

5. Kernel updates position
   fd[3].position = 10

6. RETURN 10 (bytes read)

MentOS Implementation:

  • Path parsing: kernel/src/fs/namei.c (name to inode lookup)
  • File descriptor management: kernel/src/fs/vfs.c
  • Read/write operations: kernel/src/fs/vfs.c

2. EXT2 Filesystem - "How Data Lives on Disk"

What is EXT2?

EXT2 is a specific filesystem implementation. It says: "Here's HOW I organize data on this disk."

Implementation: kernel/src/fs/ext2.c

Disk Layout

Think of a disk like a filing cabinet:

┌─────────────────────────────────────────┐
│ Boot Sector (1 block) [How to boot OS]  │
├─────────────────────────────────────────┤
│ Superblock (1 block) [Disk metadata]    │
├─────────────────────────────────────────┤
│ Inode Bitmap (several blocks)           │
│ [Which inodes are used? (bitmap)]       │
├─────────────────────────────────────────┤
│ Block Bitmap (several blocks)           │
│ [Which disk blocks are used?]           │
├─────────────────────────────────────────┤
│ Inode Table (many blocks)               │
│ [Inode #1, #2, #3, ... metadata]        │
├─────────────────────────────────────────┤
│ Data Blocks (bulk of disk)              │
│ [Actual file contents]                  │
└─────────────────────────────────────────┘

Key Concept: The Inode

On disk, each inode is 128 bytes and contains:

struct ext2_inode {
    uint16_t i_mode;           // File type + permissions
    uint16_t i_uid;            // Owner user ID
    uint32_t i_size;           // File size in bytes
    uint32_t i_atime;          // Last access time
    uint32_t i_ctime;          // Creation time
    uint32_t i_mtime;          // Modification time
    uint16_t i_gid;            // Owner group ID
    uint16_t i_links_count;    // Hard link count
    uint32_t i_blocks;         // Disk blocks used
    uint32_t i_flags;          // Special flags
    
    // MOST IMPORTANT: Where is the file data on disk?
    uint32_t i_block[15];      // "Block pointers" (see next section)
};

Block Pointers: How Does EXT2 Find File Data?

This is the clever part!

Each inode has 15 block pointers:

  • i_block[0-11] = Direct pointers (point directly to data blocks)
  • i_block[12] = Indirect pointer (points to block of pointers)
  • i_block[13] = Double indirect
  • i_block[14] = Triple indirect

Example: Reading a small file (5KB)

File: myfile.txt (5KB = 1.25 blocks, rounded to 2 blocks)

Inode #42:
  i_size = 5120
  i_block[0] = 1000  ← Block 1000 on disk
  i_block[1] = 1001  ← Block 1001 on disk
  i_block[2-14] = 0  ← Unused

When reading bytes 0-5119:
1. Check inode #42
2. See that data is in blocks 1000-1001
3. Read block 1000 (4096 bytes) → get bytes 0-4095
4. Read block 1001 (1024 bytes) → get bytes 4096-5119
5. Done!

Example: Reading a huge file (5MB)

File: bigfile.bin (5MB)

Inode #50:
  i_size = 5242880
  i_block[0-11] = 2000, 2001, 2002, ...  (12 direct blocks = 48KB)
  i_block[12] = 5000  ← Indirect block!

When we need block #1000 in the file:
1. Calculate: block 1000 is beyond direct pointers
2. Read "indirect block" #5000 from disk
3. It contains: [3000, 3001, 3002, 3003, ...]
   Each entry is a block pointer!
4. From that, get pointer to actual block
5. Read that block
6. Done!

Why this design?

  • Small files: Fast! Use direct pointers
  • Large files: Works! Use indirect pointers
  • Huge files: Still works! Use double/triple indirect

Directory Entries on Disk

Directories are just files that store directory entries:

Directory /home/user/ (stored as a file on disk)

Bytes:  Data
────────────────────────────────────────────
0-3:    12345 (inode of "desktop")
4-255:  "desktop\0" + padding

256-259: 12346 (inode of "documents")
260-383: "documents\0" + padding

384-387: 12347 (inode of "file.txt")
388-415: "file.txt\0" + padding

When kernel does ls /home/user/:

  1. Open inode of /home/user/ (it's a directory)
  2. Read its data blocks (which contain directory entries)
  3. Parse entries: filename → inode → lookup inode metadata
  4. Print: file.txt (12347, 5KB, rw-r--r--, Jan 15 14:30)

3. Other Filesystems

ProcFS - "The Virtual Filesystem"

Location: kernel/src/fs/procfs.c

ProcFS is not on disk - it's generated on-the-fly by the kernel.

/proc/1/status        ← Created on-demand by kernel
  Returns: Process 1's status as a "file"

/proc/meminfo         ← Shows system memory stats
  Generated by: kernel/src/fs/procfs.c

/proc/filesystems     ← Lists available filesystems

Example: When user reads /proc/1/status:

  1. Kernel sees: "Oh, this is a procfs file"
  2. Kernel calls: procfs_read_status()
  3. Function returns: Current process status as bytes
  4. User reads: "State: S (sleeping)\nPPID: 1\n..."

Why? Provides Unix-style interface to kernel data without storing anything on disk!

Pipes - "Files That Connect Processes"

Location: kernel/src/fs/pipe.c

A pipe is a special "file" that two processes use to communicate:

cat bigfile.txt | grep "error"
cat process              grep process
    │                        │
    ├─ Write to pipe    ─────┤
    │  (data flows)     ─────┤
    ├─ "line1"              │ Read from pipe
    ├─ "error line"         │ ← grep gets this
    ├─ "line3"              │
    └─ EOF                  │ Done reading

Implementation: kernel/src/fs/pipe.c - Creates a buffer that connects two file descriptors

Filesystem Operations Summary

Creating a File

fd = open("/home/user/newfile.txt", O_CREAT | O_WRONLY, 0644);

Behind the scenes:

  1. Parse path → navigate to /home/user/ directory inode
  2. Call EXT2 driver: "Create new inode"
  3. Inode allocated → added to inode table on disk
  4. Add directory entry: "newfile.txt" → inode #12345
  5. Write inode and directory entry changes back to disk
  6. Return fd pointing to new inode

Deleting a File

unlink("/home/user/oldfile.txt");

Behind the scenes:

  1. Parse path → find inode #54321
  2. Remove directory entry "oldfile.txt"
  3. Mark inode as free in inode bitmap
  4. If no other hard links exist, free its data blocks
  5. Mark data blocks as free in block bitmap
  6. Write changes back to disk

Directory Operations

mkdir("/home/user/newfolder", 0755);

Behind the scenes:

  1. Create new inode (type=directory)
  2. Create directory entries within it:
    • "." → self inode
    • ".." → parent inode
  3. Add entry in parent directory
  4. Write to disk

Key Files to Explore

  • kernel/src/fs/vfs.c - VFS core operations
  • kernel/src/fs/ext2.c - EXT2 filesystem implementation
  • kernel/src/fs/namei.c - Path name resolution
  • kernel/src/fs/procfs.c - Virtual procfs
  • kernel/src/fs/pipe.c - Pipes for IPC
  • kernel/inc/fs/vfs.h - VFS data structures
  • kernel/inc/fs/ext2.h - EXT2 on-disk format
  • lib/inc/fcntl.h - File control flags
  • lib/inc/sys/stat.h - File metadata structures

Further Reading


Key takeaway: Files are just organized data on disk. The VFS abstracts the details, letting kernels support many filesystems. EXT2 shows how one filesystem organizes inodes and blocks to store everything efficiently!

Directory Listing (getdents)

Clone this wiki locally