Directory blocks are metadata, but ext2fs edits them through the
directory's file-pager mapping. The disk cache, which the stop-time
sweep of journal_get_write_access copies from, never sees those edits,
so they reached disk outside the journal, through file-pager writes.
After a crash, a directory entry could then name an inode whose
creation was never committed.
Add journal entry points for a metadata block whose live bytes are
outside the disk cache. journal_get_write_access_external reserves the
block before the edit, so that pager writes of it are intercepted until
commit. The buffer has no image yet; jb_awaiting_data marks it, and the
sweep leaves it alone. journal_mark_dirty_external copies the finished
block into the shadow. The copy reads the mapping into a scratch buffer
with hurd_safe_copyin before it takes the journal lock, as a fault under
the lock would re-enter the journal through the pager. An intercept
chunk taken during the edit receives the same image, because the chunk
backs reads of the block until its home write.
journal_release_access drops a reservation that never received an
image, and drops the block from the count of logged blocks that revoke
records rely on. When a pager write was intercepted since the
reservation, that image is the page as it is, and it stays in the
transaction. A released buffer is freed at once: the stop-time sweep
only takes buffers with needs_copy set, which an external reservation
clears, and an assertion keeps it that way.
journal_get_write_access and journal_get_write_access_external share
one locked helper, with a flag that selects the disk cache or the
caller as the source of the copy. journal_write_payload asserts that
every buffer has an image. Directory blocks are now intercepted too,
so the intercept pool grows from 512 to 2048 chunks: with that, no
write went past the log during large upgrades or tar -xf and rm -rf of
a Linux kernel tree.
diskfs_direnter_hard, diskfs_dirremove_hard and diskfs_dirrewrite_hard
reserve the block before the edit and hand it to the journal before the
mapping is dropped. They work inside the RPC's journal handle, which
journal_thread_transaction returns, and which keeps the transaction open
from the reservation to the copy. For EXTEND the block is allocated
when it is reserved, as diskfs_grow only makes room for it.
---
ext2fs/dir.c | 126 ++++++++++++++++++++++++++++++++++++--
ext2fs/journal.c | 153 ++++++++++++++++++++++++++++++++++++++++++++---
ext2fs/journal.h | 24 ++++++++
3 files changed, 290 insertions(+), 13 deletions(-)
diff --git a/ext2fs/dir.c b/ext2fs/dir.c
index b90529967..09f5a7225 100644
--- a/ext2fs/dir.c
+++ b/ext2fs/dir.c
@@ -21,6 +21,7 @@
#include "ext2fs.h"
#include <libdiskfs/diskfs.h>
+#include "journal.h"
#include <string.h>
#include <stdio.h>
@@ -527,6 +528,69 @@ dirscanblock (vm_address_t blockaddr, struct node *dp, int
idx,
return 0;
}
+/* Directory blocks are metadata, and their live bytes are the directory's
+ file-pager mapping, outside the disk cache. An edit runs inside the
+ RPC's journal handle. dirblock_get_write_access comes before the first
+ byte changes, so that pager writes of the block are intercepted until
+ commit. dirblock_mark_dirty hands the finished block to the journal
+ before the mapping is dropped. dirblock_release_access gives the block
+ up when the edit never happened. A zero block number stands for an
+ edit outside the journal. */
+
+/* Find the disk block of directory block IDX of DP into *BLK and add it
+ to the thread's transaction. CREATE allocates the block, for a block
+ that diskfs_grow has only made room for. */
+static error_t
+dirblock_get_write_access (struct node *dp, int idx, int create,
+ block_t *blk)
+{
+ struct disknode *dn = diskfs_node_disknode (dp);
+ diskfs_transaction_t *txn;
+ error_t err;
+
+ *blk = 0;
+ txn = journal_thread_transaction ();
+ if (!txn)
+ return 0;
+
+ /* We must acquire a WRITE lock if we might allocate physical blocks. */
+ if (create)
+ pthread_rwlock_wrlock (&dn->alloc_lock);
+ else
+ pthread_rwlock_rdlock (&dn->alloc_lock);
+ err = ext2_getblk (dp, idx, create, blk);
+ pthread_rwlock_unlock (&dn->alloc_lock);
+ if (err)
+ return err;
+
+ return journal_get_write_access_external (txn, *blk);
+}
+
+/* Hand the journal the edited directory block BLK, mapped at DATA. */
+static error_t
+dirblock_mark_dirty (block_t blk, vm_address_t data)
+{
+ diskfs_transaction_t *txn;
+ error_t err;
+
+ if (!blk)
+ return 0;
+
+ txn = journal_thread_transaction ();
+ err = journal_mark_dirty_external (txn, blk, (const void *) data);
+ if (err)
+ journal_release_access (txn, blk);
+ return err;
+}
+
+/* Give up directory block BLK, which was never edited. */
+static void
+dirblock_release_access (block_t blk)
+{
+ if (blk)
+ journal_release_access (journal_thread_transaction (), blk);
+}
+
/* Following a lookup call for CREATE, this adds a node to a directory.
DP is the directory to be modified; NAME is the name to be entered;
NP is the node being linked in; DS is the cached information returned
@@ -545,6 +609,7 @@ diskfs_direnter_hard (struct node *dp, const char *name,
struct node *np,
size_t totfreed;
error_t err;
size_t oldsize = 0;
+ block_t blk = 0;
assert_backtrace (ds->type == CREATE);
@@ -552,6 +617,18 @@ diskfs_direnter_hard (struct node *dp, const char *name,
struct node *np,
dp->dn_set_mtime = 1;
+ /* Every slot but EXTEND's lies in an allocated block. EXTEND's block
+ is allocated once diskfs_grow has made room for it. */
+ if (ds->stat != EXTEND)
+ {
+ err = dirblock_get_write_access (dp, ds->idx, 0, &blk);
+ if (err)
+ {
+ munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ return err;
+ }
+ }
+
/* Select a location for the new directory entry. Each branch of this
switch is responsible for setting NEW to point to the on-disk
directory entry being written, and setting NEW->rec_len appropriately. */
@@ -631,15 +708,23 @@ diskfs_direnter_hard (struct node *dp, const char *name,
struct node *np,
}
}
+ err = dirblock_get_write_access (dp, ds->idx, 1, &blk);
+ if (err)
+ {
+ munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ return err;
+ }
+
new = (struct ext2_dir_entry_2 *) (ds->mapbuf + oldsize);
err = hurd_safe_memset (new, 0, DIRBLKSIZ);
if (err)
- {
- if (err == EKERN_MEMORY_ERROR)
- err = ENOSPC;
- munmap ((caddr_t) ds->mapbuf, ds->mapextent);
- return err;
- }
+ {
+ if (err == EKERN_MEMORY_ERROR)
+ err = ENOSPC;
+ dirblock_release_access (blk);
+ munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ return err;
+ }
dp->dn_stat.st_size = oldsize + DIRBLKSIZ;
dp->dn_set_ctime = 1;
@@ -676,7 +761,10 @@ diskfs_direnter_hard (struct node *dp, const char *name,
struct node *np,
diskfs_node_disknode (dp)->info.i_flags &= ~EXT2_BTREE_FL;
dp->dn_set_mtime = 1;
+ err = dirblock_mark_dirty (blk, ds->mapbuf + ds->idx * DIRBLKSIZ);
munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ if (err)
+ return err;
if (ds->stat != EXTEND)
{
@@ -737,11 +825,21 @@ diskfs_direnter_hard (struct node *dp, const char *name,
struct node *np,
error_t
diskfs_dirremove_hard (struct node *dp, struct dirstat *ds)
{
+ error_t err;
+ block_t blk;
+
assert_backtrace (ds->type == REMOVE);
assert_backtrace (ds->stat == HERE_TIS);
assert_backtrace (!diskfs_readonly);
+ err = dirblock_get_write_access (dp, ds->idx, 0, &blk);
+ if (err)
+ {
+ munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ return err;
+ }
+
if (ds->preventry == 0)
ds->entry->inode = htole32 (0);
else
@@ -754,7 +852,10 @@ diskfs_dirremove_hard (struct node *dp, struct dirstat *ds)
dp->dn_set_mtime = 1;
diskfs_node_disknode (dp)->info.i_flags &= ~EXT2_BTREE_FL;
+ err = dirblock_mark_dirty (blk, ds->mapbuf + ds->idx * DIRBLKSIZ);
munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ if (err)
+ return err;
/* If we are keeping count of this block, then keep the count up
to date. */
@@ -777,16 +878,29 @@ diskfs_dirremove_hard (struct node *dp, struct dirstat
*ds)
error_t
diskfs_dirrewrite_hard (struct node *dp, struct node *np, struct dirstat *ds)
{
+ error_t err;
+ block_t blk;
+
assert_backtrace (ds->type == RENAME);
assert_backtrace (ds->stat == HERE_TIS);
assert_backtrace (!diskfs_readonly);
+ err = dirblock_get_write_access (dp, ds->idx, 0, &blk);
+ if (err)
+ {
+ munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ return err;
+ }
+
ds->entry->inode = htole32 (np->cache_id);
dp->dn_set_mtime = 1;
diskfs_node_disknode (dp)->info.i_flags &= ~EXT2_BTREE_FL;
+ err = dirblock_mark_dirty (blk, ds->mapbuf + ds->idx * DIRBLKSIZ);
munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+ if (err)
+ return err;
diskfs_file_update (dp, diskfs_synchronous);
diff --git a/ext2fs/journal.c b/ext2fs/journal.c
index 37139056e..2993a76c1 100644
--- a/ext2fs/journal.c
+++ b/ext2fs/journal.c
@@ -35,6 +35,7 @@
#include <pthread.h>
#include <mach.h>
+#include <hurd/sigpreempt.h>
#include <hurd/store.h>
#include <libdiskfs/diskfs.h>
#include "ext2fs.h"
@@ -130,7 +131,7 @@
#define IS_RUNNING_TID(j, tid) \
((j)->j_running_transaction && (j)->j_running_transaction->t_tid == (tid))
-#define JRNL_INTERCEPT_CAPACITY 512
+#define JRNL_INTERCEPT_CAPACITY 2048
static pthread_t kjournald_tid;
@@ -158,6 +159,12 @@ static __thread int journal_thread_is_pager;
is that thread's business. Cleared by the outermost start. */
static __thread int journal_thread_sync;
+/* Allocated in journal_create. journal_mark_dirty_external copies the
+ caller's image here before it takes the journal lock. */
+static void *journal_external_scratch;
+static pthread_mutex_t journal_external_scratch_lock =
+ PTHREAD_MUTEX_INITIALIZER;
+
/**
* Holds one modified block (4KB) that needs to be written to the journal.
*/
@@ -174,6 +181,10 @@ typedef struct journal_buffer
char *jb_intercepted_data;
uint8_t jb_is_flushing; /* 1 if commit thread is actively flushing it.
*/
uint8_t jb_escaped;
+ uint8_t jb_awaiting_data; /* Reserved by
+ journal_get_write_access_external, and
+ the shadow has no image of the block's
+ current owner yet. */
} journal_buffer_t;
/**
@@ -665,6 +676,7 @@ journal_alloc_buffer (journal_t *journal)
jb->jb_is_written = 0;
jb->jb_is_flushing = 0;
jb->jb_escaped = 0;
+ jb->jb_awaiting_data = 0;
goto out;
}
jb = calloc (1, sizeof (journal_buffer_t));
@@ -2033,10 +2045,13 @@ out:
/**
* Adds a modified filesystem block to the SPECIFIC transaction handle.
- * Defers the actual memory copy until the transaction stops.
+ * With HYDRATE set, defers the actual memory copy until the transaction
+ * stops. With HYDRATE clear, the block's live bytes are outside the disk
+ * cache, and journal_mark_dirty_external supplies the copy.
*/
static error_t
-journal_get_write_access_locked (diskfs_transaction_t *txn, block_t fs_blocknr)
+journal_get_write_access_locked (diskfs_transaction_t *txn,
+ block_t fs_blocknr, int hydrate)
{
journal_buffer_t *jb;
journal_buffer_t *new_jb;
@@ -2059,7 +2074,13 @@ journal_get_write_access_locked (diskfs_transaction_t
*txn, block_t fs_blocknr)
/* Even if it is already contained in the map we have still been
* notified about the new change and therefore we must copy it
* over once more when the time comes. */
- jb->needs_copy = 1;
+ if (hydrate)
+ jb->jb_awaiting_data = 0;
+ else if (jb->needs_copy)
+ /* The shadow is due to be copied from the disk cache, which does
+ * not hold the caller's edit. */
+ jb->jb_awaiting_data = 1;
+ jb->needs_copy = hydrate;
if (jb->jb_is_written)
{
/* The pager rushed this block previously, but VFS is dirtying it
again.
@@ -2086,7 +2107,8 @@ journal_get_write_access_locked (diskfs_transaction_t
*txn, block_t fs_blocknr)
}
/* Brand new block for us? We will definitively want to copy it over. */
- new_jb->needs_copy = 1;
+ new_jb->needs_copy = hydrate;
+ new_jb->jb_awaiting_data = !hydrate;
txn->t_outstanding_io++;
journal_logged_ref_locked (ext2_journal, fs_blocknr);
out:
@@ -2220,7 +2242,7 @@ journal_get_write_access (diskfs_transaction_t *txn,
block_t fs_blocknr)
}
JOURNAL_LOCK (ext2_journal);
- err = journal_get_write_access_locked (txn, fs_blocknr);
+ err = journal_get_write_access_locked (txn, fs_blocknr, 1);
if (err)
JRNL_LOG_WARN
("Didn't manage to get journal write access for block %u. (%s).",
@@ -2260,6 +2282,119 @@ journal_thread_transaction (void)
return txn;
}
+/* Adds FS_BLOCKNR, a metadata block whose live bytes are outside the disk
+ cache, to TXN before it is edited. From here on pager writes of the
+ block are intercepted. The caller supplies the edited image through
+ journal_mark_dirty_external, or gives the block up through
+ journal_release_access. */
+error_t
+journal_get_write_access_external (diskfs_transaction_t *txn,
+ block_t fs_blocknr)
+{
+ error_t err;
+
+ if (!ext2_journal || !txn)
+ return 0;
+
+ JOURNAL_LOCK (ext2_journal);
+ err = journal_get_write_access_locked (txn, fs_blocknr, 0);
+ if (err)
+ JRNL_LOG_WARN
+ ("Didn't manage to get journal write access for block %u. (%s).",
+ fs_blocknr, strerror (err));
+ JOURNAL_UNLOCK (ext2_journal);
+ return err;
+}
+
+/* Copies DATA (block_size bytes) into TXN as the image of FS_BLOCKNR,
+ which journal_get_write_access_external added to it. DATA may be a
+ mapping of the block, so it is read into the scratch buffer before the
+ journal lock is taken: a fault under the lock would re-enter the
+ journal through the pager. Returns EIO when DATA cannot be read. */
+error_t
+journal_mark_dirty_external (diskfs_transaction_t *txn, block_t fs_blocknr,
+ const void *data)
+{
+ journal_buffer_t *jb;
+ error_t err;
+
+ if (!ext2_journal || !txn)
+ return 0;
+
+ pthread_mutex_lock (&journal_external_scratch_lock);
+ err = hurd_safe_copyin (journal_external_scratch, data, block_size);
+ if (err)
+ {
+ pthread_mutex_unlock (&journal_external_scratch_lock);
+ JRNL_LOG_WARN ("Cannot read the image of block %u. (%s).",
+ fs_blocknr, strerror (err));
+ return EIO;
+ }
+
+ JOURNAL_LOCK (ext2_journal);
+ jb = journal_map_lookup (&txn->t_buffer_map, fs_blocknr);
+ if (jb)
+ {
+ /* Only the committing transaction and the checkpoint list are
+ notified of home writes, and TXN is neither while the caller's
+ handle is open. */
+ assert_backtrace (!jb->jb_is_written);
+ memcpy (jb->jb_shadow_data, journal_external_scratch, block_size);
+ /* A pager write during the edit left a chunk behind. The chunk
+ backs reads of this block until its home write, so it holds the
+ image that commits. */
+ if (jb->jb_intercepted_data)
+ memcpy (jb->jb_intercepted_data, journal_external_scratch,
+ block_size);
+ jb->needs_copy = 0;
+ jb->jb_awaiting_data = 0;
+ }
+ else
+ JRNL_LOG_WARN ("mark_dirty_external called on unreserved block %u! "
+ "Missing get_write_access_external?", fs_blocknr);
+ JOURNAL_UNLOCK (ext2_journal);
+ pthread_mutex_unlock (&journal_external_scratch_lock);
+ return 0;
+}
+
+/* Gives up FS_BLOCKNR when TXN has no image of it yet, so commit does not
+ log an empty shadow. A pager write intercepted since the reservation
+ holds the page as it is, so that image stays in TXN instead. */
+void
+journal_release_access (diskfs_transaction_t *txn, block_t fs_blocknr)
+{
+ journal_buffer_t *jb;
+
+ if (!ext2_journal || !txn)
+ return;
+
+ JOURNAL_LOCK (ext2_journal);
+ jb = journal_map_lookup (&txn->t_buffer_map, fs_blocknr);
+ if (!jb || !jb->jb_awaiting_data)
+ goto out;
+
+ if (jb->jb_intercepted_data)
+ {
+ memcpy (jb->jb_shadow_data, jb->jb_intercepted_data, block_size);
+ jb->jb_awaiting_data = 0;
+ goto out;
+ }
+
+ /* A buffer the stop-time sweep is copying must not be freed: the sweep
+ walks its own list of them unlocked. The sweep only takes buffers
+ with needs_copy set, and an external reservation clears it. It
+ would be set only if TXN had also reserved the block through the
+ disk cache, which a directory block never is. */
+ assert_backtrace (!jb->needs_copy);
+ journal_map_remove (&txn->t_buffer_map, fs_blocknr);
+ if (!jb->jb_is_written && txn->t_outstanding_io > 0)
+ txn->t_outstanding_io--;
+ journal_logged_unref_locked (ext2_journal, fs_blocknr);
+ journal_free_buffer (ext2_journal, jb);
+out:
+ JOURNAL_UNLOCK (ext2_journal);
+}
+
/* Two flags. journal_thread_sync tells this thread's RPC tail to commit
rather than stop. txn->sync_needed tells the outermost stop to wake
kjournald when t_active_threads reaches 0, for callers that never commit
@@ -2363,6 +2498,7 @@ journal_write_payload (journal_t *journal, const
diskfs_transaction_t *txn)
while ((jb = journal_map_iterate (&txn->t_buffer_map, &iter)) != NULL)
{
assert_backtrace (!jb->needs_copy);
+ assert_backtrace (!jb->jb_awaiting_data);
/* If the descriptor block is full, flush the current batch first */
if (tag_offset + sizeof (journal_block_tag_t) > block_size)
@@ -2790,7 +2926,10 @@ journal_create (struct node *journal_inode)
j->j_min_free = j->j_max_transaction_buffers + JRNL_METADATA_OVERHEAD;
j->j_descriptor_buf = malloc (block_size);
j->j_commit_buf = malloc (block_size);
- if (!j->j_descriptor_buf || !j->j_commit_buf)
+ /* Shared by every journal this translator starts. */
+ if (!journal_external_scratch)
+ journal_external_scratch = malloc (block_size);
+ if (!j->j_descriptor_buf || !j->j_commit_buf || !journal_external_scratch)
ext2_panic ("No RAM for commit buffers!");
if (journal_load_superblock (j) != 0)
diff --git a/ext2fs/journal.h b/ext2fs/journal.h
index 06460445d..bb2cba104 100644
--- a/ext2fs/journal.h
+++ b/ext2fs/journal.h
@@ -80,6 +80,30 @@ error_t
journal_store_read (block_t start_block, size_t length, void **buf,
size_t *read_amount);
+/**
+ * Same as journal_get_write_access, for a metadata block whose live bytes
+ * are outside the disk cache, such as a directory block edited through
+ * the directory's file pager. The journal makes no copy of its own; the
+ * caller supplies the edited image through journal_mark_dirty_external,
+ * or gives the block up through journal_release_access.
+ */
+error_t journal_get_write_access_external (diskfs_transaction_t *txn,
+ block_t fs_blocknr);
+
+/**
+ * Hands the journal the edited image of FS_BLOCKNR, block_size bytes at
+ * DATA. DATA may be a mapping of the block. Returns EIO when DATA
+ * cannot be read.
+ */
+error_t journal_mark_dirty_external (diskfs_transaction_t *txn,
+ block_t fs_blocknr, const void *data);
+
+/**
+ * Gives up FS_BLOCKNR, reserved with journal_get_write_access_external,
+ * when the transaction has no image of it yet.
+ */
+void journal_release_access (diskfs_transaction_t *txn, block_t fs_blocknr);
+
/**
* Records a range of deleted blocks so they can be unpinned from older
* checkpoint lists AFTER this transaction safely commits. Returns 1 if
--
2.56.0