summaryrefslogtreecommitdiff
path: root/mm/memory_hotplug.c
diff options
context:
space:
mode:
Diffstat (limited to 'mm/memory_hotplug.c')
-rw-r--r--mm/memory_hotplug.c264
1 files changed, 189 insertions, 75 deletions
diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index bc805029da51..226ab9cb078a 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -40,6 +40,8 @@
#include <asm/tlbflush.h>
#include "internal.h"
+#include "mm_init.h"
+#include "page_alloc.h"
#include "shuffle.h"
enum {
@@ -221,7 +223,7 @@ void put_online_mems(void)
bool movable_node_enabled = false;
static int mhp_default_online_type = -1;
-int mhp_get_default_online_type(void)
+enum mmop mhp_get_default_online_type(void)
{
if (mhp_default_online_type >= 0)
return mhp_default_online_type;
@@ -239,8 +241,9 @@ int mhp_get_default_online_type(void)
return mhp_default_online_type;
}
+EXPORT_SYMBOL_GPL(mhp_get_default_online_type);
-void mhp_set_default_online_type(int online_type)
+void mhp_set_default_online_type(enum mmop online_type)
{
mhp_default_online_type = online_type;
}
@@ -319,21 +322,13 @@ static void release_memory_resource(struct resource *res)
static int check_pfn_span(unsigned long pfn, unsigned long nr_pages)
{
/*
- * Disallow all operations smaller than a sub-section and only
- * allow operations smaller than a section for
- * SPARSEMEM_VMEMMAP. Note that check_hotplug_memory_range()
- * enforces a larger memory_block_size_bytes() granularity for
- * memory that will be marked online, so this check should only
- * fire for direct arch_{add,remove}_memory() users outside of
- * add_memory_resource().
+ * Disallow all operations smaller than a sub-section.
+ * Note that check_hotplug_memory_range() enforces a larger
+ * memory_block_size_bytes() granularity for memory that will be marked
+ * online, so this check should only fire for direct
+ * arch_{add,remove}_memory() users outside of add_memory_resource().
*/
- unsigned long min_align;
-
- if (IS_ENABLED(CONFIG_SPARSEMEM_VMEMMAP))
- min_align = PAGES_PER_SUBSECTION;
- else
- min_align = PAGES_PER_SECTION;
- if (!IS_ALIGNED(pfn | nr_pages, min_align))
+ if (!IS_ALIGNED(pfn | nr_pages, PAGES_PER_SUBSECTION))
return -EINVAL;
return 0;
}
@@ -584,6 +579,7 @@ void remove_pfn_range_from_zone(struct zone *zone,
* @pfn: starting pageframe (must be aligned to start of a section)
* @nr_pages: number of pages to remove (must be multiple of section size)
* @altmap: alternative device page map or %NULL if default memmap is used
+ * @pgmap: device page map or %NULL if not ZONE_DEVICE
*
* Generic helper function to remove section mappings and sysfs entries
* for the section of the memory we are removing. Caller needs to make
@@ -591,7 +587,7 @@ void remove_pfn_range_from_zone(struct zone *zone,
* calling offline_pages().
*/
void __remove_pages(unsigned long pfn, unsigned long nr_pages,
- struct vmem_altmap *altmap)
+ struct vmem_altmap *altmap, struct dev_pagemap *pgmap)
{
const unsigned long end_pfn = pfn + nr_pages;
unsigned long cur_nr_pages;
@@ -606,7 +602,7 @@ void __remove_pages(unsigned long pfn, unsigned long nr_pages,
/* Select all remaining pages up to the next section boundary */
cur_nr_pages = min(end_pfn - pfn,
SECTION_ALIGN_UP(pfn + 1) - pfn);
- sparse_remove_section(pfn, cur_nr_pages, altmap);
+ sparse_remove_section(pfn, cur_nr_pages, altmap, pgmap);
}
}
@@ -1046,7 +1042,7 @@ static inline struct zone *default_zone_for_pfn(int nid, unsigned long start_pfn
return movable_node_enabled ? movable_zone : kernel_zone;
}
-struct zone *zone_for_pfn_range(int online_type, int nid,
+struct zone *zone_for_pfn_range(enum mmop online_type, int nid,
struct memory_group *group, unsigned long start_pfn,
unsigned long nr_pages)
{
@@ -1209,6 +1205,13 @@ int online_pages(unsigned long pfn, unsigned long nr_pages,
if (node_arg.nid >= 0)
node_set_state(nid, N_MEMORY);
+ /*
+ * Check whether we are adding normal memory to the node for the first
+ * time.
+ */
+ if (!node_state(nid, N_NORMAL_MEMORY) && zone_idx(zone) <= ZONE_NORMAL)
+ node_set_state(nid, N_NORMAL_MEMORY);
+
if (need_zonelists_rebuild)
build_all_zonelists(NULL);
@@ -1263,7 +1266,8 @@ static pg_data_t *hotadd_init_pgdat(int nid)
pgdat = NODE_DATA(nid);
/* init node's zones as empty zones, we don't have any present pages.*/
- free_area_init_core_hotplug(pgdat);
+ if (free_area_init_core_hotplug(pgdat))
+ return NULL;
/*
* The node we allocated has no zone fallback lists. For avoiding
@@ -1337,7 +1341,9 @@ static int check_hotplug_memory_range(u64 start, u64 size)
static int online_memory_block(struct memory_block *mem, void *arg)
{
- mem->online_type = mhp_get_default_online_type();
+ enum mmop *online_type = arg;
+
+ mem->online_type = *online_type;
return device_online(&mem->dev);
}
@@ -1403,6 +1409,12 @@ bool mhp_supports_memmap_on_memory(void)
}
EXPORT_SYMBOL_GPL(mhp_supports_memmap_on_memory);
+static void altmap_free(struct vmem_altmap *altmap)
+{
+ WARN_ONCE(altmap->alloc, "Altmap not fully unmapped");
+ kfree(altmap);
+}
+
static void remove_memory_blocks_and_altmaps(u64 start, u64 size)
{
unsigned long memblock_size = memory_block_size_bytes();
@@ -1417,20 +1429,17 @@ static void remove_memory_blocks_and_altmaps(u64 start, u64 size)
struct vmem_altmap *altmap = NULL;
struct memory_block *mem;
- mem = find_memory_block(pfn_to_section_nr(PFN_DOWN(cur_start)));
+ mem = memory_block_get(phys_to_block_id(cur_start));
if (WARN_ON_ONCE(!mem))
continue;
altmap = mem->altmap;
mem->altmap = NULL;
+ memory_block_put(mem);
remove_memory_block_devices(cur_start, memblock_size);
-
- arch_remove_memory(cur_start, memblock_size, altmap);
-
- /* Verify that all vmemmap pages have actually been freed. */
- WARN(altmap->alloc, "Altmap not fully unmapped");
- kfree(altmap);
+ arch_remove_memory(cur_start, memblock_size, altmap, NULL);
+ altmap_free(altmap);
}
}
@@ -1461,7 +1470,7 @@ static int create_altmaps_and_memory_blocks(int nid, struct memory_group *group,
/* call arch's memory hotadd */
ret = arch_add_memory(nid, cur_start, memblock_size, &params);
if (ret < 0) {
- kfree(params.altmap);
+ altmap_free(params.altmap);
goto out;
}
@@ -1469,8 +1478,8 @@ static int create_altmaps_and_memory_blocks(int nid, struct memory_group *group,
ret = create_memory_block_devices(cur_start, memblock_size, nid,
params.altmap, group);
if (ret) {
- arch_remove_memory(cur_start, memblock_size, NULL);
- kfree(params.altmap);
+ arch_remove_memory(cur_start, memblock_size, params.altmap, NULL);
+ altmap_free(params.altmap);
goto out;
}
}
@@ -1488,7 +1497,8 @@ out:
*
* we are OK calling __meminit stuff here - we have CONFIG_MEMORY_HOTPLUG
*/
-int add_memory_resource(int nid, struct resource *res, mhp_t mhp_flags)
+static int __add_memory_resource(int nid, struct resource *res, mhp_t mhp_flags,
+ enum mmop online_type)
{
struct mhp_params params = { .pgprot = pgprot_mhp(PAGE_KERNEL) };
enum memblock_flags memblock_flags = MEMBLOCK_NONE;
@@ -1555,7 +1565,7 @@ int add_memory_resource(int nid, struct resource *res, mhp_t mhp_flags)
/* create memory block devices after memory was added */
ret = create_memory_block_devices(start, size, nid, NULL, group);
if (ret) {
- arch_remove_memory(start, size, params.altmap);
+ arch_remove_memory(start, size, params.altmap, NULL);
goto error;
}
}
@@ -1578,8 +1588,9 @@ int add_memory_resource(int nid, struct resource *res, mhp_t mhp_flags)
merge_system_ram_resource(res);
/* online pages if requested */
- if (mhp_get_default_online_type() != MMOP_OFFLINE)
- walk_memory_blocks(start, size, NULL, online_memory_block);
+ if (online_type != MMOP_OFFLINE)
+ walk_memory_blocks(start, size, &online_type,
+ online_memory_block);
return ret;
error:
@@ -1595,7 +1606,13 @@ error_mem_hotplug_end:
return ret;
}
-/* requires device_hotplug_lock, see add_memory_resource() */
+int add_memory_resource(int nid, struct resource *res, mhp_t mhp_flags)
+{
+ return __add_memory_resource(nid, res, mhp_flags,
+ mhp_get_default_online_type());
+}
+
+/* requires device_hotplug_lock, see __add_memory_resource() */
int __add_memory(int nid, u64 start, u64 size, mhp_t mhp_flags)
{
struct resource *res;
@@ -1623,7 +1640,15 @@ int add_memory(int nid, u64 start, u64 size, mhp_t mhp_flags)
}
EXPORT_SYMBOL_GPL(add_memory);
-/*
+/**
+ * __add_memory_driver_managed - add driver-managed memory with explicit online_type
+ * @nid: NUMA node ID where the memory will be added
+ * @start: Start physical address of the memory range
+ * @size: Size of the memory range in bytes
+ * @resource_name: Resource name in format "System RAM ($DRIVER)"
+ * @mhp_flags: Memory hotplug flags
+ * @online_type: Auto-Online behavior (offline, online, kernel, movable)
+ *
* Add special, driver-managed memory to the system as system RAM. Such
* memory is not exposed via the raw firmware-provided memmap as system
* RAM, instead, it is detected and added by a driver - during cold boot,
@@ -1631,6 +1656,7 @@ EXPORT_SYMBOL_GPL(add_memory);
*
* Reasons why this memory should not be used for the initial memmap of a
* kexec kernel or for placing kexec images:
+ *
* - The booting kernel is in charge of determining how this memory will be
* used (e.g., use persistent memory as system RAM)
* - Coordination with a hypervisor is required before this memory
@@ -1643,9 +1669,12 @@ EXPORT_SYMBOL_GPL(add_memory);
*
* The resource_name (visible via /proc/iomem) has to have the format
* "System RAM ($DRIVER)".
+ *
+ * Return: 0 on success, negative error code on failure.
*/
-int add_memory_driver_managed(int nid, u64 start, u64 size,
- const char *resource_name, mhp_t mhp_flags)
+int __add_memory_driver_managed(int nid, u64 start, u64 size,
+ const char *resource_name, mhp_t mhp_flags,
+ enum mmop online_type)
{
struct resource *res;
int rc;
@@ -1655,6 +1684,9 @@ int add_memory_driver_managed(int nid, u64 start, u64 size,
resource_name[strlen(resource_name) - 1] != ')')
return -EINVAL;
+ if (online_type < MMOP_OFFLINE || online_type > MMOP_ONLINE_MOVABLE)
+ return -EINVAL;
+
lock_device_hotplug();
res = register_memory_resource(start, size, resource_name);
@@ -1663,7 +1695,7 @@ int add_memory_driver_managed(int nid, u64 start, u64 size,
goto out_unlock;
}
- rc = add_memory_resource(nid, res, mhp_flags);
+ rc = __add_memory_resource(nid, res, mhp_flags, online_type);
if (rc < 0)
release_memory_resource(res);
@@ -1671,6 +1703,30 @@ out_unlock:
unlock_device_hotplug();
return rc;
}
+EXPORT_SYMBOL_FOR_MODULES(__add_memory_driver_managed, "kmem");
+
+/**
+ * add_memory_driver_managed - add driver-managed memory
+ * @nid: NUMA node ID where the memory will be added
+ * @start: Start physical address of the memory range
+ * @size: Size of the memory range in bytes
+ * @resource_name: Resource name in format "System RAM ($DRIVER)"
+ * @mhp_flags: Memory hotplug flags
+ *
+ * Add driver-managed memory with the system default online type set by
+ * build config or kernel boot parameter.
+ *
+ * See __add_memory_driver_managed for more details.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int add_memory_driver_managed(int nid, u64 start, u64 size,
+ const char *resource_name, mhp_t mhp_flags)
+{
+ return __add_memory_driver_managed(nid, start, size, resource_name,
+ mhp_flags,
+ mhp_get_default_online_type());
+}
EXPORT_SYMBOL_GPL(add_memory_driver_managed);
/*
@@ -1745,7 +1801,8 @@ static int scan_movable_pages(unsigned long start, unsigned long end,
{
unsigned long pfn;
- for_each_valid_pfn(pfn, start, end) {
+ for (pfn = start; pfn < end; pfn++) {
+ unsigned long nr_pages;
struct page *page;
struct folio *folio;
@@ -1762,9 +1819,9 @@ static int scan_movable_pages(unsigned long start, unsigned long end,
if (PageOffline(page) && page_count(page))
return -EBUSY;
- if (!PageHuge(page))
- continue;
folio = page_folio(page);
+ if (!folio_test_hugetlb(folio))
+ continue;
/*
* This test is racy as we hold no reference or lock. The
* hugetlb page could have been free'ed and head is no longer
@@ -1774,7 +1831,11 @@ static int scan_movable_pages(unsigned long start, unsigned long end,
*/
if (folio_test_hugetlb_migratable(folio))
goto found;
- pfn |= folio_nr_pages(folio) - 1;
+ nr_pages = folio_nr_pages(folio);
+ if (unlikely(nr_pages < 1 || nr_pages > MAX_FOLIO_NR_PAGES ||
+ !is_power_of_2(nr_pages)))
+ continue;
+ pfn |= nr_pages - 1;
}
return -ENOENT;
found:
@@ -1790,7 +1851,7 @@ static void do_migrate_range(unsigned long start_pfn, unsigned long end_pfn)
static DEFINE_RATELIMIT_STATE(migrate_rs, DEFAULT_RATELIMIT_INTERVAL,
DEFAULT_RATELIMIT_BURST);
- for_each_valid_pfn(pfn, start_pfn, end_pfn) {
+ for (pfn = start_pfn; pfn < end_pfn; pfn++) {
struct page *page;
page = pfn_to_page(pfn);
@@ -1908,6 +1969,8 @@ int offline_pages(unsigned long start_pfn, unsigned long nr_pages,
unsigned long flags;
char *reason;
int ret;
+ unsigned long normal_pages = 0;
+ enum zone_type zt;
/*
* {on,off}lining is constrained to full memory sections (or more
@@ -2056,6 +2119,17 @@ int offline_pages(unsigned long start_pfn, unsigned long nr_pages,
init_per_zone_wmark_min();
/*
+ * Check whether this operation removes the last normal memory from
+ * the node. We do this before clearing N_MEMORY to avoid the possible
+ * transient "!N_MEMORY && N_NORMAL_MEMORY" state.
+ */
+ if (zone_idx(zone) <= ZONE_NORMAL) {
+ for (zt = 0; zt <= ZONE_NORMAL; zt++)
+ normal_pages += pgdat->node_zones[zt].present_pages;
+ if (!normal_pages)
+ node_clear_state(node, N_NORMAL_MEMORY);
+ }
+ /*
* Make sure to mark the node as memory-less before rebuilding the zone
* list. Otherwise this node would still appear in the fallback lists.
*/
@@ -2249,7 +2323,7 @@ static int try_remove_memory(u64 start, u64 size)
* No altmaps present, do the removal directly
*/
remove_memory_block_devices(start, size);
- arch_remove_memory(start, size, NULL);
+ arch_remove_memory(start, size, NULL, NULL);
} else {
/* all memblocks in the range have altmaps */
remove_memory_blocks_and_altmaps(start, size);
@@ -2305,7 +2379,7 @@ EXPORT_SYMBOL_GPL(remove_memory);
static int try_offline_memory_block(struct memory_block *mem, void *arg)
{
- uint8_t online_type = MMOP_ONLINE_KERNEL;
+ enum mmop online_type = MMOP_ONLINE_KERNEL;
uint8_t **online_types = arg;
struct page *page;
int rc;
@@ -2338,7 +2412,7 @@ static int try_reonline_memory_block(struct memory_block *mem, void *arg)
int rc;
if (**online_types != MMOP_OFFLINE) {
- mem->online_type = **online_types;
+ mem->online_type = (enum mmop)**online_types;
rc = device_online(&mem->dev);
if (rc < 0)
pr_warn("%s: Failed to re-online memory: %d",
@@ -2358,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();
+ struct range range = {
+ .start = start,
+ .end = start + size - 1,
+ };
+
+ 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;
- int rc;
+ unsigned int i;
+ int rc = 0;
- if (!IS_ALIGNED(start, memory_block_size_bytes()) ||
- !IS_ALIGNED(size, memory_block_size_bytes()) || !size)
+ 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 */