How Linux Implements Ext3 Ordered Data Journaling
The ext3 file system's data=ordered mode provides a
balance between file system performance and data integrity by
guaranteeing that file data is written to the main storage disk before
its associated metadata is committed to the journal. This design
prevents a critical vulnerability present in writeback caching: pointers
in newly committed metadata referencing unwritten, stale disk blocks
after a system crash. In the Linux kernel, this mechanism is coordinated
between the ext3 file system driver and the Journaling Block Device
(JBD) subsystem using transaction lists and staged I/O flushes.
The Role of the Journaling Block Device (JBD)
The ext3 file system does not handle the raw mechanics of journaling internally. Instead, it delegates transaction management to the JBD layer. JBD organizes file system changes into discrete transactions. At any given time, JBD manages:
- A Running Transaction: The active transaction gathering new file operations.
- Committing Transactions: Transactions currently being flushed to the journal on disk.
- Committed Transactions: Transactions whose metadata resides safely in the journal but has not yet been copied to its final on-disk location (checkpointing).
In data=ordered mode, JBD handles the sequence in which
data buffers and metadata buffers move through these transaction
states.
Associating Data Blocks with Transactions
When an application writes to a file in data=ordered
mode:
- Allocating Blocks: The ext3 driver assigns logical blocks to physical blocks on the storage device.
- Attaching Data to the Transaction: Ext3 calls
journal_dirty_data()to link the dirty page-cache buffers containing the file's payload to the running transaction's private data list (t_sync_datalist). - Dirtying Metadata: The metadata modifying the
inode, block allocation bitmaps, and indirect blocks is registered with
the transaction using
journal_get_write_access()andjournal_dirty_metadata().
At this stage, the data is queued in system memory, explicitly tethered to the metadata that describes it.
The Commit Pipeline
A background kernel thread named kjournald is
responsible for committing transactions to disk periodically (typically
every 5 seconds) or when an explicit synchronization event occurs (such
as an fsync() system call). The ordered implementation
executes in strict sequential phases within the
journal_commit_transaction() function:
- Phase 1: Flushing Ordered Data: Before any journal
updates occur,
kjournaldtraverses the transaction'st_sync_datalist. It initiates block I/O write requests for every data buffer attached to the transaction, writing the payload directly to its final destination on the file system. - Phase 2: Waiting for Data I/O Completion: The thread blocks until the storage controller confirms that all data blocks from Phase 1 have been physically written to non-volatile storage.
- Phase 3: Writing Journal Descriptor and Metadata: Once the payload is safely on disk, JBD writes the transaction's metadata blocks to the journal area on the disk, preceded by a descriptor block that identifies the metadata.
- Phase 4: Writing the Commit Record: Finally, a single commit block is written to the journal. The presence of the commit block marks the transaction as legally complete.
Crash Recovery Guarantees
If power fails or the operating system crashes during this process, the recovery semantics ensure file consistency:
- Crash before the Commit Record: If the system crashes prior to Phase 4, the entire transaction is discarded during post-reboot recovery. Even if data blocks were partially or fully written to disk, the metadata pointing to them was never committed, preserving the previous consistent state.
- Crash after the Commit Record: If the crash occurs after Phase 4, the JBD recovery process replays the committed metadata from the journal. Because Phases 1 and 2 guaranteed that the data blocks reached storage before the commit record was authorized, the replayed metadata will point directly to valid data rather than uninitialized blocks.