The jbd2 Journaling Layer

jbd2 (the Journaling Block Device, version 2) is the generic, filesystem-independent journaling engine that lives in fs/jbd2/ and gives ext4 (and ocfs2) its crash consistency. Its single job is to make a group of metadata block writes atomic with respect to a crash: either every block in the group lands on disk, or none of it appears to have. jbd2 achieves this with write-ahead logging — it first writes the new versions of the blocks into a dedicated circular on-disk log (the journal), terminates that record with a commit block, and only afterwards lets the blocks drift to their real “in-place” homes. If the machine dies mid-update, recovery at mount time replays the complete, committed records from the log and discards any half-written tail (per the journalling design described in Documentation/filesystems/journalling.rst, v6.12, and the original ext3 design by Stephen Tweedie, OLS 2000). This note describes jbd2 as of the 6.12 LTS kernel (released 2024-11-17).

The code is the direct descendant of Stephen Tweedie’s jbd written for ext3 in 1998–2000; the file banners in fs/jbd2/commit.c and revoke.c still carry his name. The “2” denotes the 64-bit-block-number rework done for ext4. The vocabulary below — transaction, handle, commit, checkpoint, revoke — is his.

Mental Model

The right way to think about jbd2 is as a redo log in front of the filesystem, exactly analogous to a database write-ahead log. The filesystem never modifies a metadata block on disk “in place” without first having promised, in a durable log, what that modification will be. The log is the source of truth across a crash; the in-place filesystem is merely a cache of the log’s effects that recovery can rebuild.

Three lifetimes layer on top of each other. The innermost is a handle (handle_t): one filesystem operation’s worth of changes — e.g. “rename this file” touches a couple of directory blocks, an inode, and a bitmap. Many handles are batched into one transaction (transaction_t): the unit that commits atomically. And the journal as a whole is a circular log of committed transactions that is slowly drained as their effects reach the real filesystem.

flowchart LR
  subgraph RAM["In memory"]
    H1["handle (rename)"] --> RT
    H2["handle (write meta)"] --> RT
    H3["handle (mkdir)"] --> RT
    RT["RUNNING transaction<br/>(batches many handles)"]
  end
  RT -->|"commit cycle<br/>(WAL: log first)"| LOG
  subgraph DISK["On-disk journal (circular log)"]
    direction LR
    LOG["descriptor + data blocks<br/>... + commit block"]
  end
  LOG -->|"checkpoint:<br/>blocks reach real homes"| FS["In-place filesystem<br/>(inodes, bitmaps, dirs)"]
  FS -->|"space freed"| LOG
  CRASH["CRASH"] -.->|"replay committed<br/>records at mount"| FS

The three lifetimes of jbd2. What it shows: filesystem operations become handles, handles batch into one running transaction, the transaction commits to the circular journal (write-ahead), and checkpointing later copies the logged blocks to their real homes, freeing journal space. The insight to take: the commit block is the atomicity boundary — everything before it in a transaction’s log record is “all” and everything after a crash without it is “nothing.” Recovery just replays committed records.

Mechanical Walk-through

Wrapping changes in a handle

A filesystem does not touch jbd2-managed blocks directly. Per journalling.rst, it opens a transaction handle with jbd2_journal_start(journal, nblocks), passing the maximum number of blocks it might dirty. start_this_handle() in fs/jbd2/transaction.c attaches the handle to the journal’s current running transaction and reserves nblocks worth of log space against it via add_transaction_credits(), charging t_outstanding_credits. This credit accounting is why jbd2_journal_start() can block: if the running transaction has no room for the request (t_outstanding_credits would exceed j_max_transaction_buffers, derived from the journal size), the caller waits for the current transaction to commit and a fresh one to open. The docs warn this makes jbd2_journal_start()/jbd2_journal_stop() behave “as if they were semaphores” for deadlock-ordering purposes.

Before modifying a metadata buffer, the filesystem must call one of jbd2_journal_get_write_access() (existing block), jbd2_journal_get_create_access() (freshly allocated block), or jbd2_journal_get_undo_access() (needs the old contents preserved). These let jbd2 take a snapshot of the unmodified data if the buffer is still part of a previously committed but not yet checkpointed transaction — it cannot let the running transaction scribble over a block whose old value an earlier transaction still needs. After modifying the buffer, the filesystem calls jbd2_journal_dirty_metadata() to enlist it in the transaction; then jbd2_journal_stop() ends the handle. Crucially, nothing commits until the outermost stop and the transaction’s commit cycle runs — handles are nestable but a task may only have one outstanding transaction at a time.

The running transaction and the five-second timer

All handles started in a window of time join the same running transaction. ext4 deliberately batches: rather than committing per-fsync() only, the commit= mount option (default 5 seconds, per the ext4 admin guide) bounds the maximum age of the running transaction. In transaction.c, a new transaction sets t_expires = jiffies + journal->j_commit_interval. A dedicated kernel thread, kjournald2, wakes when the timer fires (or when space pressure or an fsync() forces it) and drives the commit. Batching is the central performance trick: many independent operations amortize one expensive log write and one durability barrier.

The commit cycle — a state machine

The heart of jbd2 is jbd2_journal_commit_transaction() in fs/jbd2/commit.c, which walks the committing transaction through a fixed sequence of states (the t_state enum in include/linux/jbd2.h):

stateDiagram-v2
  [*] --> T_RUNNING: handles attach here
  T_RUNNING --> T_LOCKED: lock down; wait for<br/>open handles to finish
  T_LOCKED --> T_SWITCH: drain reserved buffers
  T_SWITCH --> T_FLUSH: new running txn opens;<br/>data buffers submitted
  T_FLUSH --> T_COMMIT: write metadata +<br/>descriptor blocks to log
  T_COMMIT --> T_COMMIT_DFLUSH: wait journal data I/O
  T_COMMIT_DFLUSH --> T_COMMIT_JFLUSH: write COMMIT block<br/>(FLUSH+FUA)
  T_COMMIT_JFLUSH --> T_COMMIT_CALLBACK: log tail updated
  T_COMMIT_CALLBACK --> T_FINISHED: callbacks run; txn done
  T_FINISHED --> [*]

The commit state machine in jbd2_journal_commit_transaction(). What it shows: the named phases the kernel logs as “commit phase 1…7,” from locking the running transaction through writing the durable commit block to finishing. The insight to take: the transition into T_COMMIT_JFLUSH — where the commit block is written with a barrier — is the moment the transaction becomes durable; everything before it is reversible, everything after is permanent.

Phase by phase, with the actual code:

  1. T_LOCKED (phase 1). commit_transaction->t_state = T_LOCKED, then jbd2_journal_wait_updates() blocks until every open handle on this transaction has called its final jbd2_journal_stop(). No new handle can join. Reserved-but-unused buffers are refiled.
  2. T_SWITCH → T_FLUSH (phase 2a). A new running transaction is opened (j_running_transaction = NULL then re-created on demand) so the rest of the system keeps making progress while this one commits. The revoke table is switched (jbd2_journal_switch_revoke_table()). Then — and this is where data=ordered lives — journal_submit_data_buffers() submits the transaction’s file data buffers to their real locations and the revoke records are written: “Data blocks go first” (comment in commit.c).
  3. T_COMMIT (phase 2b–3). jbd2 loops over the transaction’s metadata buffers, copies each into a journal buffer, and emits a descriptor block (JBD2_DESCRIPTOR_BLOCK) listing the real block numbers of the metadata that follows. The metadata and descriptors stream to the circular log.
  4. T_COMMIT_DFLUSH (phase 4–5). jbd2 waits for the journal’s own data I/O to complete, then transitions to T_COMMIT_JFLUSH and writes the commit block.
  5. The commit block (phase 5). journal_submit_commit_record() builds a struct commit_header, and — the durability linchpin — if journal->j_flags & JBD2_BARRIER and the journal is not in async_commit mode, it ORs REQ_PREFLUSH | REQ_FUA into the write flags. This is examined in detail below.
  6. Checkpoint bookkeeping (phase 6–7). The committed metadata buffers are moved onto the transaction’s checkpoint list (they still need to reach their in-place homes), the on-disk journal tail pointer is advanced past now-superseded transactions, commit callbacks fire (j_commit_callback — ext4 uses this to free blocks only after the freeing transaction is durable), and t_state = T_FINISHED.

Write-ahead logging and the FLUSH+FUA barrier

The correctness of the whole scheme rests on ordering: the descriptor and data blocks of a transaction must be physically on the platter before the commit block, and the commit block must be on the platter before recovery is allowed to trust it. But modern storage reorders and caches writes in a volatile device cache. jbd2 enforces the ordering with two block-layer primitives applied to the commit-block write, visible in journal_submit_commit_record():

  • REQ_PREFLUSH (“flush”) tells the device to flush its volatile write cache — forcing all previously submitted writes (the descriptor, metadata, and data=ordered file data) to durable media — before this commit-block write begins.
  • REQ_FUA (“Force Unit Access”) tells the device that this write itself must reach durable media before it is acknowledged, not merely the cache.

Together, FLUSH+FUA create a hard durability fence: when the commit block’s I/O completes, the entire transaction — log body and commit record — is guaranteed on stable storage, in order. This is the kernel-side meaning of the barrier=1 ext4 mount option (the default; nobarrier removes JBD2_BARRIER and is only safe with a battery-backed cache, per the ext4 admin guide). For the deeper plumbing of these flags see Write Barriers and FLUSH FUA and The Block IO Layer bio.

Revoke records — the subtle correctness fix

Consider: a metadata block B (say, a directory block) is logged in transaction T₁. Later, the directory is deleted and block B is freed and re-used as a file-data block in transaction T₂, which also commits. Now the machine crashes before T₁ has been checkpointed. Naïve replay would re-apply T₁’s old metadata image of B, clobbering the live file data that legitimately occupies B now. The fix is the revoke record, documented at the top of fs/jbd2/revoke.c: when a block is freed (so its old journal content must never be replayed past a certain point), jbd2 writes a revoke record into the log. During recovery, “if there are multiple revoke records in the log for a single block, only the last one counts, and if there is a log entry for a block beyond the last revoke, then that log entry still gets replayed.” In other words, a revoke for block B at sequence s suppresses replay of any earlier log entry for B, but a later legitimate log entry for B survives. Revokes are the mechanism that makes block re-use safe across the journal.

Crash recovery — the three passes

At mount, ext4 calls jbd2_journal_load(), which calls jbd2_journal_recover() if the journal is dirty. The on-disk journal superblock’s s_start field is zero if and only if the journal was cleanly unmounted (fs/jbd2/recovery.c); a non-zero s_start means recovery is required. Recovery runs three passes (the comment is explicit):

  1. PASS_SCAN — walk the log from the last checkpoint to find the end of the valid log: the last transaction whose commit block is present and consistent (and, if journal_checksum is on, whose checksum verifies). A torn transaction with no valid commit block defines the cutoff; everything after it is garbage from a previous wrap of the circular log and is ignored.
  2. PASS_REVOKE — assemble the revoke table: for every block, the highest transaction sequence at which it was revoked.
  3. PASS_REPLAY — re-walk the log and, for each logged metadata block, write it to its real in-place location unless the revoke table says a revoke at an equal-or-later sequence suppresses it. After replay, sync_blockdev() forces the recovered blocks to disk, and the journal is reset (j_transaction_sequence = ++end_transaction), invalidating the now-stale commit records so a second crash during recovery is itself safe (recovery is idempotent).

The result: after recovery the in-place filesystem reflects exactly the set of transactions that had a durable commit block, and nothing else. This is what e2fsck skips when the journal recovers cleanly — see Crash Consistency and fsck for how journal recovery and fsck relate.

Configuration / Code

The on-disk journal format

The journal is a sequence of fixed-size blocks (block size = filesystem block size). Every special block starts with a journal_header_s carrying the magic number and a block type (include/linux/jbd2.h):

#define JBD2_MAGIC_NUMBER 0xc03b3998U /* "first 4 bytes of /dev/random!" */
 
#define JBD2_DESCRIPTOR_BLOCK   1   /* lists block numbers of the data that follows */
#define JBD2_COMMIT_BLOCK       2   /* terminates a transaction; the atomicity point */
#define JBD2_SUPERBLOCK_V1      3
#define JBD2_SUPERBLOCK_V2      4   /* records s_sequence, s_start, s_maxlen ... */
#define JBD2_REVOKE_BLOCK       5   /* lists revoked block numbers */

A committed transaction on disk looks like: one or more descriptor blocks (each followed by the actual metadata blocks they tag, the tags giving the real destination block number), optional revoke blocks, and a terminating commit block. The journal superblock (block 0 of the log) holds s_sequence (the first commit ID expected in the log), s_start (block number of the log head — zero means clean), and s_maxlen (total journal blocks). The circular nature comes from s_start and the per-transaction t_log_start chasing each other around s_maxlen blocks; checkpointing advances the tail (freeing space) and committing advances the head.

The three data modes

jbd2 only fundamentally journals metadata. What happens to file data is the famous data= mount option (ext4 admin guide):

# Default. Metadata is journaled; file data is forced to its
# real location *before* the metadata commit. Prevents stale
# blocks (old/garbage data) being exposed in newly-extended files.
mount -o data=ordered /dev/sda1 /mnt
 
# Fastest. Metadata is journaled but data is NOT ordered against it.
# A crash can leave metadata pointing at blocks whose data write
# hadn't landed -> a file may contain stale contents after recovery.
mount -o data=writeback /dev/sda1 /mnt
 
# Safest, slowest. ALL data is written to the journal first, then to
# its home. Full data+metadata journaling. Disables delayed
# allocation and O_DIRECT.
mount -o data=journal /dev/sda1 /mnt

Line-by-line: data=ordered (the default) is implemented exactly by phase 2a above — journal_submit_data_buffers() pushes file data to its real home before the metadata commit block is written, so the metadata can never point at a block that still holds garbage. It journals no file data, yet preserves the safety property that matters. data=writeback skips that ordering: metadata commits independently of its data, so it is faster but “can potentially leave stale data” in recently-written files after a crash. data=journal routes file data through the log itself — every data block is written twice (once to the journal, once to its home), the strongest guarantee at roughly double the write cost, and it is incompatible with O_DIRECT and delayed allocation. See Direct IO and O_DIRECT for why journaling and O_DIRECT cannot coexist.

Forcing a commit

/* From journalling.rst: a filesystem can checkpoint everything,
 * e.g. to take a stable snapshot of the on-disk state. */
jbd2_journal_lock_updates(journal);  /* block all new transactions */
jbd2_journal_flush(journal);         /* commit + checkpoint everything */
/* ... operate on a clean, stable filesystem ... */
jbd2_journal_unlock_updates(journal);/* resume normal operation */

jbd2_journal_flush() both commits the running transaction and checkpoints all committed transactions, leaving the log empty. ext4 uses the lock/flush/unlock window to freeze the filesystem (FIFREEZE) for consistent snapshots. The docs explicitly note the DoS risk if this code path is reachable by unprivileged userspace.

Fast commits (ext4 only)

Since Linux 5.10, jbd2 supports fast commits: a filesystem-specific delta commit that logs only the small set of changes an fsync() needs (e.g. “inode N grew, these ranges changed”) instead of a full transaction, cutting fsync() latency. The hooks are j_fc_cleanup_cb and j_fc_replay_cb, and a fast commit is bracketed by jbd2_fc_begin_commit() / jbd2_fc_end_commit(). Only ext4 implements them (fs/ext4/fast_commit.c).

Uncertain

Verify: that fast commits landed in Linux 5.10 specifically. Reason: the v6.12 journalling.rst documents the fast-commit API but does not state the introduction version; the 5.10 figure is from memory, not a primary source consulted here. To resolve: check the merge commit for fs/ext4/fast_commit.c / the ext4 fast-commit patch series cover letter. uncertain

Failure Modes and Common Misunderstandings

“Journaling protects my data.” By default it does not. data=ordered journals only metadata; a crash can still lose the last few seconds of file data (whatever had not yet been written back). The journal guarantees the filesystem structure stays consistent — no dangling inodes, no double-allocated blocks — not that your unsynced writes survive. Only fsync()/fdatasync() (which forces a commit) makes specific data durable; see fsync fdatasync and Durability.

The nobarrier foot-gun. Mounting -o nobarrier (or barrier=0) clears JBD2_BARRIER, so the commit block is written without FLUSH+FUA. On a device with a volatile write cache, this means the commit block can reach the platter before the log body it commits — and a crash then replays a transaction whose data was never written, corrupting the filesystem. It is safe only with a non-volatile (battery/cap-backed) cache. The ext4 docs note jbd will auto-disable barriers with a warning if a barrier write errors.

Journal too small → commit stalls. Because jbd2_journal_start() blocks when the running transaction cannot reserve credits, an undersized journal on a metadata-heavy workload serializes everything behind frequent forced commits. Symptom: tasks stuck in D state inside start_this_handle/add_transaction_credits. The fix is a larger journal (tune2fs/mke2fs -J size=).

Aborted journal. If jbd2 hits an I/O error it cannot tolerate, it sets JBD2_ABORT (jbd2_journal_abort()). All subsequent commits fail and ext4 typically remounts read-only (errors=remount-ro). dmesg shows JBD2: Detected aborted journal. The filesystem must be unmounted and e2fsck’d.

“Recovery is fsck.” No. Journal recovery (replay) is fast and runs automatically at mount; it restores the structure to the last committed transaction. e2fsck is a full structural scan that runs only when the filesystem is flagged inconsistent or the check interval elapses, and it can repair damage the journal cannot (e.g. corruption from a nobarrier crash). See Crash Consistency and fsck.

Alternatives and When to Choose Them

jbd2 implements physical block journaling (it logs the whole changed metadata block). The main alternatives:

  • Logical / intent journaling (XFS). XFS logs logical change descriptions (“set this field in this inode”) rather than whole blocks, into an in-memory log buffer flushed asynchronously. This is more compact and was designed for high parallelism. See XFS Filesystem Internals. The trade-off: a more complex recovery and a log format tightly coupled to XFS’s own structures, whereas jbd2 is a clean generic layer reusable by any filesystem.
  • Copy-on-write (Btrfs, bcachefs, ZFS). No journal at all: never overwrite live data; write new blocks and atomically flip a root pointer. This gives crash consistency and cheap snapshots and end-to-end checksums, at the cost of write amplification and fragmentation. See Btrfs Filesystem Internals. jbd2’s journaling has lower steady-state write amplification for metadata-light workloads and is far more battle-tested.
  • data=journal vs. CoW for data integrity. If you genuinely need every byte of data crash-atomic, data=journal gives it on ext4 (double-writing data), whereas CoW gives it structurally for free — which is one reason data-integrity-critical deployments often choose Btrfs/ZFS.

The decision in practice: ext4 + jbd2 in default data=ordered is the conservative choice — predictable, low-overhead, decades of production hardening. Reach for XFS for huge files and parallelism, CoW filesystems for snapshots and checksums.

Production Notes

The five-second default commit interval and the page-cache writeback timer interact in a way that surprises people: even with data=ordered, freshly written file data can be lost on power failure because writeback of dirty pages only begins after /proc/sys/vm/dirty_expire_centisecs — so the window of loss is governed by writeback, not the journal commit interval. The ext4 docs spell this out under the commit= option.

External journals (journal_dev=) put the log on a separate, ideally faster or battery-backed, device — common on databases to keep journal FLUSH/FUA traffic off the main spindle. The journal_async_commit option (which writes the commit block without first waiting for the descriptor blocks, and implicitly enables journal_checksum) trades a stricter on-disk format — older kernels can no longer mount the device — for lower commit latency, relying on the commit-record checksum rather than strict write ordering to detect a torn transaction during recovery.

For observability, kjournald2 appears as a per-device kernel thread; jbd2 exposes per-filesystem stats under /proc/fs/jbd2/<dev>/ (transaction counts, average wait/commit times) and a rich set of tracepoints under trace/events/jbd2.h (jbd2_start_commit, jbd2_commit_locking, jbd2_run_stats) that are the standard way to diagnose commit-latency problems in production.

See Also