blob: 1f71d10b517521daba8a45a1de5d3c2dcdf5a78a [file] [log] [blame]
/*
ext2_block_relocator.c -- ext2 block relocator
Copyright (C) 1998-2000 Free Software Foundation, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <config.h>
#ifndef DISCOVER_ONLY
#include <stdio.h>
#include <stdlib.h>
#include "ext2.h"
/* This struct describes a single block that will be relocated. The
* block's original location is "num", and its new location is "dest".
* The block is presumebly referred to by some other block in the file
* system, which is recorded as "refblock". (Only one reference to
* the block is allowed by the block relocator.) "refoffset" describes
* the location within the refblock in which the block is referenced.
* "isindirect" is 0 for direct, 1 for single-indirect, 2 for
* double-indirect, etc.
*
* The algorithms in the file fill the entries of this struct in this order:
* num, refblock/refoffset/isindirectblock, dest.
*/
struct ext2_block_entry
{
blk_t num;
blk_t dest;
blk_t refblock;
unsigned refoffset:16;
unsigned isindirectblock:16;
};
/* This struct contains all data structures relevant to the block relocator.
* - newallocoffset is the distance between the start of a block group,
* and the first data block in the group. This can change when a
* filesystem is resized, because the size of the group descriptors is
* proportional to the size of the filesystem.
*
* - allocentries is the size of the "block" array. It is a tuneable
* parameter that determines how many blocks can be moved in each
* pass.
*
* - usedentries says how many entries of the "block" array have been
* used. That is, how many blocks have been scheduled so far to
* be moved.
*
* - resolvedentries is the number of blocks whose referencing block
* has been found and recorded in block[.]->refblock, etc.
*
* - block is an array that records which blocks need to be moved, and
* where they will be moved to, etc. At some point in the algorithm, this
* array gets sorted (grep for qsort!) by indirectness.
*
* - start: each entry in this array corresponds to a level of
* indirectness (0-3). Each level has two items: dst and num. "num"
* is the number of blocks inside "block" of that level of indirectness.
* After doscan() is finished, and the level of indirectness of each
* block is known, "block" is sorted (see above). The "dst" pointer
* is a pointer inside "block" that indicates the start of the portion
* of the array containg blocks of that level of indirectness.
*/
struct ext2_block_relocator_state
{
blk_t newallocoffset;
blk_t allocentries;
blk_t usedentries;
blk_t resolvedentries;
struct ext2_block_entry *block;
struct {
struct ext2_block_entry *dst;
int num;
} start[4];
};
static int compare_block_entries(const void *x0, const void *x1)
{
const struct ext2_block_entry *b0;
const struct ext2_block_entry *b1;
b0 = (const struct ext2_block_entry *)x0;
b1 = (const struct ext2_block_entry *)x1;
if (b0->num < b1->num)
return -1;
if (b0->num > b1->num)
return 1;
return 0;
}
static int compare_block_entries_ind(const void *x0, const void *x1)
{
const struct ext2_block_entry *b0;
const struct ext2_block_entry *b1;
b0 = (const struct ext2_block_entry *)x0;
b1 = (const struct ext2_block_entry *)x1;
if (b0->isindirectblock > b1->isindirectblock)
return -1;
if (b0->isindirectblock < b1->isindirectblock)
return 1;
return 0;
}
static int compare_block_entries_ref(const void *x0, const void *x1)
{
const struct ext2_block_entry *b0;
const struct ext2_block_entry *b1;
b0 = (const struct ext2_block_entry *)x0;
b1 = (const struct ext2_block_entry *)x1;
if (b0->refblock < b1->refblock)
return -1;
if (b0->refblock > b1->refblock)
return 1;
return 0;
}
struct ext2_block_entry *findit(struct ext2_block_relocator_state *state, blk_t block)
{
int min;
int max;
struct ext2_block_entry *retv;
int t;
blk_t tval;
max = state->usedentries - 1;
min = 0;
retv = NULL;
repeat:
if (min > max)
goto out;
t = (min + max) >> 1;
tval = state->block[t].num;
if (tval > block)
max = t - 1;
if (tval < block)
min = t + 1;
if (tval != block)
goto repeat;
retv = &state->block[t];
out:
return retv;
}
/* This function adds records a reference to a block ("blk"), if that
* block is scheduled to be moved.
*/
static int doblock(struct ext2_fs *fs,
struct ext2_block_relocator_state *state,
blk_t blk,
blk_t refblock,
off_t refoffset,
int indirect)
{
struct ext2_block_entry *ent;
if ((ent = findit(state, blk)) == NULL)
return 1;
if (ent->refblock)
{
ped_exception_throw (PED_EXCEPTION_ERROR, PED_EXCEPTION_CANCEL,
_("Cross-linked blocks found! Better go run e2fsck "
"first!"));
return 0;
}
ent->refblock = refblock;
ent->refoffset = refoffset;
ent->isindirectblock = indirect;
state->resolvedentries++;
state->start[indirect].num++;
return 1;
}
static int doindblock(struct ext2_fs *fs,
struct ext2_block_relocator_state *state,
blk_t blk,
blk_t refblock,
off_t refoffset)
{
struct ext2_buffer_head *bh;
int i;
uint32_t *uptr;
if (!doblock(fs, state, blk, refblock, refoffset, 1))
return 0;
bh = ext2_bread(fs, blk);
if (!bh)
return 0;
uptr = (uint32_t *)bh->data;
for (i=0;i<(fs->blocksize >> 2);i++)
if (uptr[i])
if (!doblock(fs, state, PED_LE32_TO_CPU(uptr[i]), blk,
i<<2, 0))
return 0;
if (!ext2_brelse(bh, 0))
return 0;
return 1;
}
static int dodindblock(struct ext2_fs *fs,
struct ext2_block_relocator_state *state,
blk_t blk,
blk_t refblock,
off_t refoffset)
{
struct ext2_buffer_head *bh;
int i;
uint32_t *uptr;
if (!doblock(fs, state, blk, refblock, refoffset, 2))
return 0;
bh = ext2_bread(fs, blk);
if (!bh)
return 0;
uptr = (uint32_t *)bh->data;
for (i=0;i<(fs->blocksize >> 2);i++)
if (uptr[i])
if (!doindblock(fs, state, PED_LE32_TO_CPU(uptr[i]),
blk, i<<2))
return 0;
if (!ext2_brelse(bh, 0))
return 0;
return 1;
}
static int dotindblock(struct ext2_fs *fs,
struct ext2_block_relocator_state *state,
blk_t blk,
blk_t refblock,
off_t refoffset)
{
struct ext2_buffer_head *bh;
int i;
uint32_t *uptr;
if (!doblock(fs, state, blk, refblock, refoffset, 3))
return 0;
bh = ext2_bread(fs, blk);
if (!bh)
return 0;
uptr = (uint32_t *)bh->data;
for (i=0;i<(fs->blocksize >> 2);i++)
if (uptr[i])
if (!dodindblock(fs, state, PED_LE32_TO_CPU(uptr[i]),
blk, i<<2))
return 0;
if (!ext2_brelse(bh, 0))
return 0;
return 1;
}
/* This function records any block references from an inode to blocks that are
* scheduled to be moved.
*/
static int doinode(struct ext2_fs *fs, struct ext2_block_relocator_state *state, int inode)
{
struct ext2_inode buf;
if (!ext2_read_inode(fs, inode, &buf))
return 0;
if (EXT2_INODE_BLOCKS(buf))
{
blk_t blk;
int i;
off_t inodeoffset;
blk_t inodeblock;
inodeoffset = ext2_get_inode_offset(fs, inode, &inodeblock);
/* do Hurd block, if there is one... */
if (EXT2_SUPER_CREATOR_OS(fs->sb) == EXT2_OS_HURD
&& EXT2_INODE_TRANSLATOR(buf)) {
if (!doblock(fs,
state,
EXT2_INODE_TRANSLATOR(buf),
inodeblock,
inodeoffset + offsetof(struct ext2_inode,
osd1.hurd1.h_i_translator),
0))
return 0;
}
for (i=0;i<EXT2_NDIR_BLOCKS;i++)
if ((blk = EXT2_INODE_BLOCK(buf, i)) != 0)
if (!doblock(fs,
state,
blk,
inodeblock,
inodeoffset + offsetof(struct ext2_inode, i_block[i]),
0))
return 0;
if ((blk = EXT2_INODE_BLOCK(buf, EXT2_IND_BLOCK)) != 0)
if (!doindblock(fs,
state,
blk,
inodeblock,
inodeoffset + offsetof(struct ext2_inode, i_block[EXT2_IND_BLOCK])))
return 0;
if ((blk = EXT2_INODE_BLOCK(buf, EXT2_DIND_BLOCK)) != 0)
if (!dodindblock(fs,
state,
blk,
inodeblock,
inodeoffset + offsetof(struct ext2_inode, i_block[EXT2_DIND_BLOCK])))
return 0;
if ((blk = EXT2_INODE_BLOCK(buf, EXT2_TIND_BLOCK)) != 0)
if (!dotindblock(fs,
state,
blk,
inodeblock,
inodeoffset + offsetof(struct ext2_inode, i_block[EXT2_TIND_BLOCK])))
return 0;
}
return 1;
}
/* This function scans the entire filesystem, to find all references to blocks
* that are scheduled to be moved.
*/
static int doscan(struct ext2_fs *fs, struct ext2_block_relocator_state *state)
{
int i;
state->start[0].num = 0;
state->start[1].num = 0;
state->start[2].num = 0;
state->start[3].num = 0;
for (i=0;i<fs->numgroups;i++)
{
struct ext2_buffer_head *bh;
unsigned int j;
int offset;
if (fs->opt_verbose)
{
fprintf(stderr, " scanning group %i.... ", i);
fflush(stderr);
}
bh = ext2_bread(fs, EXT2_GROUP_INODE_BITMAP(fs->gd[i]));
if (!bh)
return 0;
offset = i * EXT2_SUPER_INODES_PER_GROUP(fs->sb) + 1;
for (j=0;j<EXT2_SUPER_INODES_PER_GROUP(fs->sb);j++)
if (bh->data[j>>3] & _bitmap[j&7])
{
if (!doinode(fs, state, offset + j))
{
ext2_brelse(bh, 0);
return 0;
}
if (state->resolvedentries == state->usedentries)
break;
}
ext2_brelse(bh, 0);
if (fs->opt_verbose)
{
fprintf(stderr, "%i/%i blocks resolved\r",
state->resolvedentries,
state->usedentries);
fflush(stderr);
}
if (state->resolvedentries == state->usedentries)
break;
}
if (fs->opt_verbose)
fputc('\n', stderr);
state->start[3].dst = state->block;
state->start[2].dst = state->start[3].dst + state->start[3].num;
state->start[1].dst = state->start[2].dst + state->start[2].num;
state->start[0].dst = state->start[1].dst + state->start[1].num;
return 1;
}
static int ext2_block_relocator_copy(struct ext2_fs *fs, struct ext2_block_relocator_state *state)
{
unsigned char *buf;
ped_exception_fetch_all();
buf = (unsigned char *) ped_malloc(MAXCONT << fs->logsize);
if (buf)
{
int num;
int numleft;
struct ext2_block_entry *ptr;
ped_exception_leave_all();
numleft = state->usedentries;
ptr = state->block;
while (numleft)
{
num = PED_MIN(numleft, MAXCONT);
while (num != 1)
{
if (ptr[0].num + num-1 == ptr[num-1].num &&
ptr[0].dest + num-1 == ptr[num-1].dest)
break;
num >>= 1;
}
if (!ext2_bcache_flush_range(fs, ptr[0].num, num))
goto error_free_buf;
if (!ext2_bcache_flush_range(fs, ptr[0].dest, num))
goto error_free_buf;
if (!ext2_read_blocks(fs, buf, ptr[0].num, num))
goto error_free_buf;
if (!ext2_write_blocks(fs, buf, ptr[0].dest, num))
goto error_free_buf;
ptr += num;
numleft -= num;
if (fs->opt_verbose)
{
fprintf(stderr, "copied %i/%i blocks\r",
state->usedentries - numleft,
state->usedentries);
fflush(stderr);
}
}
ped_free(buf);
if (fs->opt_safe)
ext2_sync(fs);
if (fs->opt_verbose)
fputc('\n', stderr);
}
else
{
blk_t i;
ped_exception_catch();
ped_exception_leave_all();
for (i=0;i<state->usedentries;i++)
{
struct ext2_block_entry *block;
block = &state->block[i];
if (!ext2_copy_block(fs, block->num, block->dest))
goto error;
}
}
return 1;
error_free_buf:
ped_free(buf);
error:
return 0;
}
static int ext2_block_relocator_ref(struct ext2_fs *fs, struct ext2_block_relocator_state *state, struct ext2_block_entry *block)
{
struct ext2_buffer_head *bh;
static int numerrors = 0;
if (!(block->refblock || block->refoffset))
{
ped_exception_throw (PED_EXCEPTION_BUG, PED_EXCEPTION_CANCEL,
_("Block %i has no reference? Weird."),
block->num);
return 0;
}
bh = ext2_bread(fs, block->refblock);
if (!bh)
return 0;
if (fs->opt_debug)
{
if (PED_LE32_TO_CPU(*((uint32_t *)(bh->data + block->refoffset)))
!= block->num) {
fprintf(stderr,
"block %i ref error! (->%i {%i, %i})\n",
block->num,
block->dest,
block->refblock,
block->refoffset);
ext2_brelse(bh, 0);
if (numerrors++ < 4)
return 1;
fputs("all is not well!\n", stderr);
return 0;
}
}
*((uint32_t *)(bh->data + block->refoffset))
= PED_LE32_TO_CPU(block->dest);
bh->dirty = 1;
ext2_brelse(bh, 0);
ext2_set_block_state(fs, block->dest, 1, 1);
ext2_set_block_state(fs, block->num, 0, 1);
if (block->isindirectblock)
{
struct ext2_block_entry *dst;
int i;
int num;
dst = state->start[block->isindirectblock-1].dst;
num = state->start[block->isindirectblock-1].num;
for (i=0;i<num;i++)
if (dst[i].refblock == block->num)
dst[i].refblock = block->dest;
}
return 1;
}
/* This function allocates new locations for blocks that are scheduled to move
* (inside state->blocks).
*
* FIXME: doesn't seem to handle sparse block groups. That is, there might be
* some free space that could be exploited in resizing that currently isn't...
*
* FIXME: should throw an exception if it fails to allocate blocks.
*/
static int ext2_block_relocator_grab_blocks(struct ext2_fs *fs, struct ext2_block_relocator_state *state)
{
int i;
blk_t ptr;
ptr = 0;
for (i=0;i<fs->numgroups;i++)
if (EXT2_GROUP_FREE_BLOCKS_COUNT(fs->gd[i]))
{
struct ext2_buffer_head *bh;
unsigned int j;
int offset;
bh = ext2_bread(fs, EXT2_GROUP_BLOCK_BITMAP(fs->gd[i]));
offset = i * EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb)
+ EXT2_SUPER_FIRST_DATA_BLOCK(fs->sb);
for (j=state->newallocoffset;
j<EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb);
j++)
if (!(bh->data[j>>3] & _bitmap[j&7]))
{
state->block[ptr++].dest = offset + j;
if (ptr == state->usedentries)
{
ext2_brelse(bh, 0);
return 1;
}
}
ext2_brelse(bh, 0);
}
return 0;
}
static int ext2_block_relocator_flush(struct ext2_fs *fs, struct ext2_block_relocator_state *state)
{
int i;
if (!state->usedentries)
return 1;
if (fs->opt_verbose)
fputs("ext2_block_relocator_flush\n", stderr);
if (fs->opt_debug)
{
again:
for (i=0; (unsigned int) i < state->usedentries-1; i++)
if (state->block[i].num >= state->block[i+1].num)
{
fputs("ext2_block_relocator_flush: "
"blocks not in order!\n", stderr);
qsort(state->block,
state->usedentries,
sizeof(struct ext2_block_entry),
compare_block_entries);
goto again;
}
}
if (!doscan(fs, state))
return 0;
if (!ext2_block_relocator_grab_blocks(fs, state))
return 0;
if (!ext2_block_relocator_copy(fs, state))
return 0;
qsort(state->block,
state->usedentries,
sizeof(struct ext2_block_entry),
compare_block_entries_ind);
for (i=3;i>=0;i--)
{
struct ext2_block_entry *dst;
int j;
int num;
dst = state->start[i].dst;
num = state->start[i].num;
if (!num)
continue;
if (fs->opt_verbose)
{
/* FIXXXME gross hack */
fprintf(stderr, "relocating %s blocks",
((char *[4]){"direct",
"singly indirect",
"doubly indirect",
"triply indirect"})[i]);
fflush(stderr);
}
qsort(dst,
num,
sizeof(struct ext2_block_entry),
compare_block_entries_ref);
for (j=0;j<num;j++)
if (!ext2_block_relocator_ref(fs, state, &dst[j]))
return 0;
if (fs->opt_safe) {
if (!ext2_sync(fs))
return 0;
}
if (fs->opt_verbose)
fputc('\n', stderr);
}
state->usedentries = 0;
state->resolvedentries = 0;
return 1;
}
static int ext2_block_relocator_mark(struct ext2_fs *fs, struct ext2_block_relocator_state *state, blk_t block)
{
int i;
if (fs->opt_debug)
{
if (!ext2_get_block_state(fs, block) ||
!ext2_is_data_block(fs, block))
{
ped_exception_throw (PED_EXCEPTION_WARNING,
PED_EXCEPTION_IGNORE,
_("Block %i shouldn't have been marked "
"(%d, %d)!"), block,
ext2_get_block_state(fs, block),
ext2_is_data_block(fs, block));
}
}
if (state->usedentries == state->allocentries - 1)
if (!ext2_block_relocator_flush(fs, state))
return 0;
i = state->usedentries;
state->block[i].num = block;
state->block[i].dest = 0;
state->block[i].refblock = 0;
state->block[i].refoffset = 0;
state->usedentries++;
return 1;
}
static int ext2_block_relocate_grow(struct ext2_fs *fs, struct ext2_block_relocator_state *state, blk_t newsize)
{
blk_t newgdblocks;
blk_t newitoffset;
int i;
newgdblocks = ped_div_round_up (newsize
- EXT2_SUPER_FIRST_DATA_BLOCK(fs->sb),
EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb));
newgdblocks = ped_div_round_up (newgdblocks
* sizeof(struct ext2_group_desc),
fs->blocksize);
if (newgdblocks == fs->gdblocks)
return 1;
newitoffset = newgdblocks + 3;
state->newallocoffset = newitoffset + fs->inodeblocks;
for (i=0;i<fs->numgroups;i++)
{
struct ext2_buffer_head *bh;
blk_t diff;
blk_t j;
blk_t start;
int sparse;
bh = ext2_bread(fs, EXT2_GROUP_BLOCK_BITMAP(fs->gd[i]));
start = (i * EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb))
+ EXT2_SUPER_FIRST_DATA_BLOCK(fs->sb);
sparse = ext2_is_group_sparse(fs, i);
if (EXT2_GROUP_INODE_TABLE(fs->gd[i]) < start + newitoffset
|| (sparse && ((EXT2_GROUP_BLOCK_BITMAP(fs->gd[i])
< start + newitoffset - 2)
|| (EXT2_GROUP_INODE_BITMAP(fs->gd[i])
< start + newitoffset - 1))))
{
diff = newitoffset - (EXT2_GROUP_INODE_TABLE(fs->gd[i])
- start);
for (j=0;j<diff;j++)
{
blk_t k;
k = EXT2_GROUP_INODE_TABLE(fs->gd[i])
+ fs->inodeblocks + j;
if (bh->data[k>>3] & _bitmap[k&7])
if (!ext2_block_relocator_mark(fs,
state, start + k))
{
ext2_brelse(bh, 0);
return 0;
}
}
}
ext2_brelse(bh, 0);
}
if (!ext2_block_relocator_flush(fs, state))
return 0;
return 1;
}
static int ext2_block_relocate_shrink(struct ext2_fs *fs, struct ext2_block_relocator_state *state, blk_t newsize)
{
int diff;
int i;
diff = ped_div_round_up (newsize - EXT2_SUPER_FIRST_DATA_BLOCK(fs->sb),
EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb));
diff = ped_div_round_up (diff * sizeof(struct ext2_group_desc),
fs->blocksize);
diff = fs->gdblocks - diff;
state->newallocoffset = fs->itoffset + fs->inodeblocks;
for (i=0;i<fs->numgroups;i++)
{
struct ext2_buffer_head *bh;
blk_t groupsize;
blk_t j;
blk_t offset;
int sparse;
blk_t start;
int type;
offset = i * EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb)
+ EXT2_SUPER_FIRST_DATA_BLOCK(fs->sb);
sparse = ext2_is_group_sparse(fs, i);
if (newsize >= offset + EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb))
continue; /* group will survive */
bh = ext2_bread(fs, EXT2_GROUP_BLOCK_BITMAP(fs->gd[i]));
if (newsize <= offset)
type = 2; /* group is fully chopped off */
else
type = 1; /* group is partly chopped off */
if (!sparse && type == 2)
{
for (j=EXT2_GROUP_INODE_BITMAP(fs->gd[i])+1;
j<EXT2_GROUP_INODE_TABLE(fs->gd[i]);
j++)
{
blk_t k;
k = j - offset;
if (bh->data[k>>3] & _bitmap[k&7])
if (!ext2_block_relocator_mark(fs, state, j))
{
ext2_brelse(bh, 0);
return 0;
}
}
}
start = newsize;
if (type == 2)
start = EXT2_GROUP_INODE_TABLE(fs->gd[i])
+ fs->inodeblocks;
start -= offset;
groupsize = EXT2_SUPER_BLOCKS_PER_GROUP(fs->sb);
if (offset + groupsize > EXT2_SUPER_BLOCKS_COUNT(fs->sb))
groupsize = EXT2_SUPER_BLOCKS_COUNT(fs->sb) - offset;
for (j=start;j<groupsize;j++)
if (bh->data[j>>3] & _bitmap[j&7])
if (!ext2_block_relocator_mark(fs, state,
offset + j))
{
ext2_brelse(bh, 0);
return 0;
}
ext2_brelse(bh, 0);
}
return ext2_block_relocator_flush(fs, state);
}
int ext2_block_relocate(struct ext2_fs *fs, blk_t newsize)
{
struct ext2_block_relocator_state state;
if (fs->opt_verbose)
fputs("relocating blocks....\n", stderr);
state.newallocoffset = 0;
state.allocentries = (ext2_relocator_pool_size << 10) /
sizeof(struct ext2_block_entry);
state.usedentries = 0;
state.resolvedentries = 0;
state.block = (struct ext2_block_entry *)fs->relocator_pool;
if (newsize < EXT2_SUPER_BLOCKS_COUNT(fs->sb))
return ext2_block_relocate_shrink(fs, &state, newsize);
return ext2_block_relocate_grow(fs, &state, newsize);
}
#endif /* !DISCOVER_ONLY */