File Systems â NTFS, ext4, APFS & ZFS Comparison Guide
In this tutorial, you'll learn about File Systems. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
A file system controls how data is stored, organised, and retrieved on disk. The choice of file system affects performance, reliability, data integrity, and maximum file size â comparing ext4, NTFS, APFS, ZFS, and Btrfs across key dimensions.
What You'll Learn
In this tutorial, you'll learn how modern file systems work: ext4's inode structure and journaling, NTFS Master File Table and B-tree indexing, APFS copy-on-write and snapshots, FAT32/exFAT for portability, Btrfs and ZFS advanced features, the Virtual File System (VFS) abstraction, and hard links vs symbolic links.
Why It Matters
File system corruption means data loss. Understanding how your OS stores files helps you choose the right file system, diagnose disk problems, and recover data. When a database reports "disk full" but there's space â that's likely a file system issue.
Real-World Use
When you take a photo on an iPhone, APFS snapshots the file system instantly. When Windows updates, NTFS transactional NTFS ensures the update either completes or rolls back. Durga Antivirus Pro monitors file system events using inotify to scan new files as they're created.
graph TD
subgraph "VFS (Virtual File System)"
VFS[System Call Interface]
end
subgraph "File System Implementations"
EXT4[ext4]
NTFS[NTFS]
APFS[APFS]
BTRFS[Btrfs]
ZFS[ZFS]
FAT[FAT32/exFAT]
end
subgraph "Block Layer"
BLK[Block Device Layer]
end
subgraph "Storage"
SSD[SSD / NVMe]
HDD[HDD]
end
VFS --> EXT4
VFS --> NTFS
VFS --> APFS
VFS --> BTRFS
VFS --> ZFS
VFS --> FAT
EXT4 --> BLK
NTFS --> BLK
APFS --> BLK
BLK --> SSD
BLK --> HDD
Prerequisites: Python basics. Understanding of Operating Systems fundamentals helps.
ext4 â Extended File System
ext4 is the default file system for most Linux distributions. It uses inodes to store file metadata and extents for block allocation.
Inodes
Each file has an inode containing metadata: permissions, timestamps, owner, size, and pointers to data blocks. The filename is stored separately in the directory entry.
class Ext4Inode:
def __init__(self, inode_num, mode, size, blocks):
self.inode_num = inode_num
self.mode = mode
self.size = size
self.blocks = blocks
self.links_count = 1
def __repr__(self):
return f'Inode {self.inode_num}: mode={oct(self.mode)} size={self.size}B blocks={self.blocks}'
class Ext4Directory:
def __init__(self):
self.entries = {}
def create_file(self, name, inode_num, size=0):
self.entries[name] = inode_num
return Ext4Inode(inode_num, 0o100644, size, (size + 4095) // 4096)
def ls(self):
for name, inode in sorted(self.entries.items(), key=lambda x: x[0]):
print(f'{name:20s} â inode {inode}')
root = Ext4Directory()
inodes = {}
inodes[1] = root.create_file('readme.txt', 1, 2048)
inodes[2] = root.create_file('script.sh', 2, 512)
inodes[3] = root.create_file('data.csv', 3, 16384)
root.ls()
print(f'\n{inodes[3]}')
Expected output:
data.csv â inode 3
readme.txt â inode 1
script.sh â inode 2
Inode 3: mode=0o100644 size=16384B blocks=4
Journaling
ext4 uses journaling to prevent corruption after crashes. Ordered mode (default) journals metadata and writes data first.
NTFS â New Technology File System
NTFS is the primary file system for Windows. It uses a Master File Table (MFT) â a B-tree of file records. Small files fit entirely within the MFT record (resident data).
class NTFSMFTEntry:
def __init__(self, record_num, filename, is_directory=False):
self.record_num = record_num
self.filename = filename
self.is_directory = is_directory
self.attributes = {}
def add_attribute(self, attr_type, data, resident=True):
self.attributes[attr_type] = {'type': attr_type, 'resident': resident, 'size': len(data) if isinstance(data, bytes) else data}
def __repr__(self):
attrs = ', '.join(self.attributes.keys())
return f'MFT Entry {self.record_num}: {self.filename} [{attrs}]'
mft = {}
mft[0] = NTFSMFTEntry(0, '$MFT')
mft[0].add_attribute('STANDARD_INFORMATION', '...')
mft[0].add_attribute('FILE_NAME', '$MFT')
mft[5] = NTFSMFTEntry(5, 'document.docx')
mft[5].add_attribute('DATA', 1024 * 1024, resident=False)
mft[6] = NTFSMFTEntry(6, 'notes.txt')
mft[6].add_attribute('DATA', b'Hello, NTFS!', resident=True)
for entry in mft.values():
print(entry)
Expected output:
MFT Entry 0: $MFT [STANDARD_INFORMATION, FILE_NAME]
MFT Entry 5: document.docx [DATA]
MFT Entry 6: notes.txt [DATA]
APFS â Apple File System
APFS features copy-on-write (CoW): when data is modified, new data is written to a new block. The old block is freed only after all references are removed.
class APFSBlock:
def __init__(self, block_id, data=b''):
self.block_id = block_id
self.data = data
self.ref_count = 1
class APFSFile:
def __init__(self, name, data_blocks):
self.name = name
self.data_blocks = data_blocks
def modify(self, offset, new_data, block_allocator):
old_id = self.data_blocks[offset]
new_id = block_allocator.allocate(new_data)
self.data_blocks[offset] = new_id
block_allocator.decrement_ref(old_id)
return old_id
class APFSBlockAllocator:
def __init__(self):
self.blocks = {}
self.next_id = 0
def allocate(self, data):
block = APFSBlock(self.next_id, data)
self.blocks[self.next_id] = block
self.next_id += 1
return block.block_id
def decrement_ref(self, block_id):
self.blocks[block_id].ref_count -= 1
if self.blocks[block_id].ref_count == 0:
print(f' Freed block {block_id}')
allocator = APFSBlockAllocator()
file = APFSFile('document.txt', [
allocator.allocate(b'Hello World!'),
allocator.allocate(b'Second block'),
])
print(f'Before: blocks {file.data_blocks}')
old_block = file.modify(0, b'Modified data', allocator)
print(f'After: blocks {file.data_blocks}')
print(f'Old block {old_block} preserved (snapshot reference)')
Expected output:
Before: blocks [0, 1]
After: blocks [2, 1]
Old block 0 preserved (snapshot reference)
Btrfs and ZFS
| Feature | Btrfs | ZFS |
|---|---|---|
| Copy-on-write | Yes | Yes |
| Snapshots | Yes (read/write) | Yes (read/write) |
| Compression | lzo, zstd, zlib | lz4, gzip, zle |
| RAID | 0, 1, 5, 6, 10 | 0, 1, 5, 6, 10, mirror, triple |
| Deduplication | Yes | Yes |
| Checksumming | CRC-32C | Fletcher-4, SHA-256 |
| Max volume size | 16 EiB | 256 ZiB |
VFS â Virtual File System
VFS provides a common interface for all file systems. System calls like open(), read(), write() go through VFS, which dispatches to the specific file system's implementation.
class VFSNode:
def __init__(self, name, is_directory=False):
self.name = name
self.is_directory = is_directory
self.children = {}
self.data = b''
def open(self, path):
parts = path.strip('/').split('/')
node = self
for part in parts:
if part in node.children:
node = node.children[part]
else:
raise FileNotFoundError(path)
return node
def read(self):
if self.is_directory:
return list(self.children.keys())
return self.data
def write(self, data):
self.data = data
root = VFSNode('/', is_directory=True)
home = VFSNode('home', is_directory=True)
root.children['home'] = home
readme = VFSNode('readme.txt')
readme.write(b'Hello from VFS!')
home.children['readme.txt'] = readme
node = root.open('/home/readme.txt')
print(f'Opened: /home/{node.name}')
print(f'Content: {node.read()}')
Expected output:
Opened: /home/readme.txt
Content: b'Hello from VFS!'
Hard Links vs Symbolic Links
| Feature | Hard Link | Symbolic Link |
|---|---|---|
| Points to | Inode | Path |
| Across file systems | No | Yes |
| Directory links | No (usually) | Yes |
| Orphan if target deleted | Data still accessible | Broken link |
class SimulatedFS:
def __init__(self):
self.inodes = {}
self.dir_entries = {}
def create_file(self, path, data):
inode_num = len(self.inodes) + 1
self.inodes[inode_num] = {'data': data, 'links': 1}
self.dir_entries[path] = inode_num
return inode_num
def hard_link(self, src, dst):
inode = self.dir_entries[src]
self.dir_entries[dst] = inode
self.inodes[inode]['links'] += 1
print(f'Hard link: {dst} â {src} (inode {inode})')
def delete(self, path):
entry = self.dir_entries.get(path)
if isinstance(entry, int):
self.inodes[entry]['links'] -= 1
if self.inodes[entry]['links'] == 0:
del self.inodes[entry]
print(f'Inode {entry} freed')
del self.dir_entries[path]
fs = SimulatedFS()
fs.create_file('/original.txt', b'Hello!')
fs.hard_link('/original.txt', '/hardlink.txt')
print(f'Delete /original.txt')
fs.delete('/original.txt')
print(f'Hard link still works: inode {fs.dir_entries["/hardlink.txt"]}')
Common Mistakes
1. Confusing Inodes and Filenames
An inode stores metadata; the filename is in the directory entry. Multiple filenames (hard links) can point to the same inode.
2. Using FAT32 for Files Larger Than 4GB
FAT32 has a 4GB maximum file size. Use exFAT or NTFS for large files on external drives.
3. Not Considering File System in Database Performance
Databases on ext4 with ordered mode may get better performance than on CoW file systems unless tuned.
4. Running Out of Inodes
A file system with 1M inodes can't create more files even with free space. ext4 reserves enough by default, but small partitions may run out.
5. Ignoring Snapshots on CoW File Systems
ZFS/Btrfs snapshots use space until deleted. Running out of space despite "free" data is often caused by retained snapshots.
Practice Questions
1. What's stored in an ext4 inode vs a directory entry? An inode stores metadata (permissions, timestamps, block pointers). A directory entry maps a filename to an inode number.
2. How does NTFS MFT work? The MFT is a B-tree of file records. Each file has at least one record containing attributes. Small files store data directly in the MFT record (resident data).
3. What is copy-on-write in APFS? When data is modified, APFS writes new data to a new block instead of overwriting. The old block is preserved for snapshots and cloning.
4. What's the difference between VFS and a file system? VFS is the kernel abstraction layer for all file systems. The file system is the specific implementation (ext4, NTFS). VFS provides a common API.
5. Challenge: Implement a simple CoW file system in Python with snapshots. When a snapshot is taken, preserve all referenced data blocks. On modification, allocate new blocks. Free only when no snapshot references a block.
Mini Project: File System Simulator
class SimpleFS:
def __init__(self, block_size=512):
self.block_size = block_size
self.blocks = {}
self.files = {}
self.next_block = 0
def alloc_block(self, data=b''):
bid = self.next_block
self.blocks[bid] = bytearray(data or b'\x00' * self.block_size)
self.next_block += 1
return bid
def write_file(self, name, data):
blocks = []
for i in range(0, len(data), self.block_size):
blocks.append(self.alloc_block(data[i:i+self.block_size]))
self.files[name] = blocks
return len(data)
def read_file(self, name):
data = bytearray()
for bid in self.files.get(name, []):
data.extend(self.blocks[bid][:self.block_size])
return bytes(data).rstrip(b'\x00')
fs = SimpleFS()
fs.write_file("hello.txt", b"Hello File System!")
print(fs.read_file("hello.txt").decode())
FAQ
Related Concepts
What's Next
You now understand file systems! Next, learn about Device Drivers and kernel module programming, then explore OS Security for protection mechanisms.
- Practice daily â Run
stat /etc/passwd,df -i /, andmounton Linux - Build a project â Create a simulated inode-based file system with journaling
- Explore related topics â Check out ZFS snapshots and Replication for backup
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro