Preallocated blocks were marked allocated in the block bitmap while
only the node's memory owned them, so a crash between the allocation
and the discard leaked them. With a journal, a normal boot replays the
log without a full check, and nothing frees them until e2fsck -f.
Killing the translator during tar -xf left such blocks repeatedly.
Reserve the preallocation window in memory instead, in a per-group map
next to the busy blocks. ext2_new_block skips other nodes' reserved
blocks, takes them only when nothing else is free, and sets a block in
the bitmap only when a file takes it. The window is dropped under
alloc_lock, which ext2_alloc_block holds while it uses it, and when the
node is freed. The layout does not change: 8 files appended to in turn
get 1949 fragments per 64 MB file, as before. Ten kills of the
translator at random times during rm -rf and tar -xf of a Linux kernel
tree all left e2fsck -f clean.
---
ext2fs/balloc.c | 274 ++++++++++++++++++++++++++++++++--------------
ext2fs/ext2fs.h | 6 +-
ext2fs/getblk.c | 57 +++++-----
ext2fs/inode.c | 1 +
ext2fs/truncate.c | 4 +-
5 files changed, 226 insertions(+), 116 deletions(-)
diff --git a/ext2fs/balloc.c b/ext2fs/balloc.c
index 1a1418b9c..32eafc5d0 100644
--- a/ext2fs/balloc.c
+++ b/ext2fs/balloc.c
@@ -64,33 +64,61 @@ memscan (void *buf, unsigned char ch, size_t len)
static unsigned char **busy_maps; /* A bitmap per group, or NULL */
static uint32_t *busy_counts; /* Busy blocks in each group */
static unsigned long busy_total;
-static unsigned char *busy_scratch; /* Bitmap ext2_new_block searches */
-/* Make sure group BLOCK_GROUP has a busy bitmap. Returns 0 without
- memory for it. */
+/* Blocks in a node's preallocation window are reserved. The bitmap
+ shows them free, and only memory holds the reservation, so a crash
+ leaks nothing. ext2_new_block skips them for other nodes, and takes
+ them when nothing else is free. A reserved block is always free in
+ the bitmap: allocating a block clears its reservation. Protected by
+ global_lock. */
+static unsigned char **rsv_maps; /* A bitmap per group, or NULL */
+static uint32_t *rsv_counts; /* Reserved blocks in each group */
+static unsigned long rsv_total;
+
+static unsigned char *search_scratch; /* Bitmap ext2_new_block searches */
+
+/* Make sure group BLOCK_GROUP has a bitmap in *MAPS, the busy or the
+ reserved ones. Returns 0 without memory for it. */
static int
-busy_map_ready (unsigned long block_group)
+overlay_ready (unsigned char ***maps, uint32_t **counts,
+ unsigned long block_group)
{
- if (!busy_maps)
+ if (!search_scratch)
+ {
+ search_scratch = malloc (block_size);
+ if (!search_scratch)
+ return 0;
+ }
+
+ if (!*maps)
{
- busy_maps = calloc (groups_count, sizeof *busy_maps);
- busy_counts = calloc (groups_count, sizeof *busy_counts);
- busy_scratch = malloc (block_size);
- if (!busy_maps || !busy_counts || !busy_scratch)
+ *maps = calloc (groups_count, sizeof **maps);
+ *counts = calloc (groups_count, sizeof **counts);
+ if (!*maps || !*counts)
{
- free (busy_maps);
- free (busy_counts);
- free (busy_scratch);
- busy_maps = NULL;
- busy_counts = NULL;
- busy_scratch = NULL;
+ free (*maps);
+ free (*counts);
+ *maps = NULL;
+ *counts = NULL;
return 0;
}
}
- if (!busy_maps[block_group])
- busy_maps[block_group] = calloc (1, block_size);
- return busy_maps[block_group] != NULL;
+ if (!(*maps)[block_group])
+ (*maps)[block_group] = calloc (1, block_size);
+ return (*maps)[block_group] != NULL;
+}
+
+static inline int
+busy_map_ready (unsigned long block_group)
+{
+ return overlay_ready (&busy_maps, &busy_counts, block_group);
+}
+
+static inline int
+rsv_map_ready (unsigned long block_group)
+{
+ return overlay_ready (&rsv_maps, &rsv_counts, block_group);
}
/* Mark bit BIT of group BLOCK_GROUP busy. busy_map_ready must have
@@ -105,35 +133,103 @@ mark_busy (unsigned long block_group, unsigned long bit)
}
}
+/* Reserve bit BIT of group BLOCK_GROUP. rsv_map_ready must have
+ succeeded for the group. */
+static void
+mark_reserved (unsigned long block_group, unsigned long bit)
+{
+ if (!set_bit (bit, rsv_maps[block_group]))
+ {
+ rsv_counts[block_group]++;
+ rsv_total++;
+ }
+}
+
+static inline int
+is_busy (unsigned long block_group, unsigned long bit)
+{
+ return busy_maps && busy_maps[block_group]
+ && test_bit (bit, busy_maps[block_group]);
+}
+
+static inline int
+is_reserved (unsigned long block_group, unsigned long bit)
+{
+ return rsv_maps && rsv_maps[block_group]
+ && test_bit (bit, rsv_maps[block_group]);
+}
+
+static void
+unreserve (unsigned long block_group, unsigned long bit)
+{
+ if (rsv_maps && rsv_maps[block_group]
+ && clear_bit (bit, rsv_maps[block_group]))
+ {
+ rsv_counts[block_group]--;
+ rsv_total--;
+ }
+}
+
+/* Drop the reservation of COUNT blocks starting at BLOCK, all in one
+ group. Bits that another node's allocation already cleared are
+ skipped. */
+static void
+unreserve_blocks (block_t block, unsigned long count)
+{
+ unsigned long block_group =
+ (block - le32toh (sblock->s_first_data_block)) /
+ le32toh (sblock->s_blocks_per_group);
+ unsigned long bit =
+ (block - le32toh (sblock->s_first_data_block)) %
+ le32toh (sblock->s_blocks_per_group);
+
+ for (; count > 0; bit++, count--)
+ unreserve (block_group, bit);
+}
+
+void
+ext2_unreserve_blocks (block_t block, unsigned long count)
+{
+ pthread_spin_lock (&global_lock);
+ unreserve_blocks (block, count);
+ pthread_spin_unlock (&global_lock);
+}
+
/* Returns the free blocks of group BLOCK_GROUP that ext2_new_block can
- hand out. */
+ hand out, leaving out the reserved ones unless STEAL is true. */
static inline unsigned long
-group_allocatable (unsigned long block_group, struct ext2_group_desc *gdp)
+group_allocatable (unsigned long block_group, struct ext2_group_desc *gdp,
+ int steal)
{
unsigned long free_count = le16toh (gdp->bg_free_blocks_count);
- unsigned long busy = busy_counts ? busy_counts[block_group] : 0;
+ unsigned long held = busy_counts ? busy_counts[block_group] : 0;
- return free_count > busy ? free_count - busy : 0;
+ if (!steal && rsv_counts)
+ held += rsv_counts[block_group];
+ return free_count > held ? free_count - held : 0;
}
/* Returns the bitmap ext2_new_block searches in group BLOCK_GROUP: the
block bitmap BH, or a copy of it with the group's busy blocks marked
- in use. */
+ in use, and also its reserved ones unless STEAL is true. */
static unsigned char *
-search_map (unsigned long block_group, unsigned char *bh)
+search_map (unsigned long block_group, unsigned char *bh, int steal)
{
- uint32_t *dst, *src, *busy;
+ uint32_t *dst, *src, *busy = NULL, *rsv = NULL;
size_t i;
- if (!busy_counts || busy_counts[block_group] == 0)
+ if (busy_counts && busy_counts[block_group])
+ busy = (uint32_t *) busy_maps[block_group];
+ if (!steal && rsv_counts && rsv_counts[block_group])
+ rsv = (uint32_t *) rsv_maps[block_group];
+ if (!busy && !rsv)
return bh;
- dst = (uint32_t *) busy_scratch;
+ dst = (uint32_t *) search_scratch;
src = (uint32_t *) bh;
- busy = (uint32_t *) busy_maps[block_group];
for (i = 0; i < block_size / sizeof (uint32_t); i++)
- dst[i] = src[i] | busy[i];
- return busy_scratch;
+ dst[i] = src[i] | (busy ? busy[i] : 0) | (rsv ? rsv[i] : 0);
+ return search_scratch;
}
void
@@ -160,13 +256,13 @@ ext2_release_busy_blocks (block_t block, unsigned long
count)
pthread_spin_unlock (&global_lock);
}
-/* Free COUNT blocks starting at BLOCK. With BUSY set and a journal, the
- blocks stay busy until the transaction freeing them commits. When they
- cannot be kept busy, they stay allocated instead: reusing them could put
- a journal copy of their old contents over their new owner, while a
+/* Free COUNT blocks starting at BLOCK. With a journal, the blocks stay
+ busy until the transaction freeing them commits. When they cannot be
+ kept busy, they stay allocated instead: reusing them could put a
+ journal copy of their old contents over their new owner, while a
leaked block only costs space until e2fsck frees it. */
-static void
-free_blocks (block_t block, unsigned long count, int busy)
+void
+ext2_free_blocks (block_t block, unsigned long count)
{
unsigned char *bh;
unsigned long block_group;
@@ -237,7 +333,7 @@ free_blocks (block_t block, unsigned long count, int busy)
"block = %u, count = %lu",
block, count);
- int hold = busy && ext2_journal && txn;
+ int hold = ext2_journal && txn;
int leak = hold && (!busy_map_ready (block_group)
|| !journal_record_freed_blocks (txn, block, gcount));
if (leak)
@@ -274,24 +370,19 @@ free_blocks (block_t block, unsigned long count, int busy)
alloc_sync (0);
}
-void
-ext2_free_blocks (block_t block, unsigned long count)
-{
- free_blocks (block, count, 1);
-}
-
-void
-ext2_free_unused_blocks (block_t block, unsigned long count)
-{
- free_blocks (block, count, 0);
-}
-
/*
* ext2_new_block uses a goal block to assist allocation. If the goal is
* free, or there is a free block within 32 blocks of the goal, that block
* is allocated. Otherwise a forward search is made for a free block; within
* each block group the search first looks for an entire free byte in the block
- * bitmap, and then for any free bit if that fails.
+ * bitmap, and then for any free bit if that fails. Blocks reserved for
+ * other nodes are taken only when no other block is free.
+ *
+ * With PREALLOC_COUNT, the caller has a preallocation window of
+ * *PREALLOC_COUNT blocks starting at *PREALLOC_BLOCK. If it is not
+ * empty, GOAL is its first block, which is taken if it is still free.
+ * The window is then renewed after the allocated block: the rest of the
+ * old window after a hit, or up to PREALLOC_GOAL - 1 blocks otherwise.
*/
block_t
ext2_new_block (block_t goal,
@@ -304,6 +395,8 @@ ext2_new_block (block_t goal,
int i, j, k, tmp;
uint32_t lmap;
struct ext2_group_desc *gdp;
+ int steal = 0;
+ block_t rsv_len = prealloc_goal ? prealloc_goal - 1 : 0;
diskfs_transaction_t *txn = journal_thread_transaction ();
#ifdef EXT2FS_DEBUG
@@ -325,6 +418,30 @@ ext2_new_block (block_t goal,
ext2_debug ("goal=%u", goal);
+ if (prealloc_count && *prealloc_count)
+ {
+ /* The caller's window. Its blocks are free unless another node
+ took them when nothing else was free. */
+ block_t left = *prealloc_count - 1;
+
+ assert_backtrace (goal == *prealloc_block);
+ unreserve_blocks (*prealloc_block, *prealloc_count);
+ *prealloc_count = 0;
+ i = (goal - le32toh (sblock->s_first_data_block)) /
+ le32toh (sblock->s_blocks_per_group);
+ j = (goal - le32toh (sblock->s_first_data_block)) %
+ le32toh (sblock->s_blocks_per_group);
+ gdp = group_desc (i);
+ bh = disk_cache_block_ref (le32toh (gdp->bg_block_bitmap));
+ if (!test_bit (j, bh) && !is_busy (i, j))
+ {
+ rsv_len = left;
+ goto got_block;
+ }
+ disk_cache_block_deref (bh);
+ bh = NULL;
+ }
+
repeat:
assert_backtrace (bh == NULL);
/*
@@ -336,7 +453,7 @@ repeat:
i = (goal - le32toh (sblock->s_first_data_block)) /
le32toh (sblock->s_blocks_per_group);
gdp = group_desc (i);
- if (group_allocatable (i, gdp) > 0)
+ if (group_allocatable (i, gdp, steal) > 0)
{
j = ((goal - le32toh (sblock->s_first_data_block))
% le32toh (sblock->s_blocks_per_group));
@@ -345,7 +462,7 @@ repeat:
goal_attempts++;
#endif
bh = disk_cache_block_ref (le32toh (gdp->bg_block_bitmap));
- map = search_map (i, bh);
+ map = search_map (i, bh, steal);
ext2_debug ("goal is at %d:%d", i, j);
@@ -428,9 +545,15 @@ repeat:
if (i >= groups_count)
i = 0;
gdp = group_desc (i);
- if (group_allocatable (i, gdp) > 0)
+ if (group_allocatable (i, gdp, steal) > 0)
break;
}
+ if (k >= groups_count && !steal && rsv_total > 0)
+ {
+ /* Only reserved blocks are left: take them. */
+ steal = 1;
+ goto repeat;
+ }
if (k >= groups_count)
{
unsigned long busy = busy_total;
@@ -457,7 +580,7 @@ repeat:
}
assert_backtrace (bh == NULL);
bh = disk_cache_block_ref (le32toh (gdp->bg_block_bitmap));
- map = search_map (i, bh);
+ map = search_map (i, bh, steal);
r = memscan (map, 0, le32toh (sblock->s_blocks_per_group) >> 3);
j = (r - map) << 3;
if (j < le32toh (sblock->s_blocks_per_group))
@@ -502,8 +625,11 @@ got_block:
{
ext2_warning ("bit already set for block %d", j);
disk_cache_block_deref (bh);
+ bh = NULL;
goto repeat;
}
+ /* Taken from another node's window, when nothing else was free. */
+ unreserve (i, j);
/* Since due to bletcherousness block-modified bits are never turned off
when writing disk-pager pages, make sure they are here, in case this
@@ -517,36 +643,24 @@ got_block:
ext2_debug ("found bit %d", j);
- /*
- * Do block preallocation now if required.
- */
-#ifdef EXT2_PREALLOCATE
- if (prealloc_goal)
+ /* Renew the caller's window after the block. Only memory holds it;
+ the bitmap and the free counts do not change. */
+ if (prealloc_count)
{
*prealloc_count = 0;
*prealloc_block = tmp + 1;
- for (k = 1;
- k < prealloc_goal && (j + k) < le32toh (sblock->s_blocks_per_group);
k++)
- {
- if (test_bit (j + k, map) || set_bit (j + k, bh))
- break;
- (*prealloc_count)++;
-
- /* (See comment before the clear_bit above) */
- if (modified_global_blocks)
- {
- pthread_spin_lock (&modified_global_blocks_lock);
- clear_bit (tmp + k, modified_global_blocks);
- pthread_spin_unlock (&modified_global_blocks_lock);
- }
- }
- gdp->bg_free_blocks_count = htole16 (le16toh (gdp->bg_free_blocks_count)
-
- *prealloc_count);
- sblock->s_free_blocks_count = htole32 (le32toh
(sblock->s_free_blocks_count) -
- *prealloc_count);
- ext2_debug ("preallocated a further %u bits", *prealloc_count);
+ if (rsv_len > 0 && rsv_map_ready (i))
+ for (k = 1; k <= rsv_len
+ && (j + k) < le32toh (sblock->s_blocks_per_group); k++)
+ {
+ if (test_bit (j + k, bh) || is_busy (i, j + k)
+ || is_reserved (i, j + k))
+ break;
+ mark_reserved (i, j + k);
+ (*prealloc_count)++;
+ }
+ ext2_debug ("reserved a further %u blocks", *prealloc_count);
}
-#endif
j = tmp;
diff --git a/ext2fs/ext2fs.h b/ext2fs/ext2fs.h
index d9d21d6d8..0df45ced4 100644
--- a/ext2fs/ext2fs.h
+++ b/ext2fs/ext2fs.h
@@ -715,9 +715,9 @@ block_t ext2_new_block (block_t goal,
void ext2_free_blocks (block_t block, unsigned long count);
-/* Free COUNT blocks starting at BLOCK that never held data, such as
- discarded preallocations. ext2_new_block can hand them out at once. */
-void ext2_free_unused_blocks (block_t block, unsigned long count);
+/* Drop the reservation of COUNT blocks starting at BLOCK, a discarded
+ preallocation window. */
+void ext2_unreserve_blocks (block_t block, unsigned long count);
/* Make COUNT blocks starting at BLOCK, freed by a transaction that has
now committed, available to ext2_new_block again. */
diff --git a/ext2fs/getblk.c b/ext2fs/getblk.c
index 404aa62e1..9d4d2e247 100644
--- a/ext2fs/getblk.c
+++ b/ext2fs/getblk.c
@@ -38,25 +38,21 @@
#include <string.h>
#include "ext2fs.h"
-/*
- * ext2_discard_prealloc and ext2_alloc_block are atomic wrt. the
- * superblock in the same manner as are ext2_free_blocks and
- * ext2_new_block. We just wait on the super rather than locking it
- * here, since ext2_new_block will do the necessary locking and we
- * can't block until then.
- */
+/* Drop NODE's preallocation window. Only memory holds it, so this just
+ makes its blocks available to other nodes. The caller holds NODE's
+ alloc_lock, or has the only reference to NODE. */
void
ext2_discard_prealloc (struct node *node)
{
#ifdef EXT2_PREALLOCATE
- if (diskfs_node_disknode (node)->info.i_prealloc_count)
+ struct ext2_inode_info *info = &diskfs_node_disknode (node)->info;
+
+ if (info->i_prealloc_count)
{
- int i = diskfs_node_disknode (node)->info.i_prealloc_count;
ext2_debug ("discarding %d prealloced blocks for inode %d",
- i, node->cache_id);
- diskfs_node_disknode (node)->info.i_prealloc_count = 0;
- ext2_free_unused_blocks
- (diskfs_node_disknode (node)->info.i_prealloc_block, i);
+ info->i_prealloc_count, node->cache_id);
+ ext2_unreserve_blocks (info->i_prealloc_block, info->i_prealloc_count);
+ info->i_prealloc_count = 0;
}
#endif
}
@@ -74,39 +70,38 @@ ext2_alloc_block (struct node *node, block_t goal, int zero)
diskfs_transaction_t *txn = journal_thread_transaction ();
#ifdef EXT2_PREALLOCATE
- if (diskfs_node_disknode (node)->info.i_prealloc_count &&
- (goal == diskfs_node_disknode (node)->info.i_prealloc_block ||
- goal + 1 == diskfs_node_disknode (node)->info.i_prealloc_block))
+ struct ext2_inode_info *info = &diskfs_node_disknode (node)->info;
+
+ if (info->i_prealloc_count &&
+ (goal == info->i_prealloc_block || goal + 1 == info->i_prealloc_block))
{
- result = diskfs_node_disknode (node)->info.i_prealloc_block++;
- diskfs_node_disknode (node)->info.i_prealloc_count--;
+ goal = info->i_prealloc_block;
ext2_debug ("preallocation hit (%lu/%lu) => %u",
- ++alloc_hits, ++alloc_attempts, result);
+ ++alloc_hits, ++alloc_attempts, goal);
}
else
{
ext2_debug ("preallocation miss (%lu/%lu)",
alloc_hits, ++alloc_attempts);
ext2_discard_prealloc (node);
- result = ext2_new_block
- (goal,
- S_ISREG (node->dn_stat.st_mode)
- ? (sblock->s_prealloc_blocks ?: EXT2_DEFAULT_PREALLOC_BLOCKS)
- : (S_ISDIR (node->dn_stat.st_mode)
- && EXT2_HAS_COMPAT_FEATURE(sblock,
- EXT2_FEATURE_COMPAT_DIR_PREALLOC))
- ? sblock->s_prealloc_dir_blocks
- : 0,
- &diskfs_node_disknode (node)->info.i_prealloc_count,
- &diskfs_node_disknode (node)->info.i_prealloc_block);
}
+ result = ext2_new_block
+ (goal,
+ S_ISREG (node->dn_stat.st_mode)
+ ? (sblock->s_prealloc_blocks ?: EXT2_DEFAULT_PREALLOC_BLOCKS)
+ : (S_ISDIR (node->dn_stat.st_mode)
+ && EXT2_HAS_COMPAT_FEATURE(sblock,
+ EXT2_FEATURE_COMPAT_DIR_PREALLOC))
+ ? sblock->s_prealloc_dir_blocks
+ : 0,
+ &info->i_prealloc_count, &info->i_prealloc_block);
/* Trap trying to allocate superblock, block group descriptor table, or
beyond the end */
assert_backtrace (result == 0 ||
(result >= group_desc_block_end
&& result < store->size >> log2_block_size));
#else
- result = ext2_new_block (goal, 0, 0);
+ result = ext2_new_block (goal, 0, 0, 0);
#endif
if (result && zero)
diff --git a/ext2fs/inode.c b/ext2fs/inode.c
index e525d306f..00a89f6c6 100644
--- a/ext2fs/inode.c
+++ b/ext2fs/inode.c
@@ -78,6 +78,7 @@ void
diskfs_node_norefs (struct node *np)
{
ext2_orphan_drop_ram_link (np);
+ ext2_discard_prealloc (np);
if (diskfs_node_disknode (np)->dirents)
free (diskfs_node_disknode (np)->dirents);
assert_backtrace (!diskfs_node_disknode (np)->pager);
diff --git a/ext2fs/truncate.c b/ext2fs/truncate.c
index 365bc204d..f1f0de8b1 100644
--- a/ext2fs/truncate.c
+++ b/ext2fs/truncate.c
@@ -358,12 +358,12 @@ diskfs_truncate (struct node *node, off_t length)
diskfs_file_update (node, 1);
}
- ext2_discard_prealloc (node);
-
force_delayed_copies (node, length);
pthread_rwlock_wrlock (&diskfs_node_disknode (node)->alloc_lock);
+ ext2_discard_prealloc (node);
+
/* Update the size on disk; fsck will finish freeing blocks if necessary
should we crash. */
node->dn_stat.st_size = length;
--
2.56.0