관련 소스: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-티어링
메모리 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
메모리 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
};
하나의 메모리 디바이스(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;
};
};
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용 페이지 맵 (선택) */
};
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 정리 |
메모리 블록을 어떤 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 온라인 정책에서 MOVABLE:KERNEL 비율을 판단하기 위한 통계 구조입니다.
/* mm/memory_hotplug.c:789-792 */
struct auto_movable_stats {
unsigned long kernel_early_pages; /* 부팅 초기 커널 영역 페이지 수 */
unsigned long movable_pages; /* ZONE_MOVABLE 페이지 수 */
};
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()으로 롤백모든 페이지를 다른 블록으로 마이그레이션한 뒤, 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) → 시그널로 인한 중단 → -EINTRscan_movable_pages() 반환값 -EBUSY → 이동 불가 페이지 존재 → 실패물리 메모리 레인지가 커널에 등록되는 전체 과정을 담당합니다. 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, ¶ms);
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 → 인접 리소스와 병합모든 메모리 블록이 오프라인 상태인지 확인한 뒤, 리소스 해제, 메모리 블록 디바이스 제거, 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;
}
메모리 블록을 어떤 Zone에 온라인시킬지를 결정하는 정책 함수입니다. online_policy와 online_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);
}
드라이버/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()
드라이버/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에 온라인 | 불가 | 매우 어려움 |
| 모드 | 동작 | 메모리 낭비 | 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 |
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()가 아키텍처 수준에서 지원
Linux 7.0에서 메모리 온라인 시 ZONE_MOVABLE에 자동 배치하는 정책입니다.
kernelcore=와 movablecore=는 부팅 단계에서 커널 메모리와 이동 가능한 메모리의 기준선을 먼저 잡습니다. 그 위에서 movable_node와 online_policy="auto-movable"가 실제 hotplug 메모리의 배치 방향을 결정하고, auto_movable_ratio와 auto_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 (기본값)
→ 노드별 비율도 검사하여 특정 노드에 쏠림 방지