offline_pages() migrates and isolates pages in two nested loops with no
upper bound. A page that can never be migrated or freed (a long-term
pin, or a slab page that raced into the range) keeps the loop spinning
and the offline never returns. This is a known issue, noted in the code
("TODO: fatal migration failures should bail out") and in the admin
guide ("memory offlining might retry for a long time (or even forever),
until aborted by the user").
The only escape is signal_pending(current), which works when userspace
drives the offline but not for in-kernel callers. ACPI DIMM hot-unplug
(kacpi_hotplug_wq) and similar in-kernel hotplug paths run offline_pages()
on ordered workqueues where signal_pending() can never become true, so a
stuck offline wedges the workqueue and blocks all later hotplug events
with no way to abort it.
Add an opt-in backstop: a counter at the top of the inner migration loop
bounds the number of passes over the range. The check sits in the inner
loop because that is where a stuck page spins; since every outer pass
runs the inner loop at least once, this bounds both loops. On the limit
offline_pages() fails with -EBUSY and dump_page()s the stuck page.
The parameter (memory_hotplug.offline_migrate_max_passes) defaults to 0
(unlimited, today's behaviour) and is re-read every pass, so an offline
that is already stuck can be rescued at runtime by writing a non-zero
value. Such callers already handle -EBUSY, so no caller changes are
needed.
Signed-off-by: Aboorva Devarajan <[email protected]>
---
.../admin-guide/kernel-parameters.txt | 14 +++++++
.../admin-guide/mm/memory-hotplug.rst | 26 ++++++++++++-
mm/memory_hotplug.c | 38 +++++++++++++++++++
3 files changed, 77 insertions(+), 1 deletion(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt
b/Documentation/admin-guide/kernel-parameters.txt
index 53f08950a630..90b67c457c2a 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -3980,6 +3980,20 @@ Kernel parameters
Note that even when enabled, there are a few cases where
the feature is not effective.
+ memory_hotplug.offline_migrate_max_passes=
+ [KNL] Maximum number of migration passes
+ for memory offlining.
+ Format: <integer>
+ default: 0 (no limit, historical behaviour)
+ When non-zero, memory offlining gives up with
+ -EBUSY after this many migration passes over
+ the range, instead of retrying forever on a
+ stuck page. Each pass scans the range and
+ migrates what it can.
+ The parameter is re-read on every pass, so an
+ offline request that is already stuck can be
+ rescued at runtime by writing the parameter.
+
memtest= [KNL,X86,ARM,M68K,PPC,RISCV,EARLY] Enable memtest
Format: <integer>
default : 0 <disable>
diff --git a/Documentation/admin-guide/mm/memory-hotplug.rst
b/Documentation/admin-guide/mm/memory-hotplug.rst
index 0207f8725142..79b45109c408 100644
--- a/Documentation/admin-guide/mm/memory-hotplug.rst
+++ b/Documentation/admin-guide/mm/memory-hotplug.rst
@@ -224,7 +224,10 @@ increases memory offlining reliability; still, memory
offlining can fail in
some corner cases.
Further, memory offlining might retry for a long time (or even forever), until
-aborted by the user.
+aborted by the user. The ``memory_hotplug.offline_migrate_max_passes``
+parameter can be used to bound the number of retry passes, making an offline
+request that cannot make progress fail with ``-EBUSY`` instead of retrying
+indefinitely (see `Module Parameters`_).
Offlining of a memory block can be triggered via::
@@ -547,6 +550,27 @@ The following module parameters are currently defined:
possible.
Parameter availability depends on CONFIG_NUMA.
+``offline_migrate_max_passes`` read-write: Maximum number of migration
+ passes for a single memory offline
+ request. Memory offlining retries to
+ migrate and isolate pages until it succeeds;
+ a page that can never be migrated or freed
+ makes it retry forever. When this parameter
+ is non-zero, an offline request gives up
+ with ``-EBUSY`` after the configured number
+ of migration passes and the memory block
+ stays online.
+
+ The parameter is re-read on every pass, so
+ an offline request that is already stuck
+ retrying can be rescued at runtime by
+ writing a non-zero value, without a reboot.
+
+ The default is "0", meaning no limit (the
+ historical behaviour).
+
+ Parameter availability depends on
+ CONFIG_MEMORY_HOTREMOVE.
================================
===============================================
ZONE_MOVABLE
diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index 226ab9cb078a..9067829349a0 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -1785,6 +1785,12 @@ bool mhp_range_allowed(u64 start, u64 size, bool
need_mapping)
}
#ifdef CONFIG_MEMORY_HOTREMOVE
+/* Max offline_pages() migration passes before -EBUSY; 0 = retry forever. */
+static unsigned int offline_migrate_max_passes __read_mostly;
+module_param(offline_migrate_max_passes, uint, 0644);
+MODULE_PARM_DESC(offline_migrate_max_passes,
+ "Max migration passes before memory offline gives up (0 = no limit)");
+
/*
* Scan pfn range [start,end) to find movable/migratable pages (LRU and
* hugetlb folio, movable_ops pages). Will skip over most unmovable
@@ -1966,6 +1972,8 @@ int offline_pages(unsigned long start_pfn, unsigned long
nr_pages,
struct node_notify node_arg = {
.nid = NUMA_NO_NODE,
};
+ unsigned int max_passes;
+ unsigned int pass = 0;
unsigned long flags;
char *reason;
int ret;
@@ -2062,6 +2070,36 @@ int offline_pages(unsigned long start_pfn, unsigned long
nr_pages,
goto failed_removal_isolated;
}
+ /*
+ * A page that can never be migrated (e.g. a
+ * long-term pin) makes this loop retry forever.
+ * Give up after the configured number of passes.
+ * The limit is re-read on every pass so that an
+ * offline request that is already stuck here can
+ * be aborted at runtime by writing the parameter,
+ * which matters for in-kernel callers (e.g. ACPI
+ * DIMM hot-unplug) that run on kworkers and can
+ * never be aborted by a signal.
+ */
+ max_passes = READ_ONCE(offline_migrate_max_passes);
+ if (max_passes && pass++ >= max_passes) {
+ pr_warn("memory offlining [mem
%#010llx-%#010llx]: giving up after %u passes\n",
+ (unsigned long long)start_pfn <<
PAGE_SHIFT,
+ ((unsigned long long)end_pfn <<
PAGE_SHIFT) - 1,
+ pass - 1);
+ /*
+ * Dump the page we last stopped at as a
+ * diagnostic hint: usually the stuck page,
+ * but just the range start after a restart.
+ */
+ if (pfn >= start_pfn && pfn < end_pfn)
+ dump_page(pfn_to_page(pfn),
+ "memory offline retry limit");
+ ret = -EBUSY;
+ reason = "retry limit exceeded";
+ goto failed_removal_isolated;
+ }
+
cond_resched();
ret = scan_movable_pages(pfn, end_pfn, &pfn);
--
2.54.0