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 --- .../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: + 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: default : 0 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