Filesystem Implementation: Layout, Journaling, COW, and Recovery

On this page

A filesystem mounting successfully does not imply that two business writes performed before a crash are both preserved. Below, we expand on block layout, structural recovery, data verification, and historical recovery separately, avoiding the mistake of summarizing all guarantees under a single feature.

Phenomenon to ExplainReading Location
How does a file offset map to a device block?Section 1: Manual extent calculation
How do journals and COW recover?Sections 2–4: ext4, XFS, Btrfs
What coordination is needed for device tiers?Section 5: bcachefs
How to compare under identical conditions?Section 6: Capability boundaries and computational experiments

1. Extents are contiguous range mappings, not constant-time magic

Assume a block size of 4 KiB. An extent describes "8 logical blocks starting from logical block 100, corresponding to 8 physical blocks starting from physical block 900." When accessing 102 × 4096 + 17:

StepCalculation
Locate relative block within extent102 − 100 = 2
Map to physical block900 + 2 = 902
Retain byte offset within blockByte 17 within block 902

The physical block number still belongs to the filesystem layer; underneath, it may pass through volume management or device mapping.

What extents save is the mapping record for contiguous regions: 8 blocks can be described by a single record instead of one pointer per block. Indirect block designs do not linearly scan N pointers to find the Nth block; they can locate via indexes level by level. The cost of extent tree lookups also depends on tree height and fragmentation, so it cannot be generalized as "indirect blocks O(N), extents O(1)."

The ext4 inode contains a 60-byte region to hold the extent tree root: after a 12-byte header, it can accommodate 4 records of 12 bytes each. When the tree height is zero, these are leaf extents; as it grows, the root holds indexes, so one can no longer say "the first four segments of data always remain directly in the inode." The ee_len field also has encoding rules for initialized/unwritten states, not just an uninterpreted ordinary length. See ext4 inodes and block mapping.

Three types of "not yet written" have different occupancy semantics

StatePhysical Location and Read Semantics
HoleTypically no data blocks yet
Unwritten extentBlocks reserved; reads return zeros; converts to valid state after write
Delayed allocationPhysical location decision deferred during caching phase

Delayed allocation aids aggregation, but subsequent writeback may still fail to allocate; pre-allocation does not guarantee that future metadata updates or COW operations will never run out of space.

Deferred allocation can fail later. Preallocation does not guarantee every future metadata or COW allocation.

Preparing the visual
The same zero read can hide different allocation states

Holes lack data blocks; unwritten extents reserve blocks but read as zeros. A write to a shared extent separates the affected range while the other file retains old contents.

2. ext4: What does the journal protect, and what does ordered add?

JBD2 writes a set of metadata modifications to the journal. After establishing a valid commit record, it can later checkpoint these to their final locations. Crash recovery replays valid transactions, preventing directory, allocation bitmap, inode, and other related modifications from stopping in an inconsistent intermediate state. A commit is the boundary for recoverable journal transactions, while checkpointing is the progression to final locations; the two do not happen at the same moment.

Data ModeRelationship Between User Data and JournalWhat the Application Must Still Handle
data=orderedTypically only metadata is logged; order constraints are applied to the submission of related data and metadataIn-place rewrites and multiple writes do not automatically become application transactions
data=writebackDoes not provide the data-first constraint of the ordered modeContent after recovery may not be the new data expected by the application
data=journalData also passes through the journalApplication commit points must still be defined, and sync results checked

For example, if an application continuously overwrites two regions of the same file, even if the filesystem metadata is completely valid, one should not assume both regions are definitely updated to the new version after a crash just because of data=ordered. Journal consistency concerns the recoverable structure of the filesystem; a single business transaction from the application requires its own atomic update protocol. See ext4 journal documentation.

Delayed allocation and background writeback can expand the set of dirty data in memory that has not yet been persisted, but "crash losing dirty pages" is not unique to delayed allocation. Whether modifications must be retained here should be expressed via interfaces like fsync or synchronous opens, rather than guessing that the background will flush them after a few seconds.

3. XFS: Distributing allocation concurrency across AGs

AG stands for Allocation Group. Each group manages local free space and inode allocation information, reducing the pressure of all allocation requests competing for a single global structure. File data can be distributed across multiple AGs; AGs are not fixed exclusive partitions for each CPU; logs, concurrent modifications to the same inode, and device throughput can still become shared bottlenecks.

XFS uses different indexes to answer different questions: free space indexes organized by address and size are used to find suitable regions; the inode allocation tree describes inode chunks and their usage; the file's own extent mapping describes the mapping from logical offsets to physical regions. One cannot simply write the inode allocation tree as "each leaf stores a complete inode." Small-scale records may also exist in an embedded form, meaning it is not always necessary to traverse an external B+tree for every access.

Shared extents: Independent files can still share content

Filesystems supporting reflink allow two different inodes to share data extents, executing COW when modifying the shared range. Copying is not a hard link: their names and metadata are independent, and rewriting one will not affect the other according to hard link semantics. Both XFS reflink and Btrfs can share and split by range; they cannot be distinguished by "extent-level vs. whole-file-level COW."

Modern XFS self-describing metadata can include checksums, owning object, and location information to detect corruption or misplaced writes; this does not mean ordinary user file data has the same end-to-end checksums. See XFS self-describing metadata design.

4. Btrfs: Updating a reference path

COW snapshots: update the path, retain referenced old blocks Snapshot root R₀ Preserves old version Current root R₁ New committed version Old metadata M₀ New metadata M₁ Old data A Unchanged data B New data A′ Changing A need not copy the whole file; a snapshot reference keeps old A allocated.

The default COW data path writes modifications to a new location and then updates the relevant metadata references. Transaction commits must satisfy write ordering and root validity requirements; it is not "safe because each pointer is written atomically, so any intermediate state is safe." Old extents can only be reclaimed when they are no longer needed by the current tree, snapshots, or other references.

Snapshots typically share existing structures and data first; creation does not require copying the entire subvolume content, but it is not strictly O(1) under all scales and loads. The preserved old versions will gradually consume space; after deleting a file, snapshots may still retain the blocks it occupied. Subvolumes have their own file trees and can be mounted separately, but they are not independent physical volumes and do not automatically form mount namespace isolation. The content of nested subvolumes is not recursively included in parent-child volume snapshots.

COW does not require small modifications to always copy the original complete large extent; in practice, they are split/rewritten according to implementation and compression unit rules; compression, fragmentation, and sharing degree affect write amplification. Enabling configurations like NOCOW results in different data verification and shared write restrictions. Btrfs has a tree log for accelerating fsync, so the statement "COW filesystems do not need any journal" is also incorrect. See Btrfs feature documentation and Subvolume semantics.

Detection, Repair, and Historical Recovery are Three Capabilities

Required CapabilityWhat Must Be PresentWhat Cannot Be Assumed
Detect CorruptionContent and checksum records are comparableDetection does not equal reconstruction capability
Repair CorruptionAnother verifiable correct copyMirrors do not automatically retain history before deletion
Recover HistorySnapshots or historical versions still existSnapshots in the same pool still share the fault domain
Handle Pool-Wide LossBackup in an independent fault domainRecovery process must still be verified

Therefore, "two disks," "has checksums," and "has snapshots" are different conditions; one cannot replace the others.

Btrfs's data checksum algorithm is configurable; not all files should be hardcoded to CRC32C; data and metadata profiles can also differ. "Having two disks" cannot replace checking the actual profile, device errors, and remaining available replicas.

5. bcachefs: Tiered placement and reclamation are also design priorities

bcachefs combines extent mapping, B-trees, journaling, checksums, compression, and multi-device management. Fast devices handle writes or caching, while slower devices store data migrated in the background, involving coordination between placement targets, replica counts, background migration, and space reclamation. copygc moves still-valid data out of reclaimable regions, so user write volume and total device write volume may differ.

The journal is not a decorative option that can be casually removed; "COW-centric, optional journal" does not describe its recovery protocol. Comparisons should look at persistent commit conditions, data and metadata replica policies, and recovery paths after device failures, rather than writing project promotional goals as proven performance conclusions.

Version and Deployment Boundaries

Version Check: 2026-09-08. bcachefs entered the mainline via Linux 6.7, but the official documentation currently states that it is no longer distributed with the mainline kernel from version 6.18 onwards, switching to an out-of-tree DKMS module. Deployment and upgrades require checking the compatibility of the module, tools, target kernel, and disk format simultaneously; one can no longer simply write "built-in to 6.7+ kernels." See bcachefs official documentation and official source repository.

6. Compare using capability boundaries, not fixed champions

Mechanismext4XFSBtrfs
Typical Metadata RecoveryJBD2 JournalMetadata JournalCOW transactions and tree log mechanisms
File Data SharingDoes not provide standard native reflinkFormats with reflink support availablereflink and subvolume snapshots
Ordinary Data ChecksumsNot commonly providedNot commonly providedUsually provided, subject to configuration
Space Organization FocusBlock groups, extents, delayed allocationAGs, extents, multiple allocation indexesBlock groups, profiles, shared references

Capacity limits depend on block size, format features, kernel implementation, and tool versions; it is inappropriate to directly write the theoretical bit width of the format as the user-available upper limit. Performance comparisons should fix the dataset, concurrency, sync frequency, remaining space, and snapshot state; short-duration buffered writes only measure memory absorption speed and cannot demonstrate device sustained capabilities.

The following pure calculation example checks logical range mappings without modifying the device:

extents = [(100, 8, 900), (108, 4, 3000)]  # Logical start, length, physical start

def locate(byte_offset, block_size=4096):
    if byte_offset < 0:
        raise ValueError("negative offset")
    logical, within = divmod(byte_offset, block_size)
    for start, count, physical in extents:
        if start <= logical < start + count:
            return physical + logical - start, within
    return None  # Represents a hole in this model; unwritten/delalloc not modeled

assert locate(102 * 4096 + 17) == (902, 17)
assert locate(108 * 4096) == (3000, 0)
assert locate(112 * 4096) is None
print("extent range and boundaries: passed")

Persistent update application protocols are covered in VFS Advanced; storage pools, TXG, and ZIL are covered in ZFS.