mm/memory_hotplug: add offline_and_remove_memory_ranges()

offline_and_remove_memory() handles a single contiguous range.

Callers that manage a device composed of several ranges (dax/kmem)
currently have to call it in a loop, which gives up atomicity.

In addition to pushing rollback logic into the driver, the lack of
atomicity creates a race condition between system daemons trying to manage
the same resource:

   - Manager 1:  Offlines memory blocks.    Removes device.
                                        ^^^^
   - Manager 2:  Detects offline memory blocks, re-onlines them.

Add offline_and_remove_memory_ranges(), which takes an array of ranges and
processes them as one operation under a single lock_device_hotplug():

  - Phase 1 offlines every block of every range.
  - Phase 2 removes the ranges only if all ranges are offline.
  - If any offline fails, the whole operation is reverted.

This gives callers all-or-nothing semantics for the offline step, so a
failed or interrupted unplug leaves the device in a consistent state.

This also resolves the battling managers race - the second manager's
operation simply fails when the block is destroyed / cannot be onlined.

offline_and_remove_memory() becomes a thin wrapper that passes its single
range to the new helper, so the offline/rollback logic lives in one place.

Link: https://lore.kernel.org/20260712154505.3564379-7-gourry@gourry.net
Signed-off-by: Gregory Price <gourry@gourry.net>
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Dave Jiang <dave.jiang@intel.com>
Acked-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Dan Williams <djbw@kernel.org>
Cc: Alison Schofield <alison.schofield@intel.com>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Hannes Reinecke <hare@suse.de>
Cc: Liam R. Howlett <liam@infradead.org>
Cc: Lorenzo Stoakes <ljs@kernel.org>
Cc: Michal Hocko <mhocko@suse.com>
Cc: Mike Rapoport <rppt@kernel.org>
Cc: Oscar Salvador <osalvador@suse.de>
Cc: Pankaj Gupta <pankaj.gupta@amd.com>
Cc: "Rafael J. Wysocki" <rafael@kernel.org>
Cc: Shuah Khan <shuah@kernel.org>
Cc: Suren Baghdasaryan <surenb@google.com>
Cc: Vishal Verma <vishal.l.verma@intel.com>
Cc: Vlastimil Babka <vbabka@kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
This commit is contained in:
Gregory Price
2026-07-12 11:45:00 -04:00
committed by Andrew Morton
parent 2b047c8a8c
commit c17ba40c37
2 changed files with 76 additions and 28 deletions

View File

@@ -268,6 +268,8 @@ extern int offline_pages(unsigned long start_pfn, unsigned long nr_pages,
extern int remove_memory(u64 start, u64 size);
extern void __remove_memory(u64 start, u64 size);
extern int offline_and_remove_memory(u64 start, u64 size);
int offline_and_remove_memory_ranges(const struct range *ranges,
unsigned int nr_ranges);
#else
static inline void try_offline_node(int nid) {}
@@ -284,6 +286,12 @@ static inline int remove_memory(u64 start, u64 size)
}
static inline void __remove_memory(u64 start, u64 size) {}
static inline int offline_and_remove_memory_ranges(const struct range *ranges,
unsigned int nr_ranges)
{
return -EBUSY;
}
#endif /* CONFIG_MEMORY_HOTREMOVE */
#ifdef CONFIG_MEMORY_HOTPLUG

View File

@@ -2432,58 +2432,98 @@ static int try_reonline_memory_block(struct memory_block *mem, void *arg)
*/
int offline_and_remove_memory(u64 start, u64 size)
{
const unsigned long mb_count = size / memory_block_size_bytes();
uint8_t *online_types, *tmp;
int rc;
struct range range = {
.start = start,
.end = start + size - 1,
};
if (!IS_ALIGNED(start, memory_block_size_bytes()) ||
!IS_ALIGNED(size, memory_block_size_bytes()) || !size)
return offline_and_remove_memory_ranges(&range, 1);
}
EXPORT_SYMBOL_GPL(offline_and_remove_memory);
/**
* offline_and_remove_memory_ranges - offline and remove multiple memory ranges
* @ranges: array of physical address ranges to offline and remove
* @nr_ranges: number of entries in @ranges
*
* Offline and remove several memory ranges as one operation, serialized
* against other hotplug operations by a single lock_device_hotplug().
*
* This offlines all ranges before removing any of them. If offlining any
* range fails, the entire process is reverted and nothing is removed.
* This provides a fully atomic semantic for unplugging an entire device.
*
* Each range must be memory-block aligned in start and size.
*
* Return: 0 on success, negative errno on failure (never positive). On
* failure no range has been removed.
*/
int offline_and_remove_memory_ranges(const struct range *ranges,
unsigned int nr_ranges)
{
unsigned long mb_count = 0;
uint8_t *online_types, *tmp;
unsigned int i;
int rc = 0;
if (!ranges || !nr_ranges)
return -EINVAL;
for (i = 0; i < nr_ranges; i++) {
const u64 start = ranges[i].start;
const u64 size = range_len(&ranges[i]);
if (!IS_ALIGNED(start, memory_block_size_bytes()) ||
!IS_ALIGNED(size, memory_block_size_bytes()) || !size)
return -EINVAL;
mb_count += size / memory_block_size_bytes();
}
/*
* We'll remember the old online type of each memory block, so we can
* try to revert whatever we did when offlining one memory block fails
* after offlining some others succeeded.
* Remember the old online type of every memory block across all ranges,
* so we can revert if offlining a later block fails. All entries start
* as MMOP_OFFLINE so blocks we never touched are skipped on rollback.
*/
online_types = kmalloc_array(mb_count, sizeof(*online_types),
GFP_KERNEL);
if (!online_types)
return -ENOMEM;
/*
* Initialize all states to MMOP_OFFLINE, so when we abort processing in
* try_offline_memory_block(), we'll skip all unprocessed blocks in
* try_reonline_memory_block().
*/
memset(online_types, MMOP_OFFLINE, mb_count);
lock_device_hotplug();
tmp = online_types;
rc = walk_memory_blocks(start, size, &tmp, try_offline_memory_block);
/*
* In case we succeeded to offline all memory, remove it.
* This cannot fail as it cannot get onlined in the meantime.
* Phase 1: offline every block in every range. An already-offline
* block folds to success, so out-of-band offlining never blocks unplug.
*/
if (!rc) {
rc = try_remove_memory(start, size);
tmp = online_types;
for (i = 0; i < nr_ranges; i++) {
rc = walk_memory_blocks(ranges[i].start, range_len(&ranges[i]),
&tmp, try_offline_memory_block);
if (rc)
pr_err("%s: Failed to remove memory: %d", __func__, rc);
break;
}
/*
* Rollback what we did. While memory onlining might theoretically fail
* (nacked by a notifier), it barely ever happens.
*/
/* If any failure occurred at all, rollback any changes and bail */
if (rc) {
tmp = online_types;
walk_memory_blocks(start, size, &tmp,
try_reonline_memory_block);
for (i = 0; i < nr_ranges; i++)
walk_memory_blocks(ranges[i].start,
range_len(&ranges[i]), &tmp,
try_reonline_memory_block);
goto out_unlock;
}
/* Phase 2: Remove. This should never fail holding the hotplug lock */
for (i = 0; i < nr_ranges; i++)
WARN_ON_ONCE(try_remove_memory(ranges[i].start,
range_len(&ranges[i])));
out_unlock:
unlock_device_hotplug();
kfree(online_types);
return rc;
}
EXPORT_SYMBOL_GPL(offline_and_remove_memory);
EXPORT_SYMBOL_GPL(offline_and_remove_memory_ranges);
#endif /* CONFIG_MEMORY_HOTREMOVE */