[Devel] [PATCH hci-8.1 v5 10/11] dm-ploop: reload metadata on resume
Alexander Ivanov
alexander.ivanov at virtuozzo.com
Tue Oct 13 10:55:12 MSK 2026
On "dmsetup reload" the new ploop reads image metadata in ploop_ctr(),
but IO keeps going through the old ploop until suspend, and userspace
may modify images of a suspended device (merge zeroes BAT entries of
the base delta, snapshot creation trims the top delta). So after resume
the new ploop may work with a stale BAT and file sizes, e.g. allocate
clusters already in use.
Reload all metadata in ploop_preresume(): the top delta header, BAT of
every delta and delta file sizes, and rebuild the BAT tree and
holes_bitmap. On error the old metadata is kept and resume fails. DM
has already swapped the new table in by then, so the device stays
suspended and the next resume retries. Userspace must check the result
and must not resume the previous instance on the same images.
Skip the in-use signature check on reload, it is done in ploop_ctr().
Take the signature from disk: libploop marks the image closed on the
suspended device before umount, and an in-memory IN_USE written over
it makes umount trim the image and lose CBT. After flip_upper_deltas
keep the in-memory signature, as userspace relies on it being written
to the new top delta, otherwise the next load fails with -ENOLCK after
online merge.
If metadata is dirty after suspend, keep it, as md pages may still be
referenced. Data bios complete before BAT writeback, so wait for
inflight_md in postsuspend.
Request preallocation in ploop_preresume() after the reload instead of
in ploop_ctr(), so the new ploop doesn't truncate the top delta back to
a stale size before resume. Wait for pending preallocation in
postsuspend, killably, as the allocator may hang on a dead file. Don't
wait in standby, so that dm_replace() of libploop can suspend the
device to recover it. Skip the reload and prealloc on resume while
preallocation is pending, as the allocator changes the top delta size
under bat_lock only. Skip the reload in standby too, as the images may
be inaccessible.
Track standby per ploop instead of the queue bit. The queue is shared
by all tables, and ploop_ctr() of a new table clears the bit before the
old table is suspended, so the old ploop would wait for its stuck
allocator in postsuspend.
ploop_llseek_hole() needs no locking against the reload:
dm_llseek_hole() calls it under md->io_barrier and returns -EAGAIN on
a suspended device.
https://virtuozzo.atlassian.net/browse/VSTOR-148171
Feature: dm-ploop: ploop target driver
Signed-off-by: Alexander Ivanov <alexander.ivanov at virtuozzo.com>
---
drivers/md/dm-ploop-bat.c | 146 ++++++++++++++++++++++++++++++++++-
drivers/md/dm-ploop-cmd.c | 1 +
drivers/md/dm-ploop-map.c | 9 ++-
drivers/md/dm-ploop-target.c | 87 +++++++++++++++------
drivers/md/dm-ploop.h | 13 +++-
5 files changed, 227 insertions(+), 29 deletions(-)
diff --git a/drivers/md/dm-ploop-bat.c b/drivers/md/dm-ploop-bat.c
index 5a89771a2e02..c060afdd8d3e 100644
--- a/drivers/md/dm-ploop-bat.c
+++ b/drivers/md/dm-ploop-bat.c
@@ -185,7 +185,8 @@ static int ploop_setup_holes_bitmap(struct ploop *ploop, u32 bat_clusters)
}
ALLOW_ERROR_INJECTION(ploop_setup_holes_bitmap, ERRNO);
-int ploop_setup_metadata(struct ploop *ploop, struct page *page)
+int ploop_setup_metadata(struct ploop *ploop, struct page *page,
+ bool check_in_use)
{
struct ploop_pvd_header *m_hdr = NULL;
u32 bat_clusters, offset_clusters;
@@ -200,7 +201,8 @@ int ploop_setup_metadata(struct ploop *ploop, struct page *page)
goto out;
ret = -ENOLCK;
- if (m_hdr->m_DiskInUse != cpu_to_le32(SIGNATURE_DISK_IN_USE) &&
+ if (check_in_use &&
+ m_hdr->m_DiskInUse != cpu_to_le32(SIGNATURE_DISK_IN_USE) &&
!ploop_is_ro(ploop) && !ignore_signature_disk_in_use)
goto out;
@@ -567,6 +569,146 @@ int ploop_add_delta(struct ploop *ploop, u32 level, struct file *file, bool is_r
}
ALLOW_ERROR_INJECTION(ploop_add_delta, ERRNO);
+static bool ploop_md_pages_are_clean(struct ploop *ploop)
+{
+ struct rb_node *node;
+ struct md_page *md;
+
+ if (!llist_empty(&ploop->wb_batch_llist) ||
+ atomic_read(&ploop->inflight_md))
+ return false;
+
+ ploop_for_each_md_page(ploop, md, node) {
+ if (test_bit(MD_DIRTY, &md->status) ||
+ test_bit(MD_WRITEBACK, &md->status) ||
+ test_bit(MD_UPDATING, &md->status) ||
+ md->piwb || !llist_empty(&md->wait_llist))
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Build in-memory metadata from scratch: re-read header of top delta,
+ * BAT of every delta and sizes of delta files. This is called on resume
+ * of suspended device, since after "dmsetup reload" the metadata of the
+ * new ploop may be outdated: the images could be changed by the old
+ * ploop after the new one was constructed. On error the old metadata
+ * is kept.
+ */
+int ploop_reload_metadata(struct ploop *ploop)
+{
+ struct rb_root old_bat_entries = ploop->bat_entries;
+ void *old_holes_bitmap = ploop->holes_bitmap;
+ u32 old_nr_bat_entries = ploop->nr_bat_entries;
+ u32 old_hb_nr = ploop->hb_nr;
+ u32 old_m_Type = ploop->m_Type;
+ u32 old_m_Sectors = ploop->m_Sectors;
+ struct ploop_pvd_header *hdr, *old_hdr;
+ u8 old_m_Sig[sizeof(ploop->m_Sig)];
+ struct ploop_delta *delta, *old_deltas;
+ struct page *page;
+ loff_t file_size;
+ u32 nr_be;
+ int level, ret;
+
+ /*
+ * Metadata of the device that has already been running is up to
+ * date, so we only need to reload clean metadata. Dirty metadata
+ * must not appear after suspend, as all IO and BAT writeback are
+ * completed (see ploop_postsuspend()), but we can't free md pages,
+ * which are still in use, in any case.
+ */
+ if (WARN_ON_ONCE(!ploop_md_pages_are_clean(ploop)))
+ return 0;
+
+ memcpy(old_m_Sig, ploop->m_Sig, sizeof(old_m_Sig));
+
+ old_deltas = kmemdup(ploop->deltas,
+ sizeof(*old_deltas) * ploop->nr_deltas,
+ GFP_KERNEL);
+ page = alloc_page(GFP_KERNEL);
+ ret = -ENOMEM;
+ if (!old_deltas || !page)
+ goto out;
+
+ ret = ploop_rw_page_sync(READ, ploop_top_delta(ploop)->file, 0, page);
+ if (ret)
+ goto out;
+
+ ploop->bat_entries = RB_ROOT;
+ ploop->holes_bitmap = NULL;
+
+ /* Image is already checked for being in use in ploop_ctr() */
+ ret = ploop_setup_metadata(ploop, page, false);
+ if (ret)
+ goto restore;
+
+ ret = ploop_prealloc_md_pages(&ploop->bat_entries, 0,
+ ploop->nr_bat_entries);
+ if (ret)
+ goto restore;
+
+ /*
+ * Same order as in ploop_add_deltas_stack(): from top to base.
+ * ploop_read_delta() relies on it, see the comment there. nr_be of
+ * the delta is updated right away, as the lower delta is checked
+ * against it on the next iteration.
+ */
+ for (level = ploop_top_level(ploop); level >= 0; level--) {
+ delta = &ploop->deltas[level];
+
+ ret = ploop_read_delta(ploop, level, delta->file,
+ &file_size, &nr_be);
+ if (ret)
+ goto restore;
+
+ delta->file_size = file_size;
+ delta->file_preallocated_area_start = file_size;
+ delta->nr_be = nr_be;
+ }
+
+ /*
+ * Header of top delta is written with BAT page 0. Userspace doesn't
+ * mark the delta, which becomes top after flip_upper_deltas, as in
+ * use, and relies that the in-use signature of the old top delta
+ * will be written to it. So keep the signature we had. In other
+ * cases take it from disk: userspace may have changed it on the
+ * suspended device (e.g. marked the image closed on umount).
+ */
+ if (ploop->top_flipped) {
+ old_hdr = ploop_md_first_entry(&old_bat_entries)->kmpage;
+ hdr = ploop_md_first_entry(&ploop->bat_entries)->kmpage;
+ hdr->m_DiskInUse = old_hdr->m_DiskInUse;
+ ploop->top_flipped = false;
+ }
+
+ ploop_free_md_pages_tree(&old_bat_entries);
+ kvfree(old_holes_bitmap);
+ goto out;
+restore:
+ ploop_free_md_pages_tree(&ploop->bat_entries);
+ kvfree(ploop->holes_bitmap);
+ ploop->bat_entries = old_bat_entries;
+ ploop->holes_bitmap = old_holes_bitmap;
+ ploop->nr_bat_entries = old_nr_bat_entries;
+ ploop->hb_nr = old_hb_nr;
+ memcpy(ploop->m_Sig, old_m_Sig, sizeof(ploop->m_Sig));
+ ploop->m_Type = old_m_Type;
+ ploop->m_Sectors = old_m_Sectors;
+ memcpy(ploop->deltas, old_deltas,
+ sizeof(*old_deltas) * ploop->nr_deltas);
+out:
+ if (page)
+ put_page(page);
+ kfree(old_deltas);
+ if (ret)
+ PL_ERR("Failed to reload metadata: %d", ret);
+ return ret;
+}
+ALLOW_ERROR_INJECTION(ploop_reload_metadata, ERRNO);
+
void ploop_put_delta(struct ploop *ploop, struct ploop_delta *delta)
{
int i;
diff --git a/drivers/md/dm-ploop-cmd.c b/drivers/md/dm-ploop-cmd.c
index 977b6920a5b4..1590af23360f 100644
--- a/drivers/md/dm-ploop-cmd.c
+++ b/drivers/md/dm-ploop-cmd.c
@@ -1049,6 +1049,7 @@ static int process_flip_upper_deltas(struct ploop *ploop)
/* FIXME */
swap(ploop->deltas[level], ploop->deltas[level+1]);
+ ploop->top_flipped = true;
return 0;
}
diff --git a/drivers/md/dm-ploop-map.c b/drivers/md/dm-ploop-map.c
index e2b70cb3c142..5733b7d28196 100644
--- a/drivers/md/dm-ploop-map.c
+++ b/drivers/md/dm-ploop-map.c
@@ -1247,7 +1247,7 @@ void ploop_should_prealloc(struct ploop *ploop)
spin_unlock_irqrestore(&ploop->bat_lock, flags);
}
-static int ploop_no_pending_prealloc(struct ploop *ploop)
+int ploop_no_pending_prealloc(struct ploop *ploop)
{
int ret;
unsigned long flags;
@@ -1259,6 +1259,12 @@ static int ploop_no_pending_prealloc(struct ploop *ploop)
return ret;
}
+void ploop_wait_prealloc(struct ploop *ploop)
+{
+ wait_event_killable(ploop->dispatcher_wq_prealloc,
+ ploop_no_pending_prealloc(ploop));
+}
+
static int ploop_allocate_cluster(struct ploop *ploop, u32 *dst_clu, struct file *file)
{
struct ploop_delta *top = ploop_top_delta(ploop);
@@ -1446,6 +1452,7 @@ static void ploop_check_standby_mode(struct ploop *ploop, long res)
spin_lock_irqsave(&q->queue_lock, flags);
prev = test_and_set_bit(QUEUE_FLAG_STANDBY, &q->queue_flags);
spin_unlock_irqrestore(&q->queue_lock, flags);
+ WRITE_ONCE(ploop->standby, true);
if (!prev)
PL_INFO("was switched into the standby mode");
diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
index dc9410212831..3a24769d0a8f 100644
--- a/drivers/md/dm-ploop-target.c
+++ b/drivers/md/dm-ploop-target.c
@@ -21,6 +21,7 @@
#include <linux/uio.h>
#include <linux/timer.h>
#include <linux/error-injection.h>
+#include <linux/sched/mm.h>
#include "dm-ploop.h"
#define DM_MSG_PREFIX "ploop"
@@ -295,7 +296,7 @@ static int ploop_check_top_delta(struct ploop *ploop, struct file *file)
/* flush dirty pages before changing direct IO */
vfs_fsync(file, 1);
- ret = ploop_setup_metadata(ploop, page);
+ ret = ploop_setup_metadata(ploop, page, true);
if (ret)
goto out;
@@ -426,8 +427,7 @@ static struct ploop_worker *ploop_worker_create(struct ploop *ploop,
* suspended and resumed. This behavior is intentional to allow for atomic
* updates to the mapping table without disrupting ongoing I/O operations.
*
- * But dm-ploop is not fully compatible with this design. Ploop does not do a
- * "metadata" reload on resume. So there can be situations like:
+ * So there can be situations like:
*
* 1. dmsetup reload is called, and new ploop is created with ploop_ctr for
* the same delta image file as an old ploop and ploop_add_deltas_stack reads
@@ -438,23 +438,19 @@ static struct ploop_worker *ploop_worker_create(struct ploop *ploop,
* the new one, new ploop has outdated metadata and thus may corrupt the
* images by relying on it.
*
- * For instance we thought to truncate the preallocations to the delta image
- * file on ploop_dtr, but that can't be done as ploop_dtr is called on the old
- * ploop after the new one is resumed, so we can't possibly modify the image
- * from the old ploop context at this point, as new ploop may be already doing
- * its own IO or preallocation.
+ * To handle this, ploop_preresume reloads all the metadata from the images
+ * (see ploop_reload_metadata), so that the new ploop "sees" all previous
+ * updates from the old one. For the same reason the new ploop must not
+ * modify the images before resume, so preallocation is started only after
+ * the metadata reload, and the old ploop waits for its preallocation to
+ * complete on postsuspend.
*
- * The better way to do this truncation is on ploop_*suspend, and then on resume
- * to reload the metadata in the new ploop so that the new ploop "sees" all
- * previous updates from the old one.
- *
- * For now we can leave with it, as we chose not to do this truncation on
- * destroy and we rely that userspace always does dmsetup reload on already
- * suspended device.
- *
- * FIXME: We must implement metadata reload on resume. After that we can
- * implement preallocation truncation on suspend (see dm-qcow2 as it already
- * implements that in qcow2_parse_metadata and qcow2_truncate_preallocations).
+ * Still, we can't truncate the preallocations to the delta image file on
+ * ploop_dtr, as ploop_dtr is called on the old ploop after the new one is
+ * resumed, so we can't possibly modify the image from the old ploop context
+ * at this point, as new ploop may be already doing its own IO or
+ * preallocation. This truncation should be done on ploop_*suspend (see
+ * dm-qcow2 as it already implements that in qcow2_truncate_preallocations).
*/
static int ploop_ctr(struct dm_target *ti, unsigned int argc, char **argv)
{
@@ -644,9 +640,6 @@ static int ploop_ctr(struct dm_target *ti, unsigned int argc, char **argv)
ti->num_discard_bios = 1;
ti->discards_supported = true;
- if (!ploop_is_ro(ploop) && ploop->nr_deltas > 0)
- ploop_should_prealloc(ploop);
-
return 0;
err:
@@ -726,6 +719,11 @@ static void ploop_set_suspended(struct dm_target *ti, bool suspended)
up_write(&ploop->ctl_rwsem);
}
+static bool ploop_is_standby(struct ploop *ploop)
+{
+ return READ_ONCE(ploop->standby);
+}
+
static void ploop_presuspend(struct dm_target *ti)
{
struct ploop *ploop = ti->private;
@@ -750,19 +748,58 @@ static void ploop_presuspend_undo(struct dm_target *ti)
}
static void ploop_postsuspend(struct dm_target *ti)
{
+ struct ploop *ploop = ti->private;
+
+ /*
+ * Let the next resumed ploop (maybe of reloaded table)
+ * see final size of the top delta. Don't wait in standby
+ * mode: the allocator may be stuck on the dead file, while
+ * dm_replace() of libploop must be able to suspend the device
+ * to recover it. The wait is killable for the same reason.
+ * If the wait is skipped or killed, a ploop of a reloaded
+ * table may see a stale size, and a late truncate of our
+ * allocator may shrink the file under it.
+ */
+ if (!ploop_is_standby(ploop))
+ ploop_wait_prealloc(ploop);
+ /*
+ * Data bios are completed before BAT writeback is finished
+ * (see ploop_put_piwb()), so wait for it explicitly to make
+ * md pages clean for ploop_reload_metadata().
+ */
+ wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
ploop_set_suspended(ti, true);
}
static int ploop_preresume(struct dm_target *ti)
{
struct ploop *ploop = ti->private;
+ unsigned int noio_flags;
+ bool reload = false;
int ret = 0;
- down_read(&ploop->ctl_rwsem);
- if (ploop->noresume)
+ down_write(&ploop->ctl_rwsem);
+ if (ploop->noresume) {
ret = -EAGAIN;
- up_read(&ploop->ctl_rwsem);
+ } else if (!ploop_is_standby(ploop) &&
+ ploop_no_pending_prealloc(ploop)) {
+ reload = true;
+ /*
+ * The device is suspended, so reclaim must not wait
+ * for IO to it. Otherwise we deadlock with ourselves.
+ */
+ noio_flags = memalloc_noio_save();
+ ret = ploop_reload_metadata(ploop);
+ memalloc_noio_restore(noio_flags);
+ }
+ up_write(&ploop->ctl_rwsem);
if (ret == 0) {
+ /*
+ * Preallocate only after the metadata is reloaded,
+ * as prealloc relies on the actual top delta size.
+ */
+ if (reload && !ploop_is_ro(ploop))
+ ploop_should_prealloc(ploop);
/*
* We are singleton target. There will be
* no more reasons to break resume.
diff --git a/drivers/md/dm-ploop.h b/drivers/md/dm-ploop.h
index 5041fc8f1e13..5750eafc8a1b 100644
--- a/drivers/md/dm-ploop.h
+++ b/drivers/md/dm-ploop.h
@@ -245,6 +245,13 @@ struct ploop {
/* Resume is prohibited */
bool noresume;
+ /* Top delta was swapped by flip_upper_deltas */
+ bool top_flipped;
+ /*
+ * This ploop switched the queue into standby. Unlike the queue
+ * bit, it is not cleared by ploop_ctr() of a new table.
+ */
+ bool standby;
/* Device is suspended */
bool suspended;
/* Device wants suspend */
@@ -633,7 +640,9 @@ extern struct pio *ploop_alloc_pio_with_pages(struct ploop *ploop);
extern void ploop_free_pio_with_pages(struct ploop *ploop, struct pio *pio);
extern void ploop_pio_prepare_offsets(struct ploop *ploop, struct pio *pio, u32 clu);
-extern int ploop_setup_metadata(struct ploop *ploop, struct page *page);
+extern int ploop_setup_metadata(struct ploop *ploop, struct page *page,
+ bool check_in_use);
+extern int ploop_reload_metadata(struct ploop *ploop);
extern int ploop_read_delta_metadata(struct ploop *ploop, struct file *file,
struct rb_root *md_root, u32 *delta_nr_be);
extern void ploop_index_wb_init(struct ploop_index_wb *piwb,
@@ -651,6 +660,8 @@ extern void ploop_disable_writeback_delay(struct ploop *ploop);
extern void ploop_enable_writeback_delay(struct ploop *ploop);
extern void ploop_should_prealloc(struct ploop *ploop);
+extern void ploop_wait_prealloc(struct ploop *ploop);
+extern int ploop_no_pending_prealloc(struct ploop *ploop);
extern void ploop_resubmit_enospc_pios(struct ploop *ploop);
#endif /* __DM_PLOOP_H */
--
2.53.0
More information about the Devel
mailing list