Applied, thanks!
Milos Nikic, le ven. 09 oct. 2026 09:21:45 -0700, a ecrit:
> Journal space came back only when the pager happened to write a
> transaction's blocks home, or when a commit found the log full and
> checkpointed in the committing thread, stalling it. Let kjournald
> reclaim space in the background instead.
>
> After each wake, kjournald checkpoints the oldest committed
> transactions once less than half of the journal is free, and stops
> once three quarters are free again. The gap keeps it from running on
> every wake, and lets the pager write hot blocks lazily in between.
That'll yield better performance overall indeed.
Samuel
> A
> background pass gives way between transactions to a requested commit,
> to a commit in flight and to shutdown. A stop that finds the journal
> past half full wakes kjournald at once rather than at the next tick.
>
> kjournald used to commit on every wake. A wake now commits only on
> the 5-second tick or when a stop requested it (j_commit_requested), so
> that waking it to checkpoint does not force a commit.
>
> Only one thread checkpoints at a time (j_checkpointing); a second
> waits for the first and then flushes only what is still needed. While
> the lock is dropped for I/O, the transaction being flushed is pinned
> through t_outstanding_io, so that a pager write finishing its last
> blocks cannot advance the tail and free it underneath the flusher.
>
> The checkpoint inside commit stays as a fallback and now logs a
> warning. To check that it is no longer needed in practice, I made it
> panic: it never fired under a large apt upgrade, a full-disk stress
> with parallel data and metadata load, and a 4 MB journal, the smallest
> tune2fs allows.
>
> With space reclaimed in the background, the sync_global calls on
> journaled paths no longer serve to free the log. They only write
> blocks home early, which works against the write-ahead order and grows
> the intercept cache; later patches can drop them.
> ---
> ext2fs/journal.c | 127 +++++++++++++++++++++++++++++++++++++++++++----
> 1 file changed, 117 insertions(+), 10 deletions(-)
>
> diff --git a/ext2fs/journal.c b/ext2fs/journal.c
> index 91323f3da..ea12c133b 100644
> --- a/ext2fs/journal.c
> +++ b/ext2fs/journal.c
> @@ -85,6 +85,15 @@
> */
> #define JRNL_LOW_WATER_RATIO 8
>
> +/**
> + * Background checkpoint ratios.
> + * kjournald starts checkpointing once less than 1/2 of the journal is free,
> + * and stops once 3/4 of it is free again. The gap keeps it from running
> + * on every wake, and lets the pager write hot blocks lazily in between.
> + */
> +#define JRNL_BG_START_RATIO 2
> +#define JRNL_BG_STOP_RATIO 4
> +
> /**
> * Slab Allocator Pool Size.
> * Pre-allocates a contiguous chunk of memory for journal buffers
> @@ -285,6 +294,10 @@ typedef struct journal
>
> int j_must_exit; /* variable that tells journal thread when to
> stop. */
> pthread_cond_t j_flusher_wakeup; /* Cond. var for the kjournald sleep
> cycle */
> + int j_commit_requested; /* A stop asked kjournald to commit now. */
> +
> + int j_checkpointing; /* A flusher owns the checkpoint list.
> */
> + pthread_cond_t j_checkpoint_done; /* Signalled when that flusher is done.
> */
>
> /* Pre-allocated buffers for zero-allocation commits */
> void *j_descriptor_buf;
> @@ -1469,26 +1482,52 @@ journal_flush_intercepted_payloads (journal_t
> *journal,
> }
> }
>
> +/* Whether a background flush should give way: the journal is shutting
> + down (quiesce owns the list), a stop asked for a commit, or a commit is
> + in flight and may be waiting for this flush. */
> +static int
> +journal_checkpoint_should_yield_locked (journal_t *journal)
> +{
> + return journal->j_must_exit || journal->j_commit_requested
> + || journal->j_committing_transaction;
> +}
> +
> /**
> * Internal helper to flush checkpoint transactions to the main filesystem.
> * If target_free is UINT32_MAX, it flushes ALL transactions in the list.
> * Otherwise, it flushes until j_free >= target_free.
> + * A BACKGROUND flush also stops between transactions when
> + * journal_checkpoint_should_yield_locked says so.
> + * Only one flusher runs at a time; a second one waits for the first, then
> + * flushes only what is still needed.
> * Returns 1 if a hardware I/O error occurred, 0 on success.
> * MUST be called with JOURNAL_LOCK held.
> */
> static int
> -journal_flush_checkpoints_locked (journal_t *journal, uint32_t target_free)
> +journal_flush_checkpoints_locked (journal_t *journal, uint32_t target_free,
> + int background)
> {
> int sb_changed = 0;
> int global_io_error = 0;
>
> - while (journal->j_free < target_free && journal->j_checkpoint_list)
> + while (journal->j_checkpointing)
> + JOURNAL_WAIT (&journal->j_checkpoint_done, journal);
> + journal->j_checkpointing = 1;
> +
> + while (journal->j_free < target_free && journal->j_checkpoint_list
> + && !(background && journal_checkpoint_should_yield_locked (journal)))
> {
> diskfs_transaction_t *txn = journal->j_checkpoint_list;
> size_t iter = 0;
> journal_buffer_t *jb;
> int io_error = 0;
>
> + /* Pin TXN. The lock is dropped for I/O and while waiting for a
> + pager claim; a pager write that completes the last outstanding
> + blocks would otherwise let journal_notify_blocks_written_locked
> + advance the tail and free TXN under us. */
> + txn->t_outstanding_io++;
> +
> /* Stream the fully committed WAL shadow blocks directly to the metal
> */
> while ((jb = journal_map_iterate (&txn->t_buffer_map, &iter)) != NULL)
> {
> @@ -1552,6 +1591,8 @@ journal_flush_checkpoints_locked (journal_t *journal,
> uint32_t target_free)
> }
> }
>
> + txn->t_outstanding_io--;
> +
> /* All blocks are physically on disk. Reclaim the space! */
> if (!io_error)
> {
> @@ -1600,6 +1641,9 @@ journal_flush_checkpoints_locked (journal_t *journal,
> uint32_t target_free)
> }
> }
>
> + journal->j_checkpointing = 0;
> + pthread_cond_broadcast (&journal->j_checkpoint_done);
> +
> return global_io_error;
> }
>
> @@ -1619,12 +1663,46 @@ journal_force_checkpoint_locked (journal_t *journal)
> uint32_t runway = (journal->j_last - journal->j_first) / 8;
> uint32_t target_free = journal->j_min_free + runway;
>
> - journal_flush_checkpoints_locked (journal, target_free);
> + journal_flush_checkpoints_locked (journal, target_free, 0);
>
> JRNL_LOG_DEBUG ("[CHECKPOINT] Done checkpointing (Free: %u).",
> journal->j_free);
> }
>
> +/* Whether kjournald should checkpoint in the background: less than
> + 1/JRNL_BG_START_RATIO of the journal is free and there is something to
> + flush. */
> +static int
> +journal_bg_checkpoint_needed_locked (journal_t *journal)
> +{
> + uint32_t capacity = journal->j_last - journal->j_first;
> +
> + return journal->j_checkpoint_list && !journal->j_must_exit
> + && !diskfs_readonly
> + && journal->j_free < capacity / JRNL_BG_START_RATIO;
> +}
> +
> +/* Checkpoint the oldest transactions until 1 - 1/JRNL_BG_STOP_RATIO of the
> + journal is free, so that commit rarely has to checkpoint itself. Gives
> + way to commits and to shutdown between transactions. Called by
> + kjournald. */
> +static void
> +journal_bg_checkpoint_locked (journal_t *journal)
> +{
> + uint32_t capacity = journal->j_last - journal->j_first;
> +
> + if (!journal_bg_checkpoint_needed_locked (journal))
> + return;
> +
> + JRNL_LOG_DEBUG ("[CHECKPOINT] Background checkpoint (Free: %u).",
> + journal->j_free);
> + journal_flush_checkpoints_locked (journal,
> + capacity - capacity / JRNL_BG_STOP_RATIO,
> + 1);
> + JRNL_LOG_DEBUG ("[CHECKPOINT] Background checkpoint done (Free: %u).",
> + journal->j_free);
> +}
> +
> /**
> * Peeks at the next block in the ring buffer without allocating it.
> * Caller MUST hold JOURNAL_LOCK or guarantee exclusive access.
> @@ -2287,6 +2365,10 @@ journal_commit_running_transaction_locked (journal_t
> *journal)
>
> /* Wake up everyone waiting in journal_wait_on_tid */
> pthread_cond_broadcast (&journal->j_commit_done);
> + /* Start a background checkpoint now rather than at the next tick. When
> + kjournald is the committer it checks for itself after the commit. */
> + if (journal_bg_checkpoint_needed_locked (journal))
> + pthread_cond_signal (&journal->j_flusher_wakeup);
> JOURNAL_UNLOCK (journal);
> if (need_sb_flush)
> flush_to_disk ();
> @@ -2326,6 +2408,14 @@ out:
> * that when the Mach VM Pager eventually needs to flush dirty pages to the
> * main disk, it doesn't stall the system waiting for synchronous journal
> I/O.
> *
> + * - Background Checkpointing: After each wake it checkpoints the oldest
> + * committed transactions once the journal is more than half full (see
> + * journal_bg_checkpoint_locked), so that commit rarely has to stall on a
> + * forced checkpoint. A pass gives way to a requested commit.
> + *
> + * It commits on the 5-second tick and when a stop requests it
> + * (j_commit_requested). A wake only for checkpointing does not commit.
> + *
> * Future work: The interval could be made dynamic based on VFS load, but
> * a static 5-second interval provides a solid baseline.
> */
> @@ -2338,18 +2428,28 @@ kjournald_thread (void *arg)
> JOURNAL_LOCK (journal);
> while (!journal->j_must_exit)
> {
> - clock_gettime (CLOCK_MONOTONIC, &ts);
> - ts.tv_sec += 5;
> + int commit = journal->j_commit_requested;
>
> - pthread_cond_clockwait (&journal->j_flusher_wakeup,
> - &journal->j_state_lock, CLOCK_MONOTONIC, &ts);
> + /* A request made while we were busy is not waited for: its signal
> + has already gone. */
> + if (!commit)
> + {
> + clock_gettime (CLOCK_MONOTONIC, &ts);
> + ts.tv_sec += 5;
> +
> + int err = pthread_cond_clockwait (&journal->j_flusher_wakeup,
> + &journal->j_state_lock,
> + CLOCK_MONOTONIC, &ts);
> + commit = err == ETIMEDOUT || journal->j_commit_requested;
> + }
> + journal->j_commit_requested = 0;
>
> if (journal->j_must_exit)
> break;
> if (diskfs_readonly)
> continue;
>
> - if (journal->j_running_transaction)
> + if (commit && journal->j_running_transaction)
> {
> JRNL_LOG_DEBUG ("Woke the journal up:\n"
> " - Sequence: %u\n"
> @@ -2363,6 +2463,8 @@ kjournald_thread (void *arg)
> if (err)
> JRNL_LOG_WARN ("Background commit failed: %s", strerror (err));
> }
> +
> + journal_bg_checkpoint_locked (journal);
> }
> JOURNAL_UNLOCK (journal);
> return NULL;
> @@ -2403,6 +2505,7 @@ journal_create (struct node *journal_inode)
> pthread_mutex_init (&j->j_state_lock, NULL);
> pthread_cond_init (&j->j_commit_wait, NULL);
> pthread_cond_init (&j->j_flusher_wakeup, NULL);
> + pthread_cond_init (&j->j_checkpoint_done, NULL);
> pthread_cond_init (&j->j_flush_wait, NULL);
> pthread_cond_init (&j->j_running_wait, NULL);
>
> @@ -2490,7 +2593,8 @@ journal_quiesce_checkpoints (void)
>
> /* Write ALL shadow data from every checkpoint transaction to the main
> disk. */
> /* Pass UINT32_MAX to guarantee we drain the entire checkpoint list */
> - int io_error = journal_flush_checkpoints_locked (ext2_journal, UINT32_MAX);
> + int io_error =
> + journal_flush_checkpoints_locked (ext2_journal, UINT32_MAX, 0);
>
> if (io_error)
> {
> @@ -2515,7 +2619,10 @@ diskfs_journal_stop_transaction_locked (journal_t
> *journal,
> /* Semi auto-commit? */
> if (txn->t_active_threads == 0 && (txn->sync_needed ||
> (txn->t_buffer_map.size >= journal->j_max_transaction_buffers)))
> - pthread_cond_signal (&journal->j_flusher_wakeup);
> + {
> + journal->j_commit_requested = 1;
> + pthread_cond_signal (&journal->j_flusher_wakeup);
> + }
> }
>
> /* Ends the caller's participation in the given transaction TXN.
> --
> 2.56.0
>
--
Samuel
Q: How do you play religious roulette?
A: You stand around in a circle and blaspheme and see who gets struck by
lightning first.