Skip to content

File Systems

Enrico Fraccaroli (Galfurian) edited this page Jan 29, 2026 · 9 revisions

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 a vfs_file_descriptor_t:
┌─────────────────────────────────┐
│ File Descriptor (fd = 3)        │
├─────────────────────────────────┤
│ • Flags mask: O_RDONLY          │
│ • Points to: vfs_file_t         │
└─────────────────────────────────┘
       ↓
    vfs_file_t (name, flags, f_pos, operations)
       ↓
    Filesystem driver (EXT2 / procfs)

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/read_write.c and 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.

Examples of procfs entries in MentOS:

- `/proc/uptime`, `/proc/version`, `/proc/meminfo`, `/proc/stat` (see `kernel/src/io/proc_system.c`)
- `/proc/ipc/msg`, `/proc/ipc/sem`, `/proc/ipc/shm` (see `kernel/src/io/proc_ipc.c`)
- `/proc/feedback` (scheduler feedback, see `kernel/src/io/proc_feedback.c`)
- `/proc/video` (video info, see `kernel/src/io/proc_video.c`)
- `/proc/<pid>/cmdline`, `/proc/<pid>/stat` (see `kernel/src/io/proc_running.c`)

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

  1. Kernel sees: "This is a procfs file"
  2. procfs routes the read to the proc-running callbacks
  3. __procr_do_stat() formats the process data
  4. User reads the formatted stat line

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

Hands-On Exercises

Exercise 1: Explore the File System

Goal: Understand the MentOS filesystem structure.

Steps:

  1. Boot MentOS:

    make qemu
  2. Explore directories:

    /bin/ls /              # Root directory
    /bin/ls /bin           # Binaries
    /bin/ls /tmp           # Temp files
    /bin/ls -l /           # Long listing with metadata
  3. Check file properties:

    /bin/stat /bin/ls      # File metadata
    /bin/stat /            # Directory info
  4. What do you notice?

    • File sizes
    • Timestamps
    • Permissions (755, 644, etc.)
    • Hard link counts

Exercise 2: Create and Manipulate Files

Goal: Write a program that creates, reads, and modifies files.

Program - userspace/bin/file_demo.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main() {
    printf("=== File Operations Demo ===\n\n");
    
    // 1. CREATE and WRITE
    printf("1. Creating file...\n");
    int fd = open("/tmp/test.txt", O_CREAT | O_WRONLY, 0644);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    
    const char *text = "Hello from MentOS!\n";
    write(fd, text, strlen(text));
    close(fd);
    printf("   File created: /tmp/test.txt\n");
    
    // 2. READ
    printf("\n2. Reading file...\n");
    fd = open("/tmp/test.txt", O_RDONLY);
    char buffer[100];
    int n = read(fd, buffer, 100);
    printf("   Read %d bytes: %s", n, buffer);
    close(fd);
    
    // 3. APPEND
    printf("\n3. Appending to file...\n");
    fd = open("/tmp/test.txt", O_APPEND | O_WRONLY);
    const char *more = "This line was appended!\n";
    write(fd, more, strlen(more));
    close(fd);
    
    // 4. READ AGAIN
    printf("\n4. Reading file again...\n");
    fd = open("/tmp/test.txt", O_RDONLY);
    n = read(fd, buffer, 100);
    printf("   Full file content:\n%s", buffer);
    close(fd);
    
    // 5. COPY
    printf("\n5. Copying file...\n");
    fd = open("/tmp/test.txt", O_RDONLY);
    int fd2 = open("/tmp/test_copy.txt", O_CREAT | O_WRONLY, 0644);
    
    while ((n = read(fd, buffer, 50)) > 0) {
        write(fd2, buffer, n);
    }
    close(fd);
    close(fd2);
    printf("   File copied to /tmp/test_copy.txt\n");
    
    return 0;
}

Build and Run:

# Add to userspace/bin/CMakeLists.txt
# In MentOS:
/bin/file_demo
/bin/ls -l /tmp/test*   # Verify files created
/bin/cat /tmp/test_copy.txt

Exercise 3: Work with Directories

Goal: Create directories and list their contents.

Program - userspace/bin/dir_ops.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>

int main() {
    printf("=== Directory Operations ===\n\n");
    
    // 1. CREATE DIRECTORY
    printf("1. Creating directory /tmp/mydir...\n");
    mkdir("/tmp/mydir", 0755);
    
    // 2. CREATE FILES IN IT
    printf("2. Creating files in directory...\n");
    char path[100];
    for (int i = 1; i <= 5; i++) {
        snprintf(path, 100, "/tmp/mydir/file%d.txt", i);
        int fd = open(path, O_CREAT | O_WRONLY, 0644);
        write(fd, "content", 7);
        close(fd);
        printf("   Created %s\n", path);
    }
    
    // 3. LIST FILES
    printf("\n3. Directory contents:\n");
    system("/bin/ls -l /tmp/mydir");
    
    // 4. CREATE SUBDIRECTORY
    printf("\n4. Creating subdirectory...\n");
    mkdir("/tmp/mydir/subdir", 0755);
    printf("   Created /tmp/mydir/subdir\n");
    
    // 5. FULL TREE
    printf("\n5. Full tree structure:\n");
    system("/bin/ls -lR /tmp/mydir");
    
    return 0;
}

Test:

/bin/dir_ops
/bin/ls -lR /tmp/mydir

Exercise 4: Understand Inodes and Hard Links

Goal: Learn about inodes and how hard links work.

Program - userspace/bin/hardlink_demo.c:

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    printf("=== Hard Links and Inodes ===\n\n");
    
    // 1. CREATE FILE
    printf("1. Creating original file...\n");
    int fd = open("/tmp/original.txt", O_CREAT | O_WRONLY, 0644);
    write(fd, "Original data", 13);
    close(fd);
    
    // Check inode
    struct stat sb;
    stat("/tmp/original.txt", &sb);
    printf("   Inode: %lu\n", (unsigned long)sb.st_ino);
    printf("   Link count: %lu\n", (unsigned long)sb.st_nlink);
    printf("   Size: %lu bytes\n", (unsigned long)sb.st_size);
    
    // 2. CREATE HARD LINK
    printf("\n2. Creating hard link...\n");
    link("/tmp/original.txt", "/tmp/hardlink.txt");
    
    stat("/tmp/original.txt", &sb);
    printf("   Original link count: %lu\n", (unsigned long)sb.st_nlink);
    
    stat("/tmp/hardlink.txt", &sb);
    printf("   Hard link inode: %lu\n", (unsigned long)sb.st_ino);
    printf("   Hard link count: %lu\n", (unsigned long)sb.st_nlink);
    
    // 3. MODIFY THROUGH LINK
    printf("\n3. Modifying through hard link...\n");
    fd = open("/tmp/hardlink.txt", O_APPEND | O_WRONLY);
    write(fd, " - Modified!", 11);
    close(fd);
    
    // 4. READ ORIGINAL
    printf("\n4. Reading original file:\n");
    char buf[100];
    fd = open("/tmp/original.txt", O_RDONLY);
    int n = read(fd, buf, 100);
    printf("   %.*s\n", n, buf);
    close(fd);
    
    // 5. DELETE LINK
    printf("\n5. Deleting hard link...\n");
    unlink("/tmp/hardlink.txt");
    
    stat("/tmp/original.txt", &sb);
    printf("   Final link count: %lu\n", (unsigned long)sb.st_nlink);
    printf("   (Note: inode still exists until all links deleted)\n");
    
    return 0;
}

Test and Observe:

/bin/hardlink_demo
# Note: Both files share same inode!
# Modifying one affects the other
# File only deleted when ALL hard links removed

Exercise 5: Low-Level File Access

Goal: Use system calls to interact with the filesystem directly.

Program - userspace/bin/rawio.c:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main() {
    printf("=== Low-Level File I/O ===\n\n");
    
    // 1. OPEN
    printf("1. Opening file with O_RDWR...\n");
    int fd = open("/tmp/rawtest.txt", O_CREAT | O_RDWR, 0644);
    printf("   File descriptor: %d\n", fd);
    
    // 2. WRITE at specific positions
    printf("\n2. Writing at different positions...\n");
    
    lseek(fd, 0, SEEK_SET);
    write(fd, "START", 5);
    printf("   Wrote 'START' at offset 0\n");
    
    lseek(fd, 10, SEEK_SET);
    write(fd, "MIDDLE", 6);
    printf("   Wrote 'MIDDLE' at offset 10\n");
    
    lseek(fd, 20, SEEK_SET);
    write(fd, "END", 3);
    printf("   Wrote 'END' at offset 20\n");
    
    // 3. SEEK and READ
    printf("\n3. Seeking and reading...\n");
    lseek(fd, 0, SEEK_SET);
    
    char buf[100];
    int n = read(fd, buf, 100);
    
    printf("   File content (%d bytes):\n", n);
    for (int i = 0; i < n; i++) {
        if (buf[i] == '\0') {
            printf("[NULL]");
        } else {
            printf("%c", buf[i]);
        }
    }
    printf("\n");
    
    // 4. TRUNCATE
    printf("\n4. Truncating file to 15 bytes...\n");
    ftruncate(fd, 15);
    
    lseek(fd, 0, SEEK_SET);
    n = read(fd, buf, 100);
    printf("   After truncate: %d bytes\n", n);
    
    close(fd);
    return 0;
}

Explore:

  • lseek() for seeking to positions
  • read()/write() at different offsets
  • ftruncate() to change file size
  • File descriptors and offsets

Exercise 6: Traverse EXT2 Filesystem

Advanced Goal: Understand EXT2 structure on disk.

Steps:

  1. Examine EXT2 superblock and group descriptors
  2. Look at kernel/src/fs/ext2.c implementation
  3. Write program to:
    • Read superblock
    • Enumerate block groups
    • Count free inodes and blocks
    • Navigate inode bitmap

Hints:

  • Superblock at block 1
  • Group descriptors follow superblock
  • Each group has inode bitmap and block bitmap
  • Use direct disk I/O through /dev/hda (careful!)

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