File Systems

Learning Objectives

By the end of this reading, you will be able to:

  • Understand the purpose and structure of file systems
  • Explain file concepts, attributes, and operations
  • Describe directory structures and their implementation
  • Understand different file allocation methods (contiguous, linked, indexed)
  • Explain the FAT (File Allocation Table) file system
  • Understand the inode-based file system structure
  • Describe journaling file systems and their benefits
  • Understand free space management techniques
  • Explain file system mounting and disk organization

Table of Contents

  1. Introduction to File Systems
  2. File Concept
  3. File Operations
  4. Directory Structure
  5. File Allocation Methods
  6. FAT File System
  7. Inode-Based File Systems
  8. Journaling File Systems
  9. Free Space Management
  10. File System Implementation
  11. Exercises
  12. Summary

Introduction to File Systems

A file system controls how data is stored and retrieved on storage devices. Without a file system, data would be one large undifferentiated blob with no way to identify where one piece of information stops and another begins.

Purpose of File Systems

  1. Persistent Storage: Data survives system reboots
  2. Organization: Logical structure for storing data
  3. Naming: Human-readable names for data
  4. Protection: Access control and permissions
  5. Reliability: Data integrity and recovery

File System Layers

+-------------------------+
|   Application Layer     |  (Programs using files)
+-------------------------+
|   Logical File System   |  (Directory management, security)
+-------------------------+
| File Organization Module|  (File allocation, free space)
+-------------------------+
|   Basic File System     |  (Generic I/O commands)
+-------------------------+
|    I/O Control Layer    |  (Device drivers)
+-------------------------+
|    Physical Device      |  (Disk hardware)
+-------------------------+

Common File Systems

  • FAT32: Simple, widely compatible (USB drives)
  • NTFS: Windows default, journaling, large file support
  • ext4: Linux default, journaling, efficient
  • HFS+/APFS: macOS file systems
  • ZFS: Advanced features, snapshots, compression

File Concept

A file is a named collection of related information recorded on secondary storage.

File Attributes (Metadata)

File: document.txt
+------------------+------------------+
| Attribute        | Value            |
+------------------+------------------+
| Name             | document.txt     |
| Type             | .txt (text)      |
| Size             | 4096 bytes       |
| Created          | 2025-01-15 10:30 |
| Modified         | 2025-01-20 14:22 |
| Accessed         | 2025-01-21 09:15 |
| Owner            | user (UID: 1000) |
| Permissions      | rw-r--r--        |
| Location         | Block 5000       |
| Inode Number     | 12345            |
+------------------+------------------+

File Types

  1. Regular Files: Contains user data

    • Text files (ASCII, UTF-8)
    • Binary files (executables, images)
  2. Directory Files: Contains file names and pointers

  3. Special Files: Represent devices

    • Character special files (terminals, serial ports)
    • Block special files (disks, USB drives)
  4. Symbolic Links: Pointers to other files

File Structure

No Structure (Byte Sequence):
+--+--+--+--+--+--+--+--+
|01|23|45|67|89|AB|CD|EF|  Raw bytes
+--+--+--+--+--+--+--+--+

Record Structure:
+--------+--------+--------+
| Record | Record | Record |  Fixed-size records
|   1    |   2    |   3    |
+--------+--------+--------+

Tree Structure:
     Root
    /    \
   Key  Value
  /  \
Sub  Sub

Examples:
- Byte sequence: Most modern OS (Unix, Windows)
- Record: Legacy systems, databases
- Tree: XML, JSON files (application level)

File Operations

Operating systems provide system calls for file manipulation.

Basic File Operations

  1. Create: Make new file
  2. Open: Prepare file for access
  3. Read: Read data from file
  4. Write: Write data to file
  5. Seek: Reposition file pointer
  6. Delete: Remove file
  7. Truncate: Erase contents but keep attributes
  8. Close: Release resources

File Operations in C

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

int main() {
    int fd;
    char buffer[100];
    ssize_t bytes_read, bytes_written;

    // Create/Open file for writing
    fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    // Write to file
    const char *text = "Hello, File System!";
    bytes_written = write(fd, text, strlen(text));
    printf("Wrote %zd bytes\n", bytes_written);

    // Close file
    close(fd);

    // Open file for reading
    fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    // Read from file
    bytes_read = read(fd, buffer, sizeof(buffer) - 1);
    buffer[bytes_read] = '\0';  // Null terminate
    printf("Read: %s\n", buffer);

    // Close file
    close(fd);

    return 0;
}

File Operations in Python

# Write to file
with open('test.txt', 'w') as f:
    f.write("Hello, File System!\n")
    f.write("Second line\n")

# Read entire file
with open('test.txt', 'r') as f:
    content = f.read()
    print(content)

# Read line by line
with open('test.txt', 'r') as f:
    for line in f:
        print(line.strip())

# Append to file
with open('test.txt', 'a') as f:
    f.write("Appended line\n")

# Binary file operations
with open('image.png', 'rb') as f:
    data = f.read(100)  # Read first 100 bytes
    print(f"Read {len(data)} bytes")

Open File Table

The OS maintains a table of open files:

System-Wide Open File Table:
+-------+--------+----------+---------+
| Index | Inode  | Count    | Offset  |
+-------+--------+----------+---------+
|   0   | 12345  |    2     |   150   |  (2 processes have this open)
|   1   | 67890  |    1     |    0    |
|   2   | 11111  |    3     |  1024   |
+-------+--------+----------+---------+

Per-Process Open File Table:
Process 1:
+----+------------------+
| FD | System Table Ptr |
+----+------------------+
|  0 | stdin            |
|  1 | stdout           |
|  2 | stderr           |
|  3 | -> Entry 0       |
|  4 | -> Entry 2       |
+----+------------------+

File Descriptor (FD): Integer identifying open file

Directory Structure

Directories organize files into a hierarchical structure.

Single-Level Directory

All files in one directory (early systems).

+-------------------------------------+
| file1 | file2 | file3 | file4 | ... |
+-------------------------------------+

Problems:
- Naming conflicts
- No organization
- Difficult to find files

Two-Level Directory

Separate directory for each user.

          Root
           |
    +------+------+
    |             |
  User1         User2
    |             |
+---+---+     +---+---+
| Files |     | Files |
+-------+     +-------+

Better, but:
- Still limited organization
- Can't group files logically

Tree-Structured Directory

Hierarchical directory structure (modern systems).

                    / (root)
                    |
        +-----------+-----------+
        |           |           |
      home         etc         usr
        |           |           |
    +---+---+   config       +--+--+
    |       |   files        |     |
  alice   bob              bin   lib
    |                        |
 +--+--+              +------+------+
 |  |  |              |      |      |
doc pics code       gcc    python  ls

Directory Operations

#include <dirent.h>
#include <stdio.h>

int main() {
    DIR *dir;
    struct dirent *entry;

    // Open directory
    dir = opendir(".");
    if (dir == NULL) {
        perror("opendir");
        return 1;
    }

    // Read directory entries
    printf("Contents of current directory:\n");
    while ((entry = readdir(dir)) != NULL) {
        printf("  %s\n", entry->d_name);
    }

    // Close directory
    closedir(dir);

    return 0;
}

Python Directory Operations

import os

# List directory contents
print("Current directory:", os.getcwd())
print("\nContents:")
for item in os.listdir('.'):
    print(f"  {item}")

# Walk directory tree
print("\nDirectory tree:")
for root, dirs, files in os.walk('.'):
    level = root.replace('.', '').count(os.sep)
    indent = ' ' * 2 * level
    print(f"{indent}{os.path.basename(root)}/")
    subindent = ' ' * 2 * (level + 1)
    for file in files:
        print(f"{subindent}{file}")

# Create and remove directories
os.mkdir('test_dir')
os.makedirs('path/to/nested/dir')  # Create intermediate dirs
os.rmdir('test_dir')

Directory Implementation

Directories are special files containing mappings:

Directory: /home/alice/

Linear List:
+----------------+-------+
| File Name      | Inode |
+----------------+-------+
| .              | 1000  |
| ..             | 100   |
| document.txt   | 1234  |
| image.png      | 5678  |
| script.py      | 9012  |
+----------------+-------+

Hash Table (faster lookup):
Hash(name) -> (name, inode) list

Tree Structure (sorted):
      document.txt
      /          \
  image.png   script.py

File Allocation Methods

How disk blocks are allocated to files.

1. Contiguous Allocation

Each file occupies contiguous blocks on disk.

Disk Blocks:
+---+---+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+---+---+---+---+---+---+---+---+---+---+
      |File A |   |File B |   |File C |

Directory Entry:
+-------+-------+--------+
| Name  | Start | Length |
+-------+-------+--------+
| A     |   1   |   2    |
| B     |   4   |   2    |
| C     |   7   |   2    |
+-------+-------+--------+

Advantages:

  • Simple to implement
  • Excellent read performance (sequential access)
  • Random access is easy

Disadvantages:

  • External fragmentation
  • Difficult to grow files
  • Need to know file size in advance

2. Linked Allocation

Each block contains pointer to next block.

File A: Start at block 1

Directory Entry:
+-------+-------+
| Name  | Start |
+-------+-------+
| A     |   1   |
+-------+-------+

Disk Blocks:
Block 1: [Data | ptr=4]  →  Block 4: [Data | ptr=7]  →  Block 7: [Data | NULL]

Visual:
+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---+---+---+---+---+---+---+---+
      ↓           ↓           ↓
    File A      File A      File A
    (next=4)    (next=7)    (end)

Advantages:

  • No external fragmentation
  • Files can grow easily
  • No need to know file size in advance

Disadvantages:

  • Poor random access (must follow chain)
  • Reliability (if one pointer lost, rest of file inaccessible)
  • Overhead of pointers in each block

3. Indexed Allocation

Each file has an index block containing pointers to data blocks.

File A: Index block at 5

Directory Entry:
+-------+-------------+
| Name  | Index Block |
+-------+-------------+
| A     |      5      |
+-------+-------------+

Index Block 5:
+---+
| 1 |  →  Data block 1
| 4 |  →  Data block 4
| 7 |  →  Data block 7
| 9 |  →  Data block 9
| - |
+---+

Disk:
+---+---+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+---+---+---+---+---+---+---+---+---+---+
      ↑       ↑   Index   ↑       ↑
   File A  File A  Block  File A  File A

Advantages:

  • Good random access
  • No external fragmentation
  • Can grow files (add more index blocks)

Disadvantages:

  • Index block overhead
  • For small files, wasted space in index block

Multilevel Index

For large files, use multiple levels of index blocks.

File Structure:
+---------------+
| Inode         |
+---------------+
| Direct [0]    | →  Data block
| Direct [1]    | →  Data block
| ...           |
| Direct [11]   | →  Data block
| Single Indirect| →  Index block  →  Data blocks
| Double Indirect| →  Index  →  Index blocks  →  Data blocks
| Triple Indirect| →  Index  →  Index  →  Index blocks  →  Data
+---------------+

Example with 4KB blocks, 4-byte pointers:
- Each index block holds 1024 pointers
- Direct: 12 blocks = 48 KB
- Single indirect: 1024 blocks = 4 MB
- Double indirect: 1024 × 1024 blocks = 4 GB
- Triple indirect: 1024 × 1024 × 1024 blocks = 4 TB

FAT File System

FAT (File Allocation Table) is a simple file system used on removable media.

FAT Structure

Disk Layout:
+---------------+
| Boot Sector   |
+---------------+
| FAT 1         |  (Primary)
+---------------+
| FAT 2         |  (Backup)
+---------------+
| Root Directory|
+---------------+
| Data Area     |
|  (Clusters)   |
+---------------+

File Allocation Table

The FAT is an array where each entry represents a cluster.

FAT Table:
+-------+-------+
| Index | Value |
+-------+-------+
|   0   | 0xF8  |  (Media descriptor)
|   1   | 0xFF  |  (Reserved)
|   2   |   5   |  (Next cluster of file)
|   3   | 0xFF  |  (End of chain)
|   4   |   0   |  (Free cluster)
|   5   |   8   |  (Next cluster)
|   6   |   7   |  (Next cluster)
|   7   | 0xFF  |  (End of chain)
|   8   | 0xFF  |  (End of chain)
+-------+-------+

File "document.txt" starts at cluster 2:
Cluster chain: 2 → 5 → 8 (end)

Directory Entry (FAT)

32-byte Directory Entry:
+----------------+--------+
| Offset | Field         |
+--------+---------------+
|   0    | Name (8)      | document
|   8    | Extension (3) | txt
|  11    | Attributes    | 0x20 (Archive)
|  12    | Reserved      |
|  14    | Create Time   |
|  16    | Create Date   |
|  18    | Access Date   |
|  20    | EA-Index      |
|  22    | Modified Time |
|  24    | Modified Date |
|  26    | First Cluster | 2
|  28    | File Size     | 12288
+--------+---------------+

Attributes:
0x01 - Read Only
0x02 - Hidden
0x04 - System
0x08 - Volume Label
0x10 - Directory
0x20 - Archive

FAT Variants

  • FAT12: 12-bit cluster numbers (max 4084 clusters)
  • FAT16: 16-bit cluster numbers (max 65,524 clusters)
  • FAT32: 28-bit cluster numbers (max 268M clusters)

FAT Example Implementation

class FATFileSystem:
    def __init__(self, num_clusters):
        self.fat = [0] * num_clusters
        self.fat[0] = 0xF8  # Media descriptor
        self.fat[1] = 0xFF  # Reserved
        self.clusters = {}  # Cluster data

    def allocate_file(self, name, num_clusters):
        """Allocate clusters for a new file"""
        # Find free clusters
        free_clusters = [i for i in range(2, len(self.fat))
                        if self.fat[i] == 0]

        if len(free_clusters) < num_clusters:
            return None  # Not enough space

        # Take first num_clusters
        allocated = free_clusters[:num_clusters]

        # Build chain in FAT
        for i in range(len(allocated) - 1):
            self.fat[allocated[i]] = allocated[i + 1]
        self.fat[allocated[-1]] = 0xFF  # End of chain

        return allocated[0]  # Return first cluster

    def read_file(self, start_cluster):
        """Read all clusters of a file"""
        clusters = []
        current = start_cluster

        while current != 0xFF and current != 0:
            clusters.append(current)
            current = self.fat[current]

        return clusters

    def delete_file(self, start_cluster):
        """Delete file and free its clusters"""
        current = start_cluster

        while current != 0xFF and current != 0:
            next_cluster = self.fat[current]
            self.fat[current] = 0  # Mark as free
            current = next_cluster

# Example usage
fs = FATFileSystem(20)

# Allocate file needing 3 clusters
start = fs.allocate_file("test.txt", 3)
print(f"File allocated starting at cluster {start}")
print(f"Cluster chain: {fs.read_file(start)}")

# Delete file
fs.delete_file(start)
print("File deleted")

Inode-Based File Systems

Inodes (Index Nodes) are data structures used in Unix-like file systems (ext2/3/4, XFS, etc.).

Inode Structure

Inode (128 bytes in ext4):
+------------------+------------------+
| Field            | Size             |
+------------------+------------------+
| Mode             | 2 bytes          | (File type + permissions)
| Owner            | 2 bytes          | (UID)
| Size             | 8 bytes          | (File size)
| Access Time      | 4 bytes          |
| Creation Time    | 4 bytes          |
| Modification Time| 4 bytes          |
| Deletion Time    | 4 bytes          |
| Group            | 2 bytes          | (GID)
| Link Count       | 2 bytes          | (Hard links)
| Block Count      | 4 bytes          | (512-byte blocks)
| Direct Blocks[12]| 48 bytes         | (Pointers to data blocks)
| Indirect Block   | 4 bytes          |
| Double Indirect  | 4 bytes          |
| Triple Indirect  | 4 bytes          |
| Generation       | 4 bytes          |
| File ACL         | 4 bytes          |
| Directory ACL    | 4 bytes          |
| ...              | ...              |
+------------------+------------------+

Inode Addressing

Inode:
+---------------+
| Direct [0]    | →  Data block 1000
| Direct [1]    | →  Data block 1001
| Direct [2]    | →  Data block 1002
| ...           |
| Direct [11]   | →  Data block 1011
+---------------+
| Indirect      | →  Index Block 2000
+---------------+     +----+
| Double Indirect| →  | ...| →  Index blocks  →  Data
+---------------+     +----+
| Triple Indirect| →  Index  →  Index  →  Index  →  Data
+---------------+

For 4KB blocks with 4-byte pointers (1024 pointers per block):
- 12 direct blocks: 12 × 4KB = 48 KB
- 1 indirect: 1024 × 4KB = 4 MB
- 1 double indirect: 1024 × 1024 × 4KB = 4 GB
- 1 triple indirect: 1024³ × 4KB = 4 TB
Maximum file size: ~4 TB

Directory in Inode System

Directory is a special file containing:
+--------+----------+
| Inode  | Filename |
+--------+----------+
| 12345  | .        |  (This directory)
| 100    | ..       |  (Parent directory)
| 12346  | file1.txt|
| 12347  | file2.c  |
| 12348  | subdir   |
+--------+----------+

Directory data blocks:
+------+-------+---------+----------+
| Inode| RecLen| NameLen | Name     |
+------+-------+---------+----------+
| 12345|  12   |   1     | .        |
| 100  |  12   |   2     | ..       |
| 12346|  20   |   9     | file1.txt|
| 12347|  16   |   7     | file2.c  |
| 12348|  16   |   6     | subdir   |
+------+-------+---------+----------+

Hard Link: Multiple directory entries point to same inode

Directory A:          Directory B:
+-------+-------+     +-------+-------+
| inode | name  |     | inode | name  |
+-------+-------+     +-------+-------+
| 1234  | file1 |     | 1234  | file2 |
+-------+-------+     +-------+-------+
         ↓                     ↓
         +---------------------+
                   |
              Inode 1234
           (link count = 2)
                   |
              Data Blocks

Deleting file1 doesn't delete data (link count becomes 1)
Only when link count reaches 0 is data deleted

Symbolic Link: Special file containing path to another file

Directory:
+-------+--------+
| inode | name   |
+-------+--------+
| 1234  | file1  |
| 5678  | link1  | (Symbolic link)
+-------+--------+
    ↓         ↓
Inode 1234   Inode 5678 (type = symlink)
    ↓         ↓
Data blocks  Data: "/path/to/file1"

If file1 deleted, link1 becomes broken (dangling)

Inode Simulator

class Inode:
    def __init__(self, inode_num, file_type, size):
        self.inode_num = inode_num
        self.file_type = file_type  # 'f' or 'd'
        self.size = size
        self.link_count = 1
        self.direct_blocks = [None] * 12
        self.indirect = None
        self.double_indirect = None
        self.triple_indirect = None

    def allocate_blocks(self, block_allocator, num_blocks):
        """Allocate data blocks to this inode"""
        allocated = 0

        # Allocate direct blocks first
        for i in range(12):
            if allocated >= num_blocks:
                break
            self.direct_blocks[i] = block_allocator.allocate()
            allocated += 1

        # TODO: Handle indirect blocks for larger files
        return allocated

class Directory:
    def __init__(self, inode):
        self.inode = inode
        self.entries = {}  # name -> inode_num

    def add_entry(self, name, inode_num):
        self.entries[name] = inode_num

    def lookup(self, name):
        return self.entries.get(name)

class SimpleInodeFS:
    def __init__(self):
        self.inodes = {}
        self.next_inode = 1
        self.block_allocator = BlockAllocator(1000)

    def create_file(self, name, size):
        inode_num = self.next_inode
        self.next_inode += 1

        inode = Inode(inode_num, 'f', size)
        num_blocks = (size + 4095) // 4096  # Ceiling division
        inode.allocate_blocks(self.block_allocator, num_blocks)

        self.inodes[inode_num] = inode
        return inode_num

class BlockAllocator:
    def __init__(self, num_blocks):
        self.free_blocks = list(range(num_blocks))

    def allocate(self):
        if self.free_blocks:
            return self.free_blocks.pop(0)
        return None

# Example
fs = SimpleInodeFS()
inode_num = fs.create_file("test.txt", 8192)
inode = fs.inodes[inode_num]
print(f"Created file with inode {inode_num}")
print(f"Direct blocks: {inode.direct_blocks[:3]}")

Journaling File Systems

Journaling provides reliability by logging changes before committing them.

The Problem: Crash Consistency

Operation: Delete file
Steps:
1. Remove directory entry
2. Mark inode as free
3. Mark data blocks as free

If crash occurs between steps:
- After step 1: Inode and blocks leaked (space wasted)
- After step 2: Blocks still marked used (space wasted)
- Inconsistent file system state

Journaling Solution

Write changes to a log (journal) first, then apply to file system.

Journal:
+------------------+
| TxBegin          |
| Update directory |
| Update inode     |
| Update bitmap    |
| TxEnd            |
+------------------+
        ↓
  After TxEnd written:
        ↓
Apply changes to
actual file system
        ↓
   Mark journal
transaction complete

Journal Workflow

1. Journal Write:
   Write intended changes to journal
   +--------+
   | Begin  |
   | Op 1   |
   | Op 2   |
   | Commit |
   +--------+

2. Checkpoint:
   Apply changes to actual file system locations
   Directory → Updated
   Inode → Updated
   Bitmap → Updated

3. Free:
   Mark journal space as free
   +--------+
   |  Free  |
   +--------+

If crash occurs:
- Before commit: Ignore journal entry (do nothing)
- After commit: Replay journal on reboot (redo changes)

Types of Journaling

  1. Data Journaling: Log both metadata and data

    • Most safe
    • Slower (everything written twice)
  2. Metadata Journaling: Log only metadata

    • Faster
    • Data may be inconsistent
  3. Ordered Journaling: Write data first, then journal metadata

    • Good balance
    • Used by ext3/ext4 (default)

Journaling Example

class Journal:
    def __init__(self):
        self.transactions = []
        self.current_tx = None

    def begin_transaction(self, tx_id):
        """Start new transaction"""
        self.current_tx = {
            'id': tx_id,
            'operations': [],
            'committed': False
        }

    def log_operation(self, op_type, data):
        """Add operation to current transaction"""
        if self.current_tx:
            self.current_tx['operations'].append({
                'type': op_type,
                'data': data
            })

    def commit_transaction(self):
        """Commit current transaction"""
        if self.current_tx:
            self.current_tx['committed'] = True
            self.transactions.append(self.current_tx)
            print(f"Transaction {self.current_tx['id']} committed")
            self.current_tx = None

    def replay(self, file_system):
        """Replay committed transactions after crash"""
        print("Replaying journal...")
        for tx in self.transactions:
            if tx['committed']:
                print(f"  Replaying transaction {tx['id']}")
                for op in tx['operations']:
                    file_system.apply_operation(op)

class FileSystem:
    def __init__(self):
        self.files = {}
        self.journal = Journal()

    def delete_file(self, filename):
        """Delete file with journaling"""
        tx_id = len(self.journal.transactions) + 1

        # Begin transaction
        self.journal.begin_transaction(tx_id)

        # Log all operations
        self.journal.log_operation('remove_dir_entry', filename)
        self.journal.log_operation('free_inode', self.files[filename]['inode'])
        self.journal.log_operation('free_blocks', self.files[filename]['blocks'])

        # Commit transaction
        self.journal.commit_transaction()

        # Now apply changes
        self.apply_delete(filename)

    def apply_delete(self, filename):
        """Actually delete the file"""
        del self.files[filename]
        print(f"File {filename} deleted")

    def apply_operation(self, op):
        """Apply a journaled operation"""
        print(f"    Applying {op['type']}: {op['data']}")

# Example
fs = FileSystem()
fs.files['test.txt'] = {'inode': 100, 'blocks': [1, 2, 3]}
fs.delete_file('test.txt')

Free Space Management

Track which disk blocks are free for allocation.

1. Bitmap (Bit Vector)

Each block represented by one bit.

Bitmap:
Block:  0  1  2  3  4  5  6  7  8  9
Bit:   [1][0][1][1][0][0][1][1][0][1]
        ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑
        Used Free Used Used Free Free...

1 = Used
0 = Free

For 1TB disk with 4KB blocks:
- 256M blocks
- 256M bits = 32 MB bitmap

2. Linked List

Link all free blocks together.

Free List Head → Block 5 → Block 9 → Block 15 → NULL
                  [next]    [next]    [next]

Each free block stores pointer to next free block

3. Grouping

Store addresses of free blocks in first free block.

Block 100:
+-----+
| 101 | →  Block 101 (free)
| 105 | →  Block 105 (free)
| 200 | →  Block 200: [More free block addresses...]
| ... |
+-----+

4. Counting

Store starting block and count of consecutive free blocks.

Free Space List:
+-------+-------+
| Start | Count |
+-------+-------+
|  100  |   5   | (Blocks 100-104 are free)
|  200  |   3   | (Blocks 200-202 are free)
|  500  |  10   | (Blocks 500-509 are free)
+-------+-------+

File System Implementation

Disk Layout Example (ext4)

Disk:
+------------------+  Block 0
| Boot Block       |
+------------------+  Block 1
| Superblock       |  (File system metadata)
+------------------+
| Group Descriptors|
+------------------+
| Block Bitmap     |  (Free block tracking)
+------------------+
| Inode Bitmap     |  (Free inode tracking)
+------------------+
| Inode Table      |  (Array of inodes)
+------------------+
| Data Blocks      |  (File and directory data)
|                  |
|    ...           |
|                  |
+------------------+

Superblock

Contains critical file system information:

Superblock:
+--------------------+--------+
| Field              | Value  |
+--------------------+--------+
| Magic Number       | 0xEF53 | (ext2/3/4 signature)
| Total Inodes       | 100000 |
| Total Blocks       | 400000 |
| Free Blocks        | 300000 |
| Free Inodes        | 95000  |
| Block Size         | 4096   |
| Blocks per Group   | 32768  |
| Inodes per Group   | 8192   |
| Mount Time         | ...    |
| Write Time         | ...    |
| Mount Count        | 15     |
| Max Mount Count    | 30     |
| State              | Clean  |
+--------------------+--------+

File System Mounting

Before Mount:
/
├── home
├── etc
└── mnt (empty)

After mounting /dev/sdb1 at /mnt:
/
├── home
├── etc
└── mnt
    ├── file1.txt
    ├── file2.c
    └── subdir

The file system on /dev/sdb1 is now accessible under /mnt

Mount Example

import os

# Linux mount (requires root)
# os.system("mount /dev/sdb1 /mnt")

# List mounted file systems
with open('/proc/mounts', 'r') as f:
    for line in f:
        device, mount_point, fs_type, options, _, _ = line.split()
        print(f"{device} on {mount_point} type {fs_type}")

Exercises

Basic Exercises

  1. File Attributes

    • List all attributes of a file
    • Use stat command or os.stat() to examine a file
    • Explain what each attribute means
  2. Directory Navigation

    • Write a program to recursively list all files in a directory tree
    • Count total files and directories
    • Calculate total size
  3. File Operations

    • Create a file, write data, read it back
    • Demonstrate seek operations
    • Test file locking mechanisms

Intermediate Exercises

  1. FAT Simulator Implement a simple FAT file system:

    • Initialize FAT with given number of clusters
    • Implement file creation, deletion
    • Implement file read (follow cluster chain)
    • Track free space
  2. Inode File System Create a simplified inode-based system:

    • Implement inode structure with direct blocks
    • Create directory structure
    • Support file creation and deletion
    • Implement hard links (increment link count)
  3. File Allocation Comparison Compare contiguous, linked, and indexed allocation:

    • Implement all three methods
    • Measure access time for sequential reads
    • Measure access time for random reads
    • Analyze fragmentation

Advanced Exercises

  1. Journaling Implementation Build a simple journaling layer:

    • Log operations before applying
    • Implement transaction commit
    • Simulate crash and recovery
    • Replay journal to restore consistency
  2. Free Space Manager Implement multiple free space management techniques:

    • Bitmap
    • Linked list
    • Grouping
    • Compare space overhead and allocation speed
  3. Directory Cache Implement a directory cache (dentry cache):

    • Cache recently accessed paths
    • Use LRU for eviction
    • Measure hit rate improvement
  4. File System Checker Write a program that checks file system consistency:

    • Verify all blocks are accounted for
    • Check for orphaned inodes
    • Detect circular directory links
    • Verify inode link counts

Challenge Exercises

  1. Complete File System Design and implement a full file system:

    • Support files and directories
    • Implement hierarchical directory structure
    • Use inodes with multi-level indexing
    • Include journaling for reliability
    • Provide mount/unmount functionality
    • Support basic permissions
  2. File System Performance Analysis Analyze real file system performance:

    • Measure read/write speeds with different block sizes
    • Test impact of fragmentation
    • Compare different file systems (ext4, XFS, Btrfs)
    • Benchmark metadata operations
    • Create performance visualization

Summary

In this reading, we explored file systems in depth:

Key Takeaways

  1. File System Purpose: Organize persistent storage, provide naming, protection, and reliability

  2. Files: Named collections of data with attributes (metadata) like size, timestamps, permissions

  3. Directories: Special files organizing other files in hierarchical structures

  4. File Allocation Methods:

    • Contiguous: Fast but inflexible
    • Linked: Flexible but slow random access
    • Indexed: Best balance for large files
  5. FAT File System: Simple linked allocation using file allocation table

    • Easy to implement
    • Still used on removable media
    • Limited features
  6. Inode-Based Systems: Unix/Linux approach

    • Separate metadata (inode) from data
    • Efficient multi-level indexing
    • Support for hard and symbolic links
  7. Journaling: Ensures consistency after crashes

    • Log changes before applying
    • Replay journal on recovery
    • Trade-off between safety and performance
  8. Free Space Management: Various techniques

    • Bitmap: Fast, space-efficient for random access
    • Linked list: Space-efficient but slow
    • Hybrid approaches common

Important Concepts to Remember

  • File systems provide abstraction over raw storage blocks
  • Metadata (inodes, directory entries) is as important as data
  • Trade-offs exist between performance, reliability, and complexity
  • Journaling adds overhead but prevents corruption
  • Different allocation methods suit different workloads

Best Practices

  1. Choose Right File System: Match to use case (journaling for OS, FAT for USB)
  2. Regular Backups: File systems can fail
  3. Monitor Fragmentation: Especially on spinning disks
  4. Proper Unmounting: Prevents corruption
  5. Understand Limits: File size, path length, number of files

Looking Ahead

In the next reading, we'll explore Synchronization and Deadlocks, examining how to coordinate concurrent access to shared resources, prevent race conditions, and avoid deadlock situations that can freeze systems.

File systems provide the persistent storage foundation, while synchronization ensures that concurrent processes and threads access this storage (and other shared resources) safely and correctly.


Previous Reading: 03-memory-management.md - Memory Management

Next Reading: 05-synchronization.md - Synchronization and Deadlocks