mirror of
https://mirrors.bfsu.edu.cn/git/linux.git
synced 2024-11-15 08:14:15 +08:00
a692e13d87
During a scrub, or device replace, we can race with block group removal and allocation and trigger the following assertion failure: [7526.385524] assertion failed: cache->start == chunk_offset, in fs/btrfs/scrub.c:3817 [7526.387351] ------------[ cut here ]------------ [7526.387373] kernel BUG at fs/btrfs/ctree.h:3599! [7526.388001] invalid opcode: 0000 [#1] PREEMPT SMP DEBUG_PAGEALLOC PTI [7526.388970] CPU: 2 PID: 1158150 Comm: btrfs Not tainted 5.17.0-rc8-btrfs-next-114 #4 [7526.390279] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.14.0-0-g155821a1990b-prebuilt.qemu.org 04/01/2014 [7526.392430] RIP: 0010:assertfail.constprop.0+0x18/0x1a [btrfs] [7526.393520] Code: f3 48 c7 c7 20 (...) [7526.396926] RSP: 0018:ffffb9154176bc40 EFLAGS: 00010246 [7526.397690] RAX: 0000000000000048 RBX: ffffa0db8a910000 RCX: 0000000000000000 [7526.398732] RDX: 0000000000000000 RSI: ffffffff9d7239a2 RDI: 00000000ffffffff [7526.399766] RBP: ffffa0db8a911e10 R08: ffffffffa71a3ca0 R09: 0000000000000001 [7526.400793] R10: 0000000000000001 R11: 0000000000000000 R12: ffffa0db4b170800 [7526.401839] R13: 00000003494b0000 R14: ffffa0db7c55b488 R15: ffffa0db8b19a000 [7526.402874] FS: 00007f6c99c40640(0000) GS:ffffa0de6d200000(0000) knlGS:0000000000000000 [7526.404038] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [7526.405040] CR2: 00007f31b0882160 CR3: 000000014b38c004 CR4: 0000000000370ee0 [7526.406112] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000 [7526.407148] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400 [7526.408169] Call Trace: [7526.408529] <TASK> [7526.408839] scrub_enumerate_chunks.cold+0x11/0x79 [btrfs] [7526.409690] ? do_wait_intr_irq+0xb0/0xb0 [7526.410276] btrfs_scrub_dev+0x226/0x620 [btrfs] [7526.410995] ? preempt_count_add+0x49/0xa0 [7526.411592] btrfs_ioctl+0x1ab5/0x36d0 [btrfs] [7526.412278] ? __fget_files+0xc9/0x1b0 [7526.412825] ? kvm_sched_clock_read+0x14/0x40 [7526.413459] ? lock_release+0x155/0x4a0 [7526.414022] ? __x64_sys_ioctl+0x83/0xb0 [7526.414601] __x64_sys_ioctl+0x83/0xb0 [7526.415150] do_syscall_64+0x3b/0xc0 [7526.415675] entry_SYSCALL_64_after_hwframe+0x44/0xae [7526.416408] RIP: 0033:0x7f6c99d34397 [7526.416931] Code: 3c 1c e8 1c ff (...) [7526.419641] RSP: 002b:00007f6c99c3fca8 EFLAGS: 00000246 ORIG_RAX: 0000000000000010 [7526.420735] RAX: ffffffffffffffda RBX: 00005624e1e007b0 RCX: 00007f6c99d34397 [7526.421779] RDX: 00005624e1e007b0 RSI: 00000000c400941b RDI: 0000000000000003 [7526.422820] RBP: 0000000000000000 R08: 00007f6c99c40640 R09: 0000000000000000 [7526.423906] R10: 00007f6c99c40640 R11: 0000000000000246 R12: 00007fff746755de [7526.424924] R13: 00007fff746755df R14: 0000000000000000 R15: 00007f6c99c40640 [7526.425950] </TASK> That assertion is relatively new, introduced with commitd04fbe19ae
("btrfs: scrub: cleanup the argument list of scrub_chunk()"). The block group we get at scrub_enumerate_chunks() can actually have a start address that is smaller then the chunk offset we extracted from a device extent item we got from the commit root of the device tree. This is very rare, but it can happen due to a race with block group removal and allocation. For example, the following steps show how this can happen: 1) We are at transaction T, and we have the following blocks groups, sorted by their logical start address: [ bg A, start address A, length 1G (data) ] [ bg B, start address B, length 1G (data) ] (...) [ bg W, start address W, length 1G (data) ] --> logical address space hole of 256M, there used to be a 256M metadata block group here [ bg Y, start address Y, length 256M (metadata) ] --> Y matches W's end offset + 256M Block group Y is the block group with the highest logical address in the whole filesystem; 2) Block group Y is deleted and its extent mapping is removed by the call to remove_extent_mapping() made from btrfs_remove_block_group(). So after this point, the last element of the mapping red black tree, its rightmost node, is the mapping for block group W; 3) While still at transaction T, a new data block group is allocated, with a length of 1G. When creating the block group we do a call to find_next_chunk(), which returns the logical start address for the new block group. This calls returns X, which corresponds to the end offset of the last block group, the rightmost node in the mapping red black tree (fs_info->mapping_tree), plus one. So we get a new block group that starts at logical address X and with a length of 1G. It spans over the whole logical range of the old block group Y, that was previously removed in the same transaction. However the device extent allocated to block group X is not the same device extent that was used by block group Y, and it also does not overlap that extent, which must be always the case because we allocate extents by searching through the commit root of the device tree (otherwise it could corrupt a filesystem after a power failure or an unclean shutdown in general), so the extent allocator is behaving as expected; 4) We have a task running scrub, currently at scrub_enumerate_chunks(). There it searches for device extent items in the device tree, using its commit root. It finds a device extent item that was used by block group Y, and it extracts the value Y from that item into the local variable 'chunk_offset', using btrfs_dev_extent_chunk_offset(); It then calls btrfs_lookup_block_group() to find block group for the logical address Y - since there's currently no block group that starts at that logical address, it returns block group X, because its range contains Y. This results in triggering the assertion: ASSERT(cache->start == chunk_offset); right before calling scrub_chunk(), as cache->start is X and chunk_offset is Y. This is more likely to happen of filesystems not larger than 50G, because for these filesystems we use a 256M size for metadata block groups and a 1G size for data block groups, while for filesystems larger than 50G, we use a 1G size for both data and metadata block groups (except for zoned filesystems). It could also happen on any filesystem size due to the fact that system block groups are always smaller (32M) than both data and metadata block groups, but these are not frequently deleted, so much less likely to trigger the race. So make scrub skip any block group with a start offset that is less than the value we expect, as that means it's a new block group that was created in the current transaction. It's pointless to continue and try to scrub its extents, because scrub searches for extents using the commit root, so it won't find any. For a device replace, skip it as well for the same reasons, and we don't need to worry about the possibility of extents of the new block group not being to the new device, because we have the write duplication setup done through btrfs_map_block(). Fixes:d04fbe19ae
("btrfs: scrub: cleanup the argument list of scrub_chunk()") CC: stable@vger.kernel.org # 5.17 Signed-off-by: Filipe Manana <fdmanana@suse.com> Signed-off-by: David Sterba <dsterba@suse.com>
1342 lines
38 KiB
C
1342 lines
38 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (C) STRATO AG 2012. All rights reserved.
|
|
*/
|
|
|
|
#include <linux/sched.h>
|
|
#include <linux/bio.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/blkdev.h>
|
|
#include <linux/kthread.h>
|
|
#include <linux/math64.h>
|
|
#include "misc.h"
|
|
#include "ctree.h"
|
|
#include "extent_map.h"
|
|
#include "disk-io.h"
|
|
#include "transaction.h"
|
|
#include "print-tree.h"
|
|
#include "volumes.h"
|
|
#include "async-thread.h"
|
|
#include "check-integrity.h"
|
|
#include "rcu-string.h"
|
|
#include "dev-replace.h"
|
|
#include "sysfs.h"
|
|
#include "zoned.h"
|
|
#include "block-group.h"
|
|
|
|
/*
|
|
* Device replace overview
|
|
*
|
|
* [Objective]
|
|
* To copy all extents (both new and on-disk) from source device to target
|
|
* device, while still keeping the filesystem read-write.
|
|
*
|
|
* [Method]
|
|
* There are two main methods involved:
|
|
*
|
|
* - Write duplication
|
|
*
|
|
* All new writes will be written to both target and source devices, so even
|
|
* if replace gets canceled, sources device still contains up-to-date data.
|
|
*
|
|
* Location: handle_ops_on_dev_replace() from __btrfs_map_block()
|
|
* Start: btrfs_dev_replace_start()
|
|
* End: btrfs_dev_replace_finishing()
|
|
* Content: Latest data/metadata
|
|
*
|
|
* - Copy existing extents
|
|
*
|
|
* This happens by re-using scrub facility, as scrub also iterates through
|
|
* existing extents from commit root.
|
|
*
|
|
* Location: scrub_write_block_to_dev_replace() from
|
|
* scrub_block_complete()
|
|
* Content: Data/meta from commit root.
|
|
*
|
|
* Due to the content difference, we need to avoid nocow write when dev-replace
|
|
* is happening. This is done by marking the block group read-only and waiting
|
|
* for NOCOW writes.
|
|
*
|
|
* After replace is done, the finishing part is done by swapping the target and
|
|
* source devices.
|
|
*
|
|
* Location: btrfs_dev_replace_update_device_in_mapping_tree() from
|
|
* btrfs_dev_replace_finishing()
|
|
*/
|
|
|
|
static int btrfs_dev_replace_finishing(struct btrfs_fs_info *fs_info,
|
|
int scrub_ret);
|
|
static int btrfs_dev_replace_kthread(void *data);
|
|
|
|
int btrfs_init_dev_replace(struct btrfs_fs_info *fs_info)
|
|
{
|
|
struct btrfs_dev_lookup_args args = { .devid = BTRFS_DEV_REPLACE_DEVID };
|
|
struct btrfs_key key;
|
|
struct btrfs_root *dev_root = fs_info->dev_root;
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
struct extent_buffer *eb;
|
|
int slot;
|
|
int ret = 0;
|
|
struct btrfs_path *path = NULL;
|
|
int item_size;
|
|
struct btrfs_dev_replace_item *ptr;
|
|
u64 src_devid;
|
|
|
|
if (!dev_root)
|
|
return 0;
|
|
|
|
path = btrfs_alloc_path();
|
|
if (!path) {
|
|
ret = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
key.objectid = 0;
|
|
key.type = BTRFS_DEV_REPLACE_KEY;
|
|
key.offset = 0;
|
|
ret = btrfs_search_slot(NULL, dev_root, &key, path, 0, 0);
|
|
if (ret) {
|
|
no_valid_dev_replace_entry_found:
|
|
/*
|
|
* We don't have a replace item or it's corrupted. If there is
|
|
* a replace target, fail the mount.
|
|
*/
|
|
if (btrfs_find_device(fs_info->fs_devices, &args)) {
|
|
btrfs_err(fs_info,
|
|
"found replace target device without a valid replace item");
|
|
ret = -EUCLEAN;
|
|
goto out;
|
|
}
|
|
ret = 0;
|
|
dev_replace->replace_state =
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED;
|
|
dev_replace->cont_reading_from_srcdev_mode =
|
|
BTRFS_DEV_REPLACE_ITEM_CONT_READING_FROM_SRCDEV_MODE_ALWAYS;
|
|
dev_replace->time_started = 0;
|
|
dev_replace->time_stopped = 0;
|
|
atomic64_set(&dev_replace->num_write_errors, 0);
|
|
atomic64_set(&dev_replace->num_uncorrectable_read_errors, 0);
|
|
dev_replace->cursor_left = 0;
|
|
dev_replace->committed_cursor_left = 0;
|
|
dev_replace->cursor_left_last_write_of_item = 0;
|
|
dev_replace->cursor_right = 0;
|
|
dev_replace->srcdev = NULL;
|
|
dev_replace->tgtdev = NULL;
|
|
dev_replace->is_valid = 0;
|
|
dev_replace->item_needs_writeback = 0;
|
|
goto out;
|
|
}
|
|
slot = path->slots[0];
|
|
eb = path->nodes[0];
|
|
item_size = btrfs_item_size(eb, slot);
|
|
ptr = btrfs_item_ptr(eb, slot, struct btrfs_dev_replace_item);
|
|
|
|
if (item_size != sizeof(struct btrfs_dev_replace_item)) {
|
|
btrfs_warn(fs_info,
|
|
"dev_replace entry found has unexpected size, ignore entry");
|
|
goto no_valid_dev_replace_entry_found;
|
|
}
|
|
|
|
src_devid = btrfs_dev_replace_src_devid(eb, ptr);
|
|
dev_replace->cont_reading_from_srcdev_mode =
|
|
btrfs_dev_replace_cont_reading_from_srcdev_mode(eb, ptr);
|
|
dev_replace->replace_state = btrfs_dev_replace_replace_state(eb, ptr);
|
|
dev_replace->time_started = btrfs_dev_replace_time_started(eb, ptr);
|
|
dev_replace->time_stopped =
|
|
btrfs_dev_replace_time_stopped(eb, ptr);
|
|
atomic64_set(&dev_replace->num_write_errors,
|
|
btrfs_dev_replace_num_write_errors(eb, ptr));
|
|
atomic64_set(&dev_replace->num_uncorrectable_read_errors,
|
|
btrfs_dev_replace_num_uncorrectable_read_errors(eb, ptr));
|
|
dev_replace->cursor_left = btrfs_dev_replace_cursor_left(eb, ptr);
|
|
dev_replace->committed_cursor_left = dev_replace->cursor_left;
|
|
dev_replace->cursor_left_last_write_of_item = dev_replace->cursor_left;
|
|
dev_replace->cursor_right = btrfs_dev_replace_cursor_right(eb, ptr);
|
|
dev_replace->is_valid = 1;
|
|
|
|
dev_replace->item_needs_writeback = 0;
|
|
switch (dev_replace->replace_state) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED:
|
|
/*
|
|
* We don't have an active replace item but if there is a
|
|
* replace target, fail the mount.
|
|
*/
|
|
if (btrfs_find_device(fs_info->fs_devices, &args)) {
|
|
btrfs_err(fs_info,
|
|
"replace devid present without an active replace item");
|
|
ret = -EUCLEAN;
|
|
} else {
|
|
dev_replace->srcdev = NULL;
|
|
dev_replace->tgtdev = NULL;
|
|
}
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED:
|
|
dev_replace->tgtdev = btrfs_find_device(fs_info->fs_devices, &args);
|
|
args.devid = src_devid;
|
|
dev_replace->srcdev = btrfs_find_device(fs_info->fs_devices, &args);
|
|
|
|
/*
|
|
* allow 'btrfs dev replace_cancel' if src/tgt device is
|
|
* missing
|
|
*/
|
|
if (!dev_replace->srcdev &&
|
|
!btrfs_test_opt(fs_info, DEGRADED)) {
|
|
ret = -EIO;
|
|
btrfs_warn(fs_info,
|
|
"cannot mount because device replace operation is ongoing and");
|
|
btrfs_warn(fs_info,
|
|
"srcdev (devid %llu) is missing, need to run 'btrfs dev scan'?",
|
|
src_devid);
|
|
}
|
|
if (!dev_replace->tgtdev &&
|
|
!btrfs_test_opt(fs_info, DEGRADED)) {
|
|
ret = -EIO;
|
|
btrfs_warn(fs_info,
|
|
"cannot mount because device replace operation is ongoing and");
|
|
btrfs_warn(fs_info,
|
|
"tgtdev (devid %llu) is missing, need to run 'btrfs dev scan'?",
|
|
BTRFS_DEV_REPLACE_DEVID);
|
|
}
|
|
if (dev_replace->tgtdev) {
|
|
if (dev_replace->srcdev) {
|
|
dev_replace->tgtdev->total_bytes =
|
|
dev_replace->srcdev->total_bytes;
|
|
dev_replace->tgtdev->disk_total_bytes =
|
|
dev_replace->srcdev->disk_total_bytes;
|
|
dev_replace->tgtdev->commit_total_bytes =
|
|
dev_replace->srcdev->commit_total_bytes;
|
|
dev_replace->tgtdev->bytes_used =
|
|
dev_replace->srcdev->bytes_used;
|
|
dev_replace->tgtdev->commit_bytes_used =
|
|
dev_replace->srcdev->commit_bytes_used;
|
|
}
|
|
set_bit(BTRFS_DEV_STATE_REPLACE_TGT,
|
|
&dev_replace->tgtdev->dev_state);
|
|
|
|
WARN_ON(fs_info->fs_devices->rw_devices == 0);
|
|
dev_replace->tgtdev->io_width = fs_info->sectorsize;
|
|
dev_replace->tgtdev->io_align = fs_info->sectorsize;
|
|
dev_replace->tgtdev->sector_size = fs_info->sectorsize;
|
|
dev_replace->tgtdev->fs_info = fs_info;
|
|
set_bit(BTRFS_DEV_STATE_IN_FS_METADATA,
|
|
&dev_replace->tgtdev->dev_state);
|
|
}
|
|
break;
|
|
}
|
|
|
|
out:
|
|
btrfs_free_path(path);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Initialize a new device for device replace target from a given source dev
|
|
* and path.
|
|
*
|
|
* Return 0 and new device in @device_out, otherwise return < 0
|
|
*/
|
|
static int btrfs_init_dev_replace_tgtdev(struct btrfs_fs_info *fs_info,
|
|
const char *device_path,
|
|
struct btrfs_device *srcdev,
|
|
struct btrfs_device **device_out)
|
|
{
|
|
struct btrfs_fs_devices *fs_devices = fs_info->fs_devices;
|
|
struct btrfs_device *device;
|
|
struct block_device *bdev;
|
|
struct rcu_string *name;
|
|
u64 devid = BTRFS_DEV_REPLACE_DEVID;
|
|
int ret = 0;
|
|
|
|
*device_out = NULL;
|
|
if (srcdev->fs_devices->seeding) {
|
|
btrfs_err(fs_info, "the filesystem is a seed filesystem!");
|
|
return -EINVAL;
|
|
}
|
|
|
|
bdev = blkdev_get_by_path(device_path, FMODE_WRITE | FMODE_EXCL,
|
|
fs_info->bdev_holder);
|
|
if (IS_ERR(bdev)) {
|
|
btrfs_err(fs_info, "target device %s is invalid!", device_path);
|
|
return PTR_ERR(bdev);
|
|
}
|
|
|
|
if (!btrfs_check_device_zone_type(fs_info, bdev)) {
|
|
btrfs_err(fs_info,
|
|
"dev-replace: zoned type of target device mismatch with filesystem");
|
|
ret = -EINVAL;
|
|
goto error;
|
|
}
|
|
|
|
sync_blockdev(bdev);
|
|
|
|
list_for_each_entry(device, &fs_devices->devices, dev_list) {
|
|
if (device->bdev == bdev) {
|
|
btrfs_err(fs_info,
|
|
"target device is in the filesystem!");
|
|
ret = -EEXIST;
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
|
|
if (bdev_nr_bytes(bdev) < btrfs_device_get_total_bytes(srcdev)) {
|
|
btrfs_err(fs_info,
|
|
"target device is smaller than source device!");
|
|
ret = -EINVAL;
|
|
goto error;
|
|
}
|
|
|
|
|
|
device = btrfs_alloc_device(NULL, &devid, NULL);
|
|
if (IS_ERR(device)) {
|
|
ret = PTR_ERR(device);
|
|
goto error;
|
|
}
|
|
|
|
name = rcu_string_strdup(device_path, GFP_KERNEL);
|
|
if (!name) {
|
|
btrfs_free_device(device);
|
|
ret = -ENOMEM;
|
|
goto error;
|
|
}
|
|
rcu_assign_pointer(device->name, name);
|
|
ret = lookup_bdev(device_path, &device->devt);
|
|
if (ret)
|
|
goto error;
|
|
|
|
set_bit(BTRFS_DEV_STATE_WRITEABLE, &device->dev_state);
|
|
device->generation = 0;
|
|
device->io_width = fs_info->sectorsize;
|
|
device->io_align = fs_info->sectorsize;
|
|
device->sector_size = fs_info->sectorsize;
|
|
device->total_bytes = btrfs_device_get_total_bytes(srcdev);
|
|
device->disk_total_bytes = btrfs_device_get_disk_total_bytes(srcdev);
|
|
device->bytes_used = btrfs_device_get_bytes_used(srcdev);
|
|
device->commit_total_bytes = srcdev->commit_total_bytes;
|
|
device->commit_bytes_used = device->bytes_used;
|
|
device->fs_info = fs_info;
|
|
device->bdev = bdev;
|
|
set_bit(BTRFS_DEV_STATE_IN_FS_METADATA, &device->dev_state);
|
|
set_bit(BTRFS_DEV_STATE_REPLACE_TGT, &device->dev_state);
|
|
device->mode = FMODE_EXCL;
|
|
device->dev_stats_valid = 1;
|
|
set_blocksize(device->bdev, BTRFS_BDEV_BLOCKSIZE);
|
|
device->fs_devices = fs_devices;
|
|
|
|
ret = btrfs_get_dev_zone_info(device, false);
|
|
if (ret)
|
|
goto error;
|
|
|
|
mutex_lock(&fs_devices->device_list_mutex);
|
|
list_add(&device->dev_list, &fs_devices->devices);
|
|
fs_devices->num_devices++;
|
|
fs_devices->open_devices++;
|
|
mutex_unlock(&fs_devices->device_list_mutex);
|
|
|
|
*device_out = device;
|
|
return 0;
|
|
|
|
error:
|
|
blkdev_put(bdev, FMODE_EXCL);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* called from commit_transaction. Writes changed device replace state to
|
|
* disk.
|
|
*/
|
|
int btrfs_run_dev_replace(struct btrfs_trans_handle *trans)
|
|
{
|
|
struct btrfs_fs_info *fs_info = trans->fs_info;
|
|
int ret;
|
|
struct btrfs_root *dev_root = fs_info->dev_root;
|
|
struct btrfs_path *path;
|
|
struct btrfs_key key;
|
|
struct extent_buffer *eb;
|
|
struct btrfs_dev_replace_item *ptr;
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
|
|
down_read(&dev_replace->rwsem);
|
|
if (!dev_replace->is_valid ||
|
|
!dev_replace->item_needs_writeback) {
|
|
up_read(&dev_replace->rwsem);
|
|
return 0;
|
|
}
|
|
up_read(&dev_replace->rwsem);
|
|
|
|
key.objectid = 0;
|
|
key.type = BTRFS_DEV_REPLACE_KEY;
|
|
key.offset = 0;
|
|
|
|
path = btrfs_alloc_path();
|
|
if (!path) {
|
|
ret = -ENOMEM;
|
|
goto out;
|
|
}
|
|
ret = btrfs_search_slot(trans, dev_root, &key, path, -1, 1);
|
|
if (ret < 0) {
|
|
btrfs_warn(fs_info,
|
|
"error %d while searching for dev_replace item!",
|
|
ret);
|
|
goto out;
|
|
}
|
|
|
|
if (ret == 0 &&
|
|
btrfs_item_size(path->nodes[0], path->slots[0]) < sizeof(*ptr)) {
|
|
/*
|
|
* need to delete old one and insert a new one.
|
|
* Since no attempt is made to recover any old state, if the
|
|
* dev_replace state is 'running', the data on the target
|
|
* drive is lost.
|
|
* It would be possible to recover the state: just make sure
|
|
* that the beginning of the item is never changed and always
|
|
* contains all the essential information. Then read this
|
|
* minimal set of information and use it as a base for the
|
|
* new state.
|
|
*/
|
|
ret = btrfs_del_item(trans, dev_root, path);
|
|
if (ret != 0) {
|
|
btrfs_warn(fs_info,
|
|
"delete too small dev_replace item failed %d!",
|
|
ret);
|
|
goto out;
|
|
}
|
|
ret = 1;
|
|
}
|
|
|
|
if (ret == 1) {
|
|
/* need to insert a new item */
|
|
btrfs_release_path(path);
|
|
ret = btrfs_insert_empty_item(trans, dev_root, path,
|
|
&key, sizeof(*ptr));
|
|
if (ret < 0) {
|
|
btrfs_warn(fs_info,
|
|
"insert dev_replace item failed %d!", ret);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
eb = path->nodes[0];
|
|
ptr = btrfs_item_ptr(eb, path->slots[0],
|
|
struct btrfs_dev_replace_item);
|
|
|
|
down_write(&dev_replace->rwsem);
|
|
if (dev_replace->srcdev)
|
|
btrfs_set_dev_replace_src_devid(eb, ptr,
|
|
dev_replace->srcdev->devid);
|
|
else
|
|
btrfs_set_dev_replace_src_devid(eb, ptr, (u64)-1);
|
|
btrfs_set_dev_replace_cont_reading_from_srcdev_mode(eb, ptr,
|
|
dev_replace->cont_reading_from_srcdev_mode);
|
|
btrfs_set_dev_replace_replace_state(eb, ptr,
|
|
dev_replace->replace_state);
|
|
btrfs_set_dev_replace_time_started(eb, ptr, dev_replace->time_started);
|
|
btrfs_set_dev_replace_time_stopped(eb, ptr, dev_replace->time_stopped);
|
|
btrfs_set_dev_replace_num_write_errors(eb, ptr,
|
|
atomic64_read(&dev_replace->num_write_errors));
|
|
btrfs_set_dev_replace_num_uncorrectable_read_errors(eb, ptr,
|
|
atomic64_read(&dev_replace->num_uncorrectable_read_errors));
|
|
dev_replace->cursor_left_last_write_of_item =
|
|
dev_replace->cursor_left;
|
|
btrfs_set_dev_replace_cursor_left(eb, ptr,
|
|
dev_replace->cursor_left_last_write_of_item);
|
|
btrfs_set_dev_replace_cursor_right(eb, ptr,
|
|
dev_replace->cursor_right);
|
|
dev_replace->item_needs_writeback = 0;
|
|
up_write(&dev_replace->rwsem);
|
|
|
|
btrfs_mark_buffer_dirty(eb);
|
|
|
|
out:
|
|
btrfs_free_path(path);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static char* btrfs_dev_name(struct btrfs_device *device)
|
|
{
|
|
if (!device || test_bit(BTRFS_DEV_STATE_MISSING, &device->dev_state))
|
|
return "<missing disk>";
|
|
else
|
|
return rcu_str_deref(device->name);
|
|
}
|
|
|
|
static int mark_block_group_to_copy(struct btrfs_fs_info *fs_info,
|
|
struct btrfs_device *src_dev)
|
|
{
|
|
struct btrfs_path *path;
|
|
struct btrfs_key key;
|
|
struct btrfs_key found_key;
|
|
struct btrfs_root *root = fs_info->dev_root;
|
|
struct btrfs_dev_extent *dev_extent = NULL;
|
|
struct btrfs_block_group *cache;
|
|
struct btrfs_trans_handle *trans;
|
|
int ret = 0;
|
|
u64 chunk_offset;
|
|
|
|
/* Do not use "to_copy" on non zoned filesystem for now */
|
|
if (!btrfs_is_zoned(fs_info))
|
|
return 0;
|
|
|
|
mutex_lock(&fs_info->chunk_mutex);
|
|
|
|
/* Ensure we don't have pending new block group */
|
|
spin_lock(&fs_info->trans_lock);
|
|
while (fs_info->running_transaction &&
|
|
!list_empty(&fs_info->running_transaction->dev_update_list)) {
|
|
spin_unlock(&fs_info->trans_lock);
|
|
mutex_unlock(&fs_info->chunk_mutex);
|
|
trans = btrfs_attach_transaction(root);
|
|
if (IS_ERR(trans)) {
|
|
ret = PTR_ERR(trans);
|
|
mutex_lock(&fs_info->chunk_mutex);
|
|
if (ret == -ENOENT) {
|
|
spin_lock(&fs_info->trans_lock);
|
|
continue;
|
|
} else {
|
|
goto unlock;
|
|
}
|
|
}
|
|
|
|
ret = btrfs_commit_transaction(trans);
|
|
mutex_lock(&fs_info->chunk_mutex);
|
|
if (ret)
|
|
goto unlock;
|
|
|
|
spin_lock(&fs_info->trans_lock);
|
|
}
|
|
spin_unlock(&fs_info->trans_lock);
|
|
|
|
path = btrfs_alloc_path();
|
|
if (!path) {
|
|
ret = -ENOMEM;
|
|
goto unlock;
|
|
}
|
|
|
|
path->reada = READA_FORWARD;
|
|
path->search_commit_root = 1;
|
|
path->skip_locking = 1;
|
|
|
|
key.objectid = src_dev->devid;
|
|
key.type = BTRFS_DEV_EXTENT_KEY;
|
|
key.offset = 0;
|
|
|
|
ret = btrfs_search_slot(NULL, root, &key, path, 0, 0);
|
|
if (ret < 0)
|
|
goto free_path;
|
|
if (ret > 0) {
|
|
if (path->slots[0] >=
|
|
btrfs_header_nritems(path->nodes[0])) {
|
|
ret = btrfs_next_leaf(root, path);
|
|
if (ret < 0)
|
|
goto free_path;
|
|
if (ret > 0) {
|
|
ret = 0;
|
|
goto free_path;
|
|
}
|
|
} else {
|
|
ret = 0;
|
|
}
|
|
}
|
|
|
|
while (1) {
|
|
struct extent_buffer *leaf = path->nodes[0];
|
|
int slot = path->slots[0];
|
|
|
|
btrfs_item_key_to_cpu(leaf, &found_key, slot);
|
|
|
|
if (found_key.objectid != src_dev->devid)
|
|
break;
|
|
|
|
if (found_key.type != BTRFS_DEV_EXTENT_KEY)
|
|
break;
|
|
|
|
if (found_key.offset < key.offset)
|
|
break;
|
|
|
|
dev_extent = btrfs_item_ptr(leaf, slot, struct btrfs_dev_extent);
|
|
|
|
chunk_offset = btrfs_dev_extent_chunk_offset(leaf, dev_extent);
|
|
|
|
cache = btrfs_lookup_block_group(fs_info, chunk_offset);
|
|
if (!cache)
|
|
goto skip;
|
|
|
|
spin_lock(&cache->lock);
|
|
cache->to_copy = 1;
|
|
spin_unlock(&cache->lock);
|
|
|
|
btrfs_put_block_group(cache);
|
|
|
|
skip:
|
|
ret = btrfs_next_item(root, path);
|
|
if (ret != 0) {
|
|
if (ret > 0)
|
|
ret = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
free_path:
|
|
btrfs_free_path(path);
|
|
unlock:
|
|
mutex_unlock(&fs_info->chunk_mutex);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool btrfs_finish_block_group_to_copy(struct btrfs_device *srcdev,
|
|
struct btrfs_block_group *cache,
|
|
u64 physical)
|
|
{
|
|
struct btrfs_fs_info *fs_info = cache->fs_info;
|
|
struct extent_map *em;
|
|
struct map_lookup *map;
|
|
u64 chunk_offset = cache->start;
|
|
int num_extents, cur_extent;
|
|
int i;
|
|
|
|
/* Do not use "to_copy" on non zoned filesystem for now */
|
|
if (!btrfs_is_zoned(fs_info))
|
|
return true;
|
|
|
|
spin_lock(&cache->lock);
|
|
if (cache->removed) {
|
|
spin_unlock(&cache->lock);
|
|
return true;
|
|
}
|
|
spin_unlock(&cache->lock);
|
|
|
|
em = btrfs_get_chunk_map(fs_info, chunk_offset, 1);
|
|
ASSERT(!IS_ERR(em));
|
|
map = em->map_lookup;
|
|
|
|
num_extents = cur_extent = 0;
|
|
for (i = 0; i < map->num_stripes; i++) {
|
|
/* We have more device extent to copy */
|
|
if (srcdev != map->stripes[i].dev)
|
|
continue;
|
|
|
|
num_extents++;
|
|
if (physical == map->stripes[i].physical)
|
|
cur_extent = i;
|
|
}
|
|
|
|
free_extent_map(em);
|
|
|
|
if (num_extents > 1 && cur_extent < num_extents - 1) {
|
|
/*
|
|
* Has more stripes on this device. Keep this block group
|
|
* readonly until we finish all the stripes.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
/* Last stripe on this device */
|
|
spin_lock(&cache->lock);
|
|
cache->to_copy = 0;
|
|
spin_unlock(&cache->lock);
|
|
|
|
return true;
|
|
}
|
|
|
|
static int btrfs_dev_replace_start(struct btrfs_fs_info *fs_info,
|
|
const char *tgtdev_name, u64 srcdevid, const char *srcdev_name,
|
|
int read_src)
|
|
{
|
|
struct btrfs_root *root = fs_info->dev_root;
|
|
struct btrfs_trans_handle *trans;
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
int ret;
|
|
struct btrfs_device *tgt_device = NULL;
|
|
struct btrfs_device *src_device = NULL;
|
|
|
|
src_device = btrfs_find_device_by_devspec(fs_info, srcdevid,
|
|
srcdev_name);
|
|
if (IS_ERR(src_device))
|
|
return PTR_ERR(src_device);
|
|
|
|
if (btrfs_pinned_by_swapfile(fs_info, src_device)) {
|
|
btrfs_warn_in_rcu(fs_info,
|
|
"cannot replace device %s (devid %llu) due to active swapfile",
|
|
btrfs_dev_name(src_device), src_device->devid);
|
|
return -ETXTBSY;
|
|
}
|
|
|
|
/*
|
|
* Here we commit the transaction to make sure commit_total_bytes
|
|
* of all the devices are updated.
|
|
*/
|
|
trans = btrfs_attach_transaction(root);
|
|
if (!IS_ERR(trans)) {
|
|
ret = btrfs_commit_transaction(trans);
|
|
if (ret)
|
|
return ret;
|
|
} else if (PTR_ERR(trans) != -ENOENT) {
|
|
return PTR_ERR(trans);
|
|
}
|
|
|
|
ret = btrfs_init_dev_replace_tgtdev(fs_info, tgtdev_name,
|
|
src_device, &tgt_device);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = mark_block_group_to_copy(fs_info, src_device);
|
|
if (ret)
|
|
return ret;
|
|
|
|
down_write(&dev_replace->rwsem);
|
|
switch (dev_replace->replace_state) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED:
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED:
|
|
ASSERT(0);
|
|
ret = BTRFS_IOCTL_DEV_REPLACE_RESULT_ALREADY_STARTED;
|
|
up_write(&dev_replace->rwsem);
|
|
goto leave;
|
|
}
|
|
|
|
dev_replace->cont_reading_from_srcdev_mode = read_src;
|
|
dev_replace->srcdev = src_device;
|
|
dev_replace->tgtdev = tgt_device;
|
|
|
|
btrfs_info_in_rcu(fs_info,
|
|
"dev_replace from %s (devid %llu) to %s started",
|
|
btrfs_dev_name(src_device),
|
|
src_device->devid,
|
|
rcu_str_deref(tgt_device->name));
|
|
|
|
/*
|
|
* from now on, the writes to the srcdev are all duplicated to
|
|
* go to the tgtdev as well (refer to btrfs_map_block()).
|
|
*/
|
|
dev_replace->replace_state = BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED;
|
|
dev_replace->time_started = ktime_get_real_seconds();
|
|
dev_replace->cursor_left = 0;
|
|
dev_replace->committed_cursor_left = 0;
|
|
dev_replace->cursor_left_last_write_of_item = 0;
|
|
dev_replace->cursor_right = 0;
|
|
dev_replace->is_valid = 1;
|
|
dev_replace->item_needs_writeback = 1;
|
|
atomic64_set(&dev_replace->num_write_errors, 0);
|
|
atomic64_set(&dev_replace->num_uncorrectable_read_errors, 0);
|
|
up_write(&dev_replace->rwsem);
|
|
|
|
ret = btrfs_sysfs_add_device(tgt_device);
|
|
if (ret)
|
|
btrfs_err(fs_info, "kobj add dev failed %d", ret);
|
|
|
|
btrfs_wait_ordered_roots(fs_info, U64_MAX, 0, (u64)-1);
|
|
|
|
/*
|
|
* Commit dev_replace state and reserve 1 item for it.
|
|
* This is crucial to ensure we won't miss copying extents for new block
|
|
* groups that are allocated after we started the device replace, and
|
|
* must be done after setting up the device replace state.
|
|
*/
|
|
trans = btrfs_start_transaction(root, 1);
|
|
if (IS_ERR(trans)) {
|
|
ret = PTR_ERR(trans);
|
|
down_write(&dev_replace->rwsem);
|
|
dev_replace->replace_state =
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED;
|
|
dev_replace->srcdev = NULL;
|
|
dev_replace->tgtdev = NULL;
|
|
up_write(&dev_replace->rwsem);
|
|
goto leave;
|
|
}
|
|
|
|
ret = btrfs_commit_transaction(trans);
|
|
WARN_ON(ret);
|
|
|
|
/* the disk copy procedure reuses the scrub code */
|
|
ret = btrfs_scrub_dev(fs_info, src_device->devid, 0,
|
|
btrfs_device_get_total_bytes(src_device),
|
|
&dev_replace->scrub_progress, 0, 1);
|
|
|
|
ret = btrfs_dev_replace_finishing(fs_info, ret);
|
|
if (ret == -EINPROGRESS)
|
|
ret = BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS;
|
|
|
|
return ret;
|
|
|
|
leave:
|
|
btrfs_destroy_dev_replace_tgtdev(tgt_device);
|
|
return ret;
|
|
}
|
|
|
|
int btrfs_dev_replace_by_ioctl(struct btrfs_fs_info *fs_info,
|
|
struct btrfs_ioctl_dev_replace_args *args)
|
|
{
|
|
int ret;
|
|
|
|
switch (args->start.cont_reading_from_srcdev_mode) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_ALWAYS:
|
|
case BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_AVOID:
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((args->start.srcdevid == 0 && args->start.srcdev_name[0] == '\0') ||
|
|
args->start.tgtdev_name[0] == '\0')
|
|
return -EINVAL;
|
|
|
|
ret = btrfs_dev_replace_start(fs_info, args->start.tgtdev_name,
|
|
args->start.srcdevid,
|
|
args->start.srcdev_name,
|
|
args->start.cont_reading_from_srcdev_mode);
|
|
args->result = ret;
|
|
/* don't warn if EINPROGRESS, someone else might be running scrub */
|
|
if (ret == BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS ||
|
|
ret == BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR)
|
|
return 0;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* blocked until all in-flight bios operations are finished.
|
|
*/
|
|
static void btrfs_rm_dev_replace_blocked(struct btrfs_fs_info *fs_info)
|
|
{
|
|
set_bit(BTRFS_FS_STATE_DEV_REPLACING, &fs_info->fs_state);
|
|
wait_event(fs_info->dev_replace.replace_wait, !percpu_counter_sum(
|
|
&fs_info->dev_replace.bio_counter));
|
|
}
|
|
|
|
/*
|
|
* we have removed target device, it is safe to allow new bios request.
|
|
*/
|
|
static void btrfs_rm_dev_replace_unblocked(struct btrfs_fs_info *fs_info)
|
|
{
|
|
clear_bit(BTRFS_FS_STATE_DEV_REPLACING, &fs_info->fs_state);
|
|
wake_up(&fs_info->dev_replace.replace_wait);
|
|
}
|
|
|
|
/*
|
|
* When finishing the device replace, before swapping the source device with the
|
|
* target device we must update the chunk allocation state in the target device,
|
|
* as it is empty because replace works by directly copying the chunks and not
|
|
* through the normal chunk allocation path.
|
|
*/
|
|
static int btrfs_set_target_alloc_state(struct btrfs_device *srcdev,
|
|
struct btrfs_device *tgtdev)
|
|
{
|
|
struct extent_state *cached_state = NULL;
|
|
u64 start = 0;
|
|
u64 found_start;
|
|
u64 found_end;
|
|
int ret = 0;
|
|
|
|
lockdep_assert_held(&srcdev->fs_info->chunk_mutex);
|
|
|
|
while (!find_first_extent_bit(&srcdev->alloc_state, start,
|
|
&found_start, &found_end,
|
|
CHUNK_ALLOCATED, &cached_state)) {
|
|
ret = set_extent_bits(&tgtdev->alloc_state, found_start,
|
|
found_end, CHUNK_ALLOCATED);
|
|
if (ret)
|
|
break;
|
|
start = found_end + 1;
|
|
}
|
|
|
|
free_extent_state(cached_state);
|
|
return ret;
|
|
}
|
|
|
|
static void btrfs_dev_replace_update_device_in_mapping_tree(
|
|
struct btrfs_fs_info *fs_info,
|
|
struct btrfs_device *srcdev,
|
|
struct btrfs_device *tgtdev)
|
|
{
|
|
struct extent_map_tree *em_tree = &fs_info->mapping_tree;
|
|
struct extent_map *em;
|
|
struct map_lookup *map;
|
|
u64 start = 0;
|
|
int i;
|
|
|
|
write_lock(&em_tree->lock);
|
|
do {
|
|
em = lookup_extent_mapping(em_tree, start, (u64)-1);
|
|
if (!em)
|
|
break;
|
|
map = em->map_lookup;
|
|
for (i = 0; i < map->num_stripes; i++)
|
|
if (srcdev == map->stripes[i].dev)
|
|
map->stripes[i].dev = tgtdev;
|
|
start = em->start + em->len;
|
|
free_extent_map(em);
|
|
} while (start);
|
|
write_unlock(&em_tree->lock);
|
|
}
|
|
|
|
static int btrfs_dev_replace_finishing(struct btrfs_fs_info *fs_info,
|
|
int scrub_ret)
|
|
{
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
struct btrfs_device *tgt_device;
|
|
struct btrfs_device *src_device;
|
|
struct btrfs_root *root = fs_info->tree_root;
|
|
u8 uuid_tmp[BTRFS_UUID_SIZE];
|
|
struct btrfs_trans_handle *trans;
|
|
int ret = 0;
|
|
|
|
/* don't allow cancel or unmount to disturb the finishing procedure */
|
|
mutex_lock(&dev_replace->lock_finishing_cancel_unmount);
|
|
|
|
down_read(&dev_replace->rwsem);
|
|
/* was the operation canceled, or is it finished? */
|
|
if (dev_replace->replace_state !=
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED) {
|
|
up_read(&dev_replace->rwsem);
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
return 0;
|
|
}
|
|
|
|
tgt_device = dev_replace->tgtdev;
|
|
src_device = dev_replace->srcdev;
|
|
up_read(&dev_replace->rwsem);
|
|
|
|
/*
|
|
* flush all outstanding I/O and inode extent mappings before the
|
|
* copy operation is declared as being finished
|
|
*/
|
|
ret = btrfs_start_delalloc_roots(fs_info, LONG_MAX, false);
|
|
if (ret) {
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
return ret;
|
|
}
|
|
btrfs_wait_ordered_roots(fs_info, U64_MAX, 0, (u64)-1);
|
|
|
|
/*
|
|
* We have to use this loop approach because at this point src_device
|
|
* has to be available for transaction commit to complete, yet new
|
|
* chunks shouldn't be allocated on the device.
|
|
*/
|
|
while (1) {
|
|
trans = btrfs_start_transaction(root, 0);
|
|
if (IS_ERR(trans)) {
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
return PTR_ERR(trans);
|
|
}
|
|
ret = btrfs_commit_transaction(trans);
|
|
WARN_ON(ret);
|
|
|
|
/* Prevent write_all_supers() during the finishing procedure */
|
|
mutex_lock(&fs_info->fs_devices->device_list_mutex);
|
|
/* Prevent new chunks being allocated on the source device */
|
|
mutex_lock(&fs_info->chunk_mutex);
|
|
|
|
if (!list_empty(&src_device->post_commit_list)) {
|
|
mutex_unlock(&fs_info->fs_devices->device_list_mutex);
|
|
mutex_unlock(&fs_info->chunk_mutex);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
down_write(&dev_replace->rwsem);
|
|
dev_replace->replace_state =
|
|
scrub_ret ? BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED
|
|
: BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED;
|
|
dev_replace->tgtdev = NULL;
|
|
dev_replace->srcdev = NULL;
|
|
dev_replace->time_stopped = ktime_get_real_seconds();
|
|
dev_replace->item_needs_writeback = 1;
|
|
|
|
/*
|
|
* Update allocation state in the new device and replace the old device
|
|
* with the new one in the mapping tree.
|
|
*/
|
|
if (!scrub_ret) {
|
|
scrub_ret = btrfs_set_target_alloc_state(src_device, tgt_device);
|
|
if (scrub_ret)
|
|
goto error;
|
|
btrfs_dev_replace_update_device_in_mapping_tree(fs_info,
|
|
src_device,
|
|
tgt_device);
|
|
} else {
|
|
if (scrub_ret != -ECANCELED)
|
|
btrfs_err_in_rcu(fs_info,
|
|
"btrfs_scrub_dev(%s, %llu, %s) failed %d",
|
|
btrfs_dev_name(src_device),
|
|
src_device->devid,
|
|
rcu_str_deref(tgt_device->name), scrub_ret);
|
|
error:
|
|
up_write(&dev_replace->rwsem);
|
|
mutex_unlock(&fs_info->chunk_mutex);
|
|
mutex_unlock(&fs_info->fs_devices->device_list_mutex);
|
|
btrfs_rm_dev_replace_blocked(fs_info);
|
|
if (tgt_device)
|
|
btrfs_destroy_dev_replace_tgtdev(tgt_device);
|
|
btrfs_rm_dev_replace_unblocked(fs_info);
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
|
|
return scrub_ret;
|
|
}
|
|
|
|
btrfs_info_in_rcu(fs_info,
|
|
"dev_replace from %s (devid %llu) to %s finished",
|
|
btrfs_dev_name(src_device),
|
|
src_device->devid,
|
|
rcu_str_deref(tgt_device->name));
|
|
clear_bit(BTRFS_DEV_STATE_REPLACE_TGT, &tgt_device->dev_state);
|
|
tgt_device->devid = src_device->devid;
|
|
src_device->devid = BTRFS_DEV_REPLACE_DEVID;
|
|
memcpy(uuid_tmp, tgt_device->uuid, sizeof(uuid_tmp));
|
|
memcpy(tgt_device->uuid, src_device->uuid, sizeof(tgt_device->uuid));
|
|
memcpy(src_device->uuid, uuid_tmp, sizeof(src_device->uuid));
|
|
btrfs_device_set_total_bytes(tgt_device, src_device->total_bytes);
|
|
btrfs_device_set_disk_total_bytes(tgt_device,
|
|
src_device->disk_total_bytes);
|
|
btrfs_device_set_bytes_used(tgt_device, src_device->bytes_used);
|
|
tgt_device->commit_bytes_used = src_device->bytes_used;
|
|
|
|
btrfs_assign_next_active_device(src_device, tgt_device);
|
|
|
|
list_add(&tgt_device->dev_alloc_list, &fs_info->fs_devices->alloc_list);
|
|
fs_info->fs_devices->rw_devices++;
|
|
|
|
up_write(&dev_replace->rwsem);
|
|
btrfs_rm_dev_replace_blocked(fs_info);
|
|
|
|
btrfs_rm_dev_replace_remove_srcdev(src_device);
|
|
|
|
btrfs_rm_dev_replace_unblocked(fs_info);
|
|
|
|
/*
|
|
* Increment dev_stats_ccnt so that btrfs_run_dev_stats() will
|
|
* update on-disk dev stats value during commit transaction
|
|
*/
|
|
atomic_inc(&tgt_device->dev_stats_ccnt);
|
|
|
|
/*
|
|
* this is again a consistent state where no dev_replace procedure
|
|
* is running, the target device is part of the filesystem, the
|
|
* source device is not part of the filesystem anymore and its 1st
|
|
* superblock is scratched out so that it is no longer marked to
|
|
* belong to this filesystem.
|
|
*/
|
|
mutex_unlock(&fs_info->chunk_mutex);
|
|
mutex_unlock(&fs_info->fs_devices->device_list_mutex);
|
|
|
|
/* replace the sysfs entry */
|
|
btrfs_sysfs_remove_device(src_device);
|
|
btrfs_sysfs_update_devid(tgt_device);
|
|
if (test_bit(BTRFS_DEV_STATE_WRITEABLE, &src_device->dev_state))
|
|
btrfs_scratch_superblocks(fs_info, src_device->bdev,
|
|
src_device->name->str);
|
|
|
|
/* write back the superblocks */
|
|
trans = btrfs_start_transaction(root, 0);
|
|
if (!IS_ERR(trans))
|
|
btrfs_commit_transaction(trans);
|
|
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
|
|
btrfs_rm_dev_replace_free_srcdev(src_device);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Read progress of device replace status according to the state and last
|
|
* stored position. The value format is the same as for
|
|
* btrfs_dev_replace::progress_1000
|
|
*/
|
|
static u64 btrfs_dev_replace_progress(struct btrfs_fs_info *fs_info)
|
|
{
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
u64 ret = 0;
|
|
|
|
switch (dev_replace->replace_state) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED:
|
|
ret = 0;
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED:
|
|
ret = 1000;
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED:
|
|
ret = div64_u64(dev_replace->cursor_left,
|
|
div_u64(btrfs_device_get_total_bytes(
|
|
dev_replace->srcdev), 1000));
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void btrfs_dev_replace_status(struct btrfs_fs_info *fs_info,
|
|
struct btrfs_ioctl_dev_replace_args *args)
|
|
{
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
|
|
down_read(&dev_replace->rwsem);
|
|
/* even if !dev_replace_is_valid, the values are good enough for
|
|
* the replace_status ioctl */
|
|
args->result = BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR;
|
|
args->status.replace_state = dev_replace->replace_state;
|
|
args->status.time_started = dev_replace->time_started;
|
|
args->status.time_stopped = dev_replace->time_stopped;
|
|
args->status.num_write_errors =
|
|
atomic64_read(&dev_replace->num_write_errors);
|
|
args->status.num_uncorrectable_read_errors =
|
|
atomic64_read(&dev_replace->num_uncorrectable_read_errors);
|
|
args->status.progress_1000 = btrfs_dev_replace_progress(fs_info);
|
|
up_read(&dev_replace->rwsem);
|
|
}
|
|
|
|
int btrfs_dev_replace_cancel(struct btrfs_fs_info *fs_info)
|
|
{
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
struct btrfs_device *tgt_device = NULL;
|
|
struct btrfs_device *src_device = NULL;
|
|
struct btrfs_trans_handle *trans;
|
|
struct btrfs_root *root = fs_info->tree_root;
|
|
int result;
|
|
int ret;
|
|
|
|
if (sb_rdonly(fs_info->sb))
|
|
return -EROFS;
|
|
|
|
mutex_lock(&dev_replace->lock_finishing_cancel_unmount);
|
|
down_write(&dev_replace->rwsem);
|
|
switch (dev_replace->replace_state) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED:
|
|
result = BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED;
|
|
up_write(&dev_replace->rwsem);
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED:
|
|
tgt_device = dev_replace->tgtdev;
|
|
src_device = dev_replace->srcdev;
|
|
up_write(&dev_replace->rwsem);
|
|
ret = btrfs_scrub_cancel(fs_info);
|
|
if (ret < 0) {
|
|
result = BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED;
|
|
} else {
|
|
result = BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR;
|
|
/*
|
|
* btrfs_dev_replace_finishing() will handle the
|
|
* cleanup part
|
|
*/
|
|
btrfs_info_in_rcu(fs_info,
|
|
"dev_replace from %s (devid %llu) to %s canceled",
|
|
btrfs_dev_name(src_device), src_device->devid,
|
|
btrfs_dev_name(tgt_device));
|
|
}
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED:
|
|
/*
|
|
* Scrub doing the replace isn't running so we need to do the
|
|
* cleanup step of btrfs_dev_replace_finishing() here
|
|
*/
|
|
result = BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR;
|
|
tgt_device = dev_replace->tgtdev;
|
|
src_device = dev_replace->srcdev;
|
|
dev_replace->tgtdev = NULL;
|
|
dev_replace->srcdev = NULL;
|
|
dev_replace->replace_state =
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED;
|
|
dev_replace->time_stopped = ktime_get_real_seconds();
|
|
dev_replace->item_needs_writeback = 1;
|
|
|
|
up_write(&dev_replace->rwsem);
|
|
|
|
/* Scrub for replace must not be running in suspended state */
|
|
ret = btrfs_scrub_cancel(fs_info);
|
|
ASSERT(ret != -ENOTCONN);
|
|
|
|
trans = btrfs_start_transaction(root, 0);
|
|
if (IS_ERR(trans)) {
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
return PTR_ERR(trans);
|
|
}
|
|
ret = btrfs_commit_transaction(trans);
|
|
WARN_ON(ret);
|
|
|
|
btrfs_info_in_rcu(fs_info,
|
|
"suspended dev_replace from %s (devid %llu) to %s canceled",
|
|
btrfs_dev_name(src_device), src_device->devid,
|
|
btrfs_dev_name(tgt_device));
|
|
|
|
if (tgt_device)
|
|
btrfs_destroy_dev_replace_tgtdev(tgt_device);
|
|
break;
|
|
default:
|
|
up_write(&dev_replace->rwsem);
|
|
result = -EINVAL;
|
|
}
|
|
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
return result;
|
|
}
|
|
|
|
void btrfs_dev_replace_suspend_for_unmount(struct btrfs_fs_info *fs_info)
|
|
{
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
|
|
mutex_lock(&dev_replace->lock_finishing_cancel_unmount);
|
|
down_write(&dev_replace->rwsem);
|
|
|
|
switch (dev_replace->replace_state) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED:
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED:
|
|
dev_replace->replace_state =
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED;
|
|
dev_replace->time_stopped = ktime_get_real_seconds();
|
|
dev_replace->item_needs_writeback = 1;
|
|
btrfs_info(fs_info, "suspending dev_replace for unmount");
|
|
break;
|
|
}
|
|
|
|
up_write(&dev_replace->rwsem);
|
|
mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
|
|
}
|
|
|
|
/* resume dev_replace procedure that was interrupted by unmount */
|
|
int btrfs_resume_dev_replace_async(struct btrfs_fs_info *fs_info)
|
|
{
|
|
struct task_struct *task;
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
|
|
down_write(&dev_replace->rwsem);
|
|
|
|
switch (dev_replace->replace_state) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED:
|
|
up_write(&dev_replace->rwsem);
|
|
return 0;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED:
|
|
break;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED:
|
|
dev_replace->replace_state =
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED;
|
|
break;
|
|
}
|
|
if (!dev_replace->tgtdev || !dev_replace->tgtdev->bdev) {
|
|
btrfs_info(fs_info,
|
|
"cannot continue dev_replace, tgtdev is missing");
|
|
btrfs_info(fs_info,
|
|
"you may cancel the operation after 'mount -o degraded'");
|
|
dev_replace->replace_state =
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED;
|
|
up_write(&dev_replace->rwsem);
|
|
return 0;
|
|
}
|
|
up_write(&dev_replace->rwsem);
|
|
|
|
/*
|
|
* This could collide with a paused balance, but the exclusive op logic
|
|
* should never allow both to start and pause. We don't want to allow
|
|
* dev-replace to start anyway.
|
|
*/
|
|
if (!btrfs_exclop_start(fs_info, BTRFS_EXCLOP_DEV_REPLACE)) {
|
|
down_write(&dev_replace->rwsem);
|
|
dev_replace->replace_state =
|
|
BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED;
|
|
up_write(&dev_replace->rwsem);
|
|
btrfs_info(fs_info,
|
|
"cannot resume dev-replace, other exclusive operation running");
|
|
return 0;
|
|
}
|
|
|
|
task = kthread_run(btrfs_dev_replace_kthread, fs_info, "btrfs-devrepl");
|
|
return PTR_ERR_OR_ZERO(task);
|
|
}
|
|
|
|
static int btrfs_dev_replace_kthread(void *data)
|
|
{
|
|
struct btrfs_fs_info *fs_info = data;
|
|
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
|
|
u64 progress;
|
|
int ret;
|
|
|
|
progress = btrfs_dev_replace_progress(fs_info);
|
|
progress = div_u64(progress, 10);
|
|
btrfs_info_in_rcu(fs_info,
|
|
"continuing dev_replace from %s (devid %llu) to target %s @%u%%",
|
|
btrfs_dev_name(dev_replace->srcdev),
|
|
dev_replace->srcdev->devid,
|
|
btrfs_dev_name(dev_replace->tgtdev),
|
|
(unsigned int)progress);
|
|
|
|
ret = btrfs_scrub_dev(fs_info, dev_replace->srcdev->devid,
|
|
dev_replace->committed_cursor_left,
|
|
btrfs_device_get_total_bytes(dev_replace->srcdev),
|
|
&dev_replace->scrub_progress, 0, 1);
|
|
ret = btrfs_dev_replace_finishing(fs_info, ret);
|
|
WARN_ON(ret && ret != -ECANCELED);
|
|
|
|
btrfs_exclop_finish(fs_info);
|
|
return 0;
|
|
}
|
|
|
|
int __pure btrfs_dev_replace_is_ongoing(struct btrfs_dev_replace *dev_replace)
|
|
{
|
|
if (!dev_replace->is_valid)
|
|
return 0;
|
|
|
|
switch (dev_replace->replace_state) {
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED:
|
|
return 0;
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED:
|
|
case BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED:
|
|
/*
|
|
* return true even if tgtdev is missing (this is
|
|
* something that can happen if the dev_replace
|
|
* procedure is suspended by an umount and then
|
|
* the tgtdev is missing (or "btrfs dev scan") was
|
|
* not called and the filesystem is remounted
|
|
* in degraded state. This does not stop the
|
|
* dev_replace procedure. It needs to be canceled
|
|
* manually if the cancellation is wanted.
|
|
*/
|
|
break;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
void btrfs_bio_counter_inc_noblocked(struct btrfs_fs_info *fs_info)
|
|
{
|
|
percpu_counter_inc(&fs_info->dev_replace.bio_counter);
|
|
}
|
|
|
|
void btrfs_bio_counter_sub(struct btrfs_fs_info *fs_info, s64 amount)
|
|
{
|
|
percpu_counter_sub(&fs_info->dev_replace.bio_counter, amount);
|
|
cond_wake_up_nomb(&fs_info->dev_replace.replace_wait);
|
|
}
|
|
|
|
void btrfs_bio_counter_inc_blocked(struct btrfs_fs_info *fs_info)
|
|
{
|
|
while (1) {
|
|
percpu_counter_inc(&fs_info->dev_replace.bio_counter);
|
|
if (likely(!test_bit(BTRFS_FS_STATE_DEV_REPLACING,
|
|
&fs_info->fs_state)))
|
|
break;
|
|
|
|
btrfs_bio_counter_dec(fs_info);
|
|
wait_event(fs_info->dev_replace.replace_wait,
|
|
!test_bit(BTRFS_FS_STATE_DEV_REPLACING,
|
|
&fs_info->fs_state));
|
|
}
|
|
}
|