Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

22. 메모리 Hotplug

관련 소스: mm/memory_hotplug.c, include/linux/memory_hotplug.h, include/linux/memory.h
관련 문서: 00-메모리 관리 개요 · 04-Memblock · 13-NUMA · 11-Compaction · 20-Migration · 18-CMA · 24-티어링

개요 (Overview)

메모리 Hotplug는 런타임에 물리 메모리를 커널에 추가(Hot-Add)하거나 제거(Hot-Remove)하는 메커니즘입니다. 가상화 환경(KVM, Hyper-V, Xen), CXL 장치, DIMM 슬롯 교체 시스템에서 메모리 용량을 동적으로 조정하는 데 사용됩니다. Linux 7.0에서는 mm/memory_hotplug.c가 이 전체 라이프사이클을 관리하며, ACPI 이벤트, sysfs, virtio-mem 같은 다양한 트리거를 지원합니다.

메모리 Hotplug는 크게 두 단계로 구성됩니다. (1) Hot-Add: 물리 메모리 레인지가 커널에 등록되고 struct page 배열(vmemmap)이 생성됩니다. 이 시점에서 메모리는 아직 사용 불가 상태입니다. (2) Online: 등록된 메모리 블록을 Buddy Allocator의 free list에 추가하여 할당 가능한 상태로 만듭니다. 반대로 제거 시에는 Offline(모든 페이지 마이그레이션) → Hot-Remove(커널 등록 해제) 순서로 진행됩니다. 오프라인 실패의 주된 원인은 MIGRATE_UNMOVABLE 타입의 페이지(커널 slab 객체, 페이지 테이블 등)가 해당 블록에 존재하는 경우이며, 이를 방지하려면 핫플러그 대상 메모리를 ZONE_MOVABLE에 배치해야 합니다.

일상적으로 보면 메모리 Hotplug는 운영 중인 도서관에 새 서가를 꽂거나 다시 빼는 작업과 비슷합니다. 서가를 들여놓는 것만으로는 책을 바로 빌릴 수 없고(Hot-Add), 서가 안의 책을 다른 구역으로 모두 옮겨야 안전하게 비울 수 있습니다(Offline → Hot-Remove). kernelcore=movablecore=는 처음부터 서가의 비율을 나누는 부팅 파라미터이고, online_policy/sys/devices/system/memory/auto_online_blocks는 새로 들어온 메모리를 어느 구역에 둘지 정하는 운영 스위치입니다.

/* 주요 소스 파일 경로 */
mm/memory_hotplug.c          /* 핵심 Hotplug 로직 (2435줄) */
include/linux/memory_hotplug.h  /* API 선언, mhp_params, MMOP_* 열거 */
include/linux/memory.h         /* struct memory_block, memory_group 정의 */
mm/sparse.c                   /* SPARSEMEM 섹션 관리 (sparse_add_section) */
drivers/base/memory.c         /* sysfs 인터페이스, memory_block 디바이스 */

빠른 점검 명령

# 메모리 블록 상태 전체 확인
cat /sys/devices/system/memory/memory*/state | sort | uniq -c | sort -rn

# 특정 메모리 블록 온라인/오프라인
echo online > /sys/devices/system/memory/memory32/state
echo offline > /sys/devices/system/memory/memory32/state

# ZONE_MOVABLE 온라인 (나중에 제거 가능)
echo online_movable > /sys/devices/system/memory/memory32/state

# 자동 온라인 정책 확인
cat /sys/devices/system/memory/auto_online_blocks
# online_movable / online_kernel / offline

# 현재 메모리 Hotplug 관련 커널 파라미터
cat /proc/cmdline | tr ' ' '\n' | grep -E 'memhp|movable|hotplug'

# kernelcore/movablecore/movable_node 설정 확인
cat /proc/cmdline | tr ' ' '\n' | grep -E 'kernelcore|movablecore|memhp_default_state|movable_node'

# NUMA 노드별 메모리 블록 정보
cat /sys/devices/system/node/node*/meminfo | head -20

# 물리 메모리 전체 맵 (/proc/iomem에서 System RAM 확인)
sudo cat /proc/iomem | grep -i "System RAM"

# 메모리 Hotplug 관련 커널 로그
dmesg | grep -Ei 'hotplug|memory block|online|offline'

# 커널 설정에서 Hotplug 관련 옵션 확인
zgrep -E 'CONFIG_MEMORY_HOTPLUG|CONFIG_MEMORY_HOTREMOVE|CONFIG_MHP_MEMMAP_ON_MEMORY' /proc/config.gz 2>/dev/null

# 메모리 블록 크기 확인 (보통 128MB)
cat /sys/devices/system/memory/block_size_bytes | xargs printf "%d\n"

# 자동 온라인 비율 설정
cat /sys/module/memory_hotplug/parameters/auto_movable_ratio
cat /sys/module/memory_hotplug/parameters/online_policy

# memmap_on_memory 모드 확인
cat /sys/module/memory_hotplug/parameters/memmap_on_memory

핵심 자료구조

struct memory_block — 메모리 블록 디바이스

메모리 Hotplug의 기본 단위입니다. sysfs의 /sys/devices/system/memory/memoryN으로 노출되며, 하나의 메모리 블록은 하나 이상의 SPARSEMEM 섹션으로 구성됩니다.

/* include/linux/memory.h:77-96 */
struct memory_block {
    unsigned long start_section_nr;              /* 시작 섹션 번호 */
    enum memory_block_state state;               /* MEM_ONLINE, MEM_OFFLINE 등 */
    int online_type;                             /* MMOP_ONLINE_KERNEL 등 */
    int nid;                                     /* 이 블록의 NUMA 노드 ID */
    struct zone *zone;                           /* 단일 zone (없으면 NULL) */
    struct device dev;                           /* sysfs 디바이스 구조체 */
    struct vmem_altmap *altmap;                  /* memmap_on_memory용 대체 맵 */
    struct memory_group *group;                  /* 소속 메모리 그룹 */
    struct list_head group_next;                 /* 그룹 내 다음 블록 */
#if defined(CONFIG_MEMORY_FAILURE) && defined(CONFIG_MEMORY_HOTPLUG)
    atomic_long_t nr_hwpoison;                   /* 하드웨어 오류 페이지 수 */
#endif
};

struct memory_group — 메모리 그룹

하나의 메모리 디바이스(DIMM, CXL 리전 등)가 관리하는 여러 메모리 블록을 논리적으로 묶습니다. 정적(static)과 동적(dynamic) 두 가지 유형이 있습니다.

/* include/linux/memory.h:51-65 */
struct memory_group {
    int nid;                                     /* 그룹 전체 노드 ID */
    struct list_head memory_blocks;              /* 소속 메모리 블록 목록 */
    unsigned long present_kernel_pages;          /* ZONE_MOVABLE 외 온라인 페이지 수 */
    unsigned long present_movable_pages;         /* ZONE_MOVABLE 내 온라인 페이지 수 */
    bool is_dynamic;                             /* 정적 vs 동적 그룹 */
    union {
        struct {
            unsigned long max_pages;             /* 정적: 최대 페이지 수 */
        } s;
        struct {
            unsigned long unit_pages;            /* 동적: 단위 페이지 수 (추가/제거 단위) */
        } d;
    };
};

struct mhp_params — Hot-Add 확장 파라미터

add_memory_resource()에 전달되는 매개변수를 담습니다. 페이지 테이블 보호 플래그, 대체 memmap 할당자, ZONE_DEVICE 맵을 지정합니다.

/* include/linux/memory_hotplug.h:68-72 */
struct mhp_params {
    struct vmem_altmap *altmap;     /* memmap 배열의 대체 할당자 (선택) */
    pgprot_t pgprot;                /* 새 페이지 테이블의 보호 플래그 (필수) */
    struct dev_pagemap *pgmap;      /* ZONE_DEVICE용 페이지 맵 (선택) */
};

enum memory_block_state — 블록 상태 머신

sysfs를 통해 사용자 공간에 텍스트로 노출되는 상태입니다.

/* include/linux/memory.h:67-75 */
enum memory_block_state {
    MEM_ONLINE,          /* 온라인 — Buddy free list에 추가됨 */
    MEM_GOING_OFFLINE,   /* 오프라인 진행 중 */
    MEM_OFFLINE,         /* 오프라인 — 사용 불가 */
    MEM_GOING_ONLINE,    /* 온라인 진행 중 */
    MEM_CANCEL_ONLINE,   /* 온라인 취소됨 */
    MEM_CANCEL_OFFLINE,  /* 오프라인 취소됨 */
};

메모리 블록 상태 전이

전이트리거핵심 동작
Hot-Add → Registered`add_memory_resource()`memblock 추가, memory_block 디바이스 생성, 아직 할당 불가
Registered → Online`online_pages()`zone 연결, Buddy free list 추가, 워터마크/kswapd/kcompactd 갱신
Online → Offline`offline_pages()`페이지 마이그레이션, 격리, Buddy 제거
Offline → Hot-Remove`try_remove_memory()`memory_block 디바이스 제거, arch 메모리 해제, memblock 정리

MMOP_* 온라인 타입 열거

메모리 블록을 어떤 Zone에 온라인시킬지를 결정합니다.

/* include/linux/memory_hotplug.h:23-32 */
enum {
    MMOP_OFFLINE = 0,           /* 오프라인 */
    MMOP_ONLINE,                /* 기본 온라인 — zone 자동 결정 */
    MMOP_ONLINE_KERNEL,         /* ZONE_NORMAL(커널)에 온라인 */
    MMOP_ONLINE_MOVABLE,        /* ZONE_MOVABLE에 온라인 (제거 가능) */
};

auto_movable_stats — 자동 MOVABLE 통계

auto-movable 온라인 정책에서 MOVABLE:KERNEL 비율을 판단하기 위한 통계 구조입니다.

/* mm/memory_hotplug.c:789-792 */
struct auto_movable_stats {
    unsigned long kernel_early_pages;  /* 부팅 초기 커널 영역 페이지 수 */
    unsigned long movable_pages;       /* ZONE_MOVABLE 페이지 수 */
};

핵심 함수

1. online_pages() — 메모리 온라인

move_pfn_range_to_zone()로 pfn 범위를 zone에 연결한 뒤, online_pages_range()를 통해 Buddy에 페이지를 추가합니다. 온라인 완료 후 zonelist 재구성, watermarks 초기화, kswapd/kcompactd 시작을 수행합니다.

/* mm/memory_hotplug.c:1144-1257 */
int online_pages(unsigned long pfn, unsigned long nr_pages,
		       struct zone *zone, struct memory_group *group)
{
    struct memory_notify mem_arg = {
		.start_pfn = pfn,
		.nr_pages = nr_pages,
	};
	struct node_notify node_arg = {
		.nid = NUMA_NO_NODE,
	};
	const int nid = zone_to_nid(zone);
	int need_zonelists_rebuild = 0;
	unsigned long flags;
	int ret;

	if (WARN_ON_ONCE(!nr_pages || !pageblock_aligned(pfn) ||
			 !IS_ALIGNED(pfn + nr_pages, PAGES_PER_SECTION)))
		return -EINVAL;

	move_pfn_range_to_zone(zone, pfn, nr_pages, NULL, MIGRATE_MOVABLE,
			       true);

	if (!node_state(nid, N_MEMORY)) {
		node_arg.nid = nid;
		ret = node_notify(NODE_ADDING_FIRST_MEMORY, &node_arg);
		ret = notifier_to_errno(ret);
		if (ret)
			goto failed_addition;
	}

	ret = memory_notify(MEM_GOING_ONLINE, &mem_arg);
	ret = notifier_to_errno(ret);
	if (ret)
		goto failed_addition;

	spin_lock_irqsave(&zone->lock, flags);
	zone->nr_isolate_pageblock += nr_pages / pageblock_nr_pages;
	spin_unlock_irqrestore(&zone->lock, flags);

	if (!populated_zone(zone)) {
		need_zonelists_rebuild = 1;
		setup_zone_pageset(zone);
	}

	online_pages_range(pfn, nr_pages);
	adjust_present_page_count(pfn_to_page(pfn), group, nr_pages);

	if (node_arg.nid >= 0)
		node_set_state(nid, N_MEMORY);
	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);

	undo_isolate_page_range(pfn, pfn + nr_pages);
	shuffle_zone(zone);
	init_per_zone_wmark_min();
	kswapd_run(nid);
	kcompactd_run(nid);

	if (node_arg.nid >= 0)
		node_notify(NODE_ADDED_FIRST_MEMORY, &node_arg);

	writeback_set_ratelimit();

	memory_notify(MEM_ONLINE, &mem_arg);
	return 0;

failed_addition:
	remove_pfn_range_from_zone(zone, pfn, nr_pages);
	return ret;
}

분기 로직:

  • !node_state(nid, N_MEMORY) → 최초 메모리 추가 노드 알림
  • !populated_zone(zone) → zonelist 재구성 + pageset 설정
  • 실패 시 remove_pfn_range_from_zone()으로 롤백
  • 2. offline_pages() — 메모리 오프라인

    모든 페이지를 다른 블록으로 마이그레이션한 뒤, Buddy에서 제거합니다. PCP 비활성화, LRU 캐시 비활성화, 페이지 격리 순서로 진행됩니다.

    /* mm/memory_hotplug.c:1901-2117 */
    int offline_pages(unsigned long start_pfn, unsigned long nr_pages,
    		 struct zone *zone, struct memory_group *group)
    {
        unsigned long pfn, managed_pages, system_ram_pages = 0;
    	const unsigned long end_pfn = start_pfn + nr_pages;
    	struct pglist_data *pgdat = zone->zone_pgdat;
    	const int node = zone_to_nid(zone);
    	struct memory_notify mem_arg = {
    		.start_pfn = start_pfn,
    		.nr_pages = nr_pages,
    	};
    	struct node_notify node_arg = {
    		.nid = NUMA_NO_NODE,
    	};
    	unsigned long flags;
    	char *reason;
    	int ret;
    	unsigned long normal_pages = 0;
    	enum zone_type zt;
    
    	if (WARN_ON_ONCE(!nr_pages || !pageblock_aligned(start_pfn) ||
    			 !IS_ALIGNED(start_pfn + nr_pages, PAGES_PER_SECTION)))
    		return -EINVAL;
    
    	walk_system_ram_range(start_pfn, nr_pages, &system_ram_pages,
    			     count_system_ram_pages_cb);
    	if (system_ram_pages != nr_pages) {
    		ret = -EINVAL;
    		reason = "memory holes";
    		goto failed_removal;
    	}
    
    	if (WARN_ON_ONCE(page_zone(pfn_to_page(start_pfn)) != zone ||
    			 page_zone(pfn_to_page(end_pfn - 1)) != zone)) {
    		ret = -EINVAL;
    		reason = "multizone range";
    		goto failed_removal;
    	}
    
    	zone_pcp_disable(zone);
    	lru_cache_disable();
    
    	ret = start_isolate_page_range(start_pfn, end_pfn,
    				       PB_ISOLATE_MODE_MEM_OFFLINE);
    	if (ret) {
    		reason = "failure to isolate range";
    		goto failed_removal_pcplists_disabled;
    	}
    
    	if (nr_pages >= pgdat->node_present_pages) {
    		node_arg.nid = node;
    		ret = node_notify(NODE_REMOVING_LAST_MEMORY, &node_arg);
    		ret = notifier_to_errno(ret);
    		if (ret) {
    			reason = "node notifier failure";
    			goto failed_removal_isolated;
    		}
    	}
    
    	ret = memory_notify(MEM_GOING_OFFLINE, &mem_arg);
    	ret = notifier_to_errno(ret);
    	if (ret) {
    		reason = "notifier failure";
    		goto failed_removal_isolated;
    	}
    
    	do {
    		pfn = start_pfn;
    		do {
    			if (signal_pending(current)) {
    				ret = -EINTR;
    				reason = "signal backoff";
    				goto failed_removal_isolated;
    			}
    
    			cond_resched();
    
    			ret = scan_movable_pages(pfn, end_pfn, &pfn);
    			if (!ret)
    				do_migrate_range(pfn, end_pfn);
    		} while (!ret);
    
    		if (ret != -ENOENT) {
    			reason = "unmovable page";
    			goto failed_removal_isolated;
    		}
    
    		ret = dissolve_free_hugetlb_folios(start_pfn, end_pfn);
    		if (ret) {
    			reason = "failure to dissolve huge pages";
    			goto failed_removal_isolated;
    		}
    
    		ret = test_pages_isolated(start_pfn, end_pfn,
    					  PB_ISOLATE_MODE_MEM_OFFLINE);
    	} while (ret);
    
    	managed_pages = __offline_isolated_pages(start_pfn, end_pfn);
    	spin_lock_irqsave(&zone->lock, flags);
    	zone->nr_isolate_pageblock -= nr_pages / pageblock_nr_pages;
    	spin_unlock_irqrestore(&zone->lock, flags);
    
    	lru_cache_enable();
    	zone_pcp_enable(zone);
    
    	adjust_managed_page_count(pfn_to_page(start_pfn), -managed_pages);
    	adjust_present_page_count(pfn_to_page(start_pfn), group, -nr_pages);
    	init_per_zone_wmark_min();
    
    	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);
    	}
    
    	if (node_arg.nid >= 0)
    		node_clear_state(node, N_MEMORY);
    	if (!populated_zone(zone)) {
    		zone_pcp_reset(zone);
    		build_all_zonelists(NULL);
    	}
    
    	if (node_arg.nid >= 0) {
    		kcompactd_stop(node);
    		kswapd_stop(node);
    		node_notify(NODE_REMOVED_LAST_MEMORY, &node_arg);
    	}
    
    	writeback_set_ratelimit();
    
    	memory_notify(MEM_OFFLINE, &mem_arg);
    	remove_pfn_range_from_zone(zone, start_pfn, nr_pages);
    	return 0;
    
    failed_removal_isolated:
    	undo_isolate_page_range(start_pfn, end_pfn);
    	memory_notify(MEM_CANCEL_OFFLINE, &mem_arg);
    	if (node_arg.nid != NUMA_NO_NODE)
    		node_notify(NODE_CANCEL_REMOVING_LAST_MEMORY, &node_arg);
    failed_removal_pcplists_disabled:
    	lru_cache_enable();
    	zone_pcp_enable(zone);
    failed_removal:
    	return ret;
    }

    분기 로직:

  • system_ram_pages != nr_pages → 메모리 홀 존재 → 오프라인 불가
  • signal_pending(current) → 시그널로 인한 중단 → -EINTR
  • scan_movable_pages() 반환값 -EBUSY → 이동 불가 페이지 존재 → 실패
  • 3. add_memory_resource() — 메모리 추가 (리소스 기반)

    물리 메모리 레인지가 커널에 등록되는 전체 과정을 담당합니다. memblock 추가, 노드 초기화, arch 메모리 추가, 메모리 블록 디바이스 생성, 온라인까지의 전체 흐름을 제어합니다.

    /* mm/memory_hotplug.c:1498-1603 */
    int add_memory_resource(int nid, struct resource *res, mhp_t mhp_flags)
    {
    	struct mhp_params params = { .pgprot = pgprot_mhp(PAGE_KERNEL) };
    	enum memblock_flags memblock_flags = MEMBLOCK_NONE;
    	struct memory_group *group = NULL;
    	u64 start, size;
    	bool new_node = false;
    	int ret;
    
    	start = res->start;
    	size = resource_size(res);
    
    	ret = check_hotplug_memory_range(start, size);
    	if (ret)
    		return ret;
    
    	if (mhp_flags & MHP_NID_IS_MGID) {
    		group = memory_group_find_by_id(nid);
    		if (!group)
    			return -EINVAL;
    		nid = group->nid;
    	}
    
    	if (!node_possible(nid)) {
    		WARN(1, "node %d was absent from the node_possible_map\n", nid);
    		return -EINVAL;
    	}
    
    	mem_hotplug_begin();
    
    	if (IS_ENABLED(CONFIG_ARCH_KEEP_MEMBLOCK)) {
    		if (res->flags & IORESOURCE_SYSRAM_DRIVER_MANAGED)
    			memblock_flags = MEMBLOCK_DRIVER_MANAGED;
    		ret = memblock_add_node(start, size, nid, memblock_flags);
    		if (ret)
    			goto error_mem_hotplug_end;
    	}
    
    	ret = __try_online_node(nid, false);
    	if (ret < 0)
    		goto error_memblock_remove;
    	if (ret) {
    		node_set_online(nid);
    		ret = register_node(nid);
    		if (WARN_ON(ret)) {
    			node_set_offline(nid);
    			goto error_memblock_remove;
    		}
    		new_node = true;
    	}
    
    	if ((mhp_flags & MHP_MEMMAP_ON_MEMORY) &&
    	    mhp_supports_memmap_on_memory()) {
    		ret = create_altmaps_and_memory_blocks(nid, group, start, size);
    		if (ret)
    			goto error;
    	} else {
    		ret = arch_add_memory(nid, start, size, &params);
    		if (ret < 0)
    			goto error;
    
    		ret = create_memory_block_devices(start, size, nid, NULL, group);
    		if (ret) {
    			arch_remove_memory(start, size, params.altmap);
    			goto error;
    		}
    	}
    
    	register_memory_blocks_under_node_hotplug(nid, PFN_DOWN(start),
    					  PFN_UP(start + size - 1));
    
    	if (!strcmp(res->name, "System RAM"))
    		firmware_map_add_hotplug(start, start + size, "System RAM");
    
    	mem_hotplug_done();
    
    	if (mhp_flags & MHP_MERGE_RESOURCE)
    		merge_system_ram_resource(res);
    
    	if (mhp_get_default_online_type() != MMOP_OFFLINE)
    		walk_memory_blocks(start, size, NULL, online_memory_block);
    
    	return ret;
    error:
    	if (new_node) {
    		node_set_offline(nid);
    		unregister_node(nid);
    	}
    error_memblock_remove:
    	if (IS_ENABLED(CONFIG_ARCH_KEEP_MEMBLOCK))
    		memblock_remove(start, size);
    error_mem_hotplug_end:
    	mem_hotplug_done();
    	return ret;
    }

    분기 로직:

  • MHP_NID_IS_MGID → nid 대신 memory_group ID 사용
  • MHP_MEMMAP_ON_MEMORY + 지원 가능 → altmap 기반 memmap 할당
  • MHP_MERGE_RESOURCE → 인접 리소스와 병합
  • 4. try_remove_memory() — 메모리 제거

    모든 메모리 블록이 오프라인 상태인지 확인한 뒤, 리소스 해제, 메모리 블록 디바이스 제거, arch 메모리 제거를 수행합니다.

    /* mm/memory_hotplug.c:2237-2288 */
    static int try_remove_memory(u64 start, u64 size)
    {
        int rc, nid = NUMA_NO_NODE;
    
        BUG_ON(check_hotplug_memory_range(start, size));
    
        rc = walk_memory_blocks(start, size, &nid, check_memblock_offlined_cb);
        if (rc)
            return rc;
    
        firmware_map_remove(start, start + size, "System RAM");
    
        mem_hotplug_begin();
    
        rc = memory_blocks_have_altmaps(start, size);
        if (rc < 0) {
            mem_hotplug_done();
            return rc;
        } else if (!rc) {
            remove_memory_block_devices(start, size);
            arch_remove_memory(start, size, NULL);
        } else {
            remove_memory_blocks_and_altmaps(start, size);
        }
    
        if (IS_ENABLED(CONFIG_ARCH_KEEP_MEMBLOCK))
            memblock_remove(start, size);
    
        release_mem_region_adjustable(start, size);
    
        if (nid != NUMA_NO_NODE)
            try_offline_node(nid);
    
        mem_hotplug_done();
        return 0;
    }

    5. zone_for_pfn_range() — 온라인 Zone 결정

    메모리 블록을 어떤 Zone에 온라인시킬지를 결정하는 정책 함수입니다. online_policyonline_type에 따라 분기됩니다.

    /* mm/memory_hotplug.c:1049-1062 */
    struct zone *zone_for_pfn_range(int online_type, int nid,
    		struct memory_group *group, unsigned long start_pfn,
    		unsigned long nr_pages)
    {
    	if (online_type == MMOP_ONLINE_KERNEL)
    		return default_kernel_zone_for_pfn(nid, start_pfn, nr_pages);
    
    	if (online_type == MMOP_ONLINE_MOVABLE)
    		return &NODE_DATA(nid)->node_zones[ZONE_MOVABLE];
    
    	if (online_policy == ONLINE_POLICY_AUTO_MOVABLE)
    		return auto_movable_zone_for_pfn(nid, group, start_pfn, nr_pages);
    
    	return default_zone_for_pfn(nid, start_pfn, nr_pages);
    }

    호출 흐름

    Hot-Add 흐름 (메모리 추가)

    드라이버/ACPI/sysfs 트리거
      └→ add_memory()                         /* lock_device_hotplug() 포함 */
          └→ __add_memory()
              └→ register_memory_resource()     /* /proc/iomem에 등록 */
              └→ add_memory_resource()          /* 핵심 추가 로직 */
                  ├→ memblock_add_node()        /* memblock에 등록 */
                  ├→ __try_online_node()        /* 노드 초기화 */
                  ├→ arch_add_memory()          /* 아키텍처별 페이지 테이블 생성 */
                  ├→ create_memory_block_devices()  /* sysfs 블록 생성 */
                  └→ online_memory_block()      /* 자동 온라인 시 */
                      └→ device_online()
                          └→ online_pages()     /* Buddy에 추가 */
                              ├→ move_pfn_range_to_zone()
                              ├→ online_pages_range()
                              │   └→ online_page_callback() (= generic_online_page)
                              │       └→ __free_pages_core()
                              ├→ build_all_zonelists()
                              ├→ init_per_zone_wmark_min()
                              └→ kswapd_run() / kcompactd_run()

    Hot-Remove 흐름 (메모리 제거)

    드라이버/ACPI/sysfs 트리거
      └→ remove_memory() / offline_and_remove_memory()
          └→ try_remove_memory()
              ├→ check_memblock_offlined_cb()  /* 모든 블록 오프라인 확인 */
              ├→ remove_memory_block_devices() / remove_memory_blocks_and_altmaps()
              ├→ arch_remove_memory()          /* 페이지 테이블 제거 */
              ├→ memblock_remove()
              └→ try_offline_node()            /* 노드 오프라인 시도 */
    
    offline_pages()                            /* 개별 블록 오프라인 */
      ├→ zone_pcp_disable() / lru_cache_disable()
      ├→ start_isolate_page_range()           /* 페이지 격리 */
      ├→ scan_movable_pages() + do_migrate_range()  /* 반복 마이그레이션 */
      │   └→ migrate_pages()
      ├→ dissolve_free_hugetlb_folios()
      ├→ test_pages_isolated()
      ├→ __offline_isolated_pages()           /* Buddy에서 제거 */
      └→ remove_pfn_range_from_zone()

    조건별 비교

    온라인 정책 비교

    정책설정 위치동작ZONE_MOVABLE제거 용이성
    `contig-zones``online_policy` 모듈 파라미터기존 zone 경계를 따라 연속 온라인기본 불가어려움
    `auto-movable``online_policy` 모듈 파라미터MOVABLE:KERNEL 비율 기반 자동 결정가능쉬움
    `online_movable`sysfs state명시적으로 ZONE_MOVABLE에 온라인항상가장 쉬움
    `online_kernel`sysfs state명시적으로 ZONE_NORMAL에 온라인불가매우 어려움

    memmap_on_memory 모드 비교

    모드동작메모리 낭비vmemmap 위치사용 시점
    DISABLE (0)memmap을 기존 메모리에 할당없음일반 커널 메모리기본값
    ENABLE (1)Hotplug 메모리 자체에 vmemmap 배치없음Hotplug 메모리 내부CXL/대용량 DIMM
    FORCE (2)pageblock 정렬 맞춰 강제 배치페이지 낭비 가능Hotplug 메모리 내부정렬 문제 해결 필요 시

    메모리 제거 실패 원인 비교

    원인증상해결 방법
    `MIGRATE_UNMOVABLE` 페이지 존재`scan_movable_pages()` → `-EBUSY``online_movable`로 온라인 후 재시도
    메모리 홀 존재`system_ram_pages != nr_pages`홀 없는 메모리만 오프라인 가능
    HugeTLB folio 미해체`dissolve_free_hugetlb_folios()` 실패사전에 HugeTLB 해제
    오프라인 중 시그널`signal_pending()` → `-EINTR`재시도 또는 프로세스 종료
    CPU가 노드에 상주`check_cpu_on_node()` → `-EBUSY`CPU 먼저 오프라인/이동

    메모리 그룹 유형 비교

    유형`is_dynamic`추가 단위제거 단위대표 사용처
    정적 (Static)`false`그룹 전체그룹 전체전통 DIMM
    동적 (Dynamic)`true``unit_pages` 단위`unit_pages` 단위virtio-mem, CXL

    memmap_on_memory 상세

    Linux 7.0에서 메모리 Hotplug 시 struct page 배열(vmemmap)을 어떻게 할당할지 결정하는 메커니즘입니다.

    memmap_on_memory = 0 (DISABLE)
      → vmemmap은 기존 커널 메모리(Direct Mapping 영역)에 할당
      → 장점: 일반 메모리 사용
      → 단점: 대용량 Hotplug 시 커널 메모리 소모
    
    memmap_on_memory = 1 (ENABLE)
      → Hotplug 메모리 블록의 앞부분에 vmemmap 배치
      → 장점: 커널 메모리 절약
      → 단점: 실제 사용 가능한 메모리 감소
    
    memmap_on_memory = 2 (FORCE)
      → ENABLE와 동일하나 pageblock 정렬을 강제
      → 장점: 정렬 문제 해결
      → 단점: 추가 메모리 낭비 가능

    mhp_supports_memmap_on_memory() 함수가 다음 조건을 모두 만족할 때만 활성화됩니다:

    1. memmap_on_memory 파라미터가 활성화됨

    2. vmemmap 크기가 PAGE_SIZE로 정렬됨

    3. vmemmap 페이지가 pageblock_nr_pages로 정렬됨

    4. vmemmap 크기가 전체 메모리 블록과 같지 않음 (유효하지 않은 핫플러그)

    5. arch_supports_memmap_on_memory()가 아키텍처 수준에서 지원


    auto-movable 온라인 정책 상세

    Linux 7.0에서 메모리 온라인 시 ZONE_MOVABLE에 자동 배치하는 정책입니다.

    kernelcore=movablecore=는 부팅 단계에서 커널 메모리와 이동 가능한 메모리의 기준선을 먼저 잡습니다. 그 위에서 movable_nodeonline_policy="auto-movable"가 실제 hotplug 메모리의 배치 방향을 결정하고, auto_movable_ratioauto_movable_numa_aware가 세부 비율을 조정합니다.

    online_policy = "auto-movable"
      → MOVABLE : KERNEL 비율을 계산하여 ZONE_MOVABLE 할당 여부 결정
    
    비율 계산:
      auto_movable_ratio (기본 301%) 기준
      MOVABLE 페이지 수 ≤ (auto_movable_ratio × KERNEL_EARLY 페이지 수) / 100
    
     KERNEL_EARLY = 부팅 시 커널 영역에 있는 페이지
       → Hotplug로 추가된 커널 영역은 제외 (향후 제거 가능성이 있으므로)
       → 동적 메모리 그룹은 예외 ( DRIVER_MANAGED 메모리 )
    
    NUMA 인식:
      auto_movable_numa_aware = true (기본값)
      → 노드별 비율도 검사하여 특정 노드에 쏠림 방지

    관련 문서

  • 00-메모리 관리 개요 — 전체 메모리 관리 조감도
  • 04-Memblock — 부팅 초기 메모리 할당자
  • 11-Compaction — Memory Compaction (마이그레이션 전제 조건)
  • 13-NUMA — NUMA 아키텍처와 노드 관리
  • 18-CMA — Contiguous Memory Allocator
  • 20-Migration — 페이지 마이그레이션 상세
  • 24-티어링 — 메모리 티어링

  • SVG 다이어그램

    Hotplug 호출 흐름

    Hotplug 호출 흐름

    자료구조 관계도

    자료구조 관계도