관련 소스:drivers/virtio/virtio_mem.c,include/uapi/linux/virtio_mem.h,mm/memory_hotplug.c
virtio-mem은 가상머신(VM)에서 호스트(하이퍼바이저)에 의해 동적으로 메모리를 추가하거나 제거하는 Virtio 드라이버입니다. 기존 virtio-balloon이 "페이지 단위로 메모리를 빌려주는" 방식인 반면, virtio-mem은 물리적 DIMM을 동적으로 크기 조절하는 것처럼 동작합니다. 마치 데스크톱 컴퓨터의 RAM 슬롯에 DIMM을 끼웠다 뺐다 하는 것과 비슷하지만, 이것이 소프트웨어적으로 일어납니다.
게스트 OS는 virtio-mem 드라이버를 통해 하이퍼바이저에게 "이 크기만큼 메모리를 더해줘" 또는 "이 크기만큼 줄여줘"라고 요청합니다. 하이퍼바이저는 요청을 처리하여 실제 물리 메모리를 할당하거나 해제합니다. 드라이버는 add_memory_driver_managed()를 사용하여 커널의 메모리 핫플러그 인프라와 연동하며, 메모리 블록 단위로 온라인/오프라인을 관리합니다.
virtio-mem은 두 가지 동작 모드를 지원합니다: SBM(Sub Block Mode)은 하나의 Linux 메모리 블록을 더 작은 서브블록으로 나누어 세밀하게 관리하고, BBM(Big Block Mode)은 큰 블록 단위로 관리합니다. 모드는 장치 블록 크기와 메모리 블록 크기에 따라 자동 선택됩니다. 새로 올라오는 페이지는 memory_hotplug.c의 온라인 콜백 경로를 거치므로, virtio-mem 영역이면 먼저 전용 fake-offline 판단을 받고 그 뒤 일반 온라인 경로로 이어집니다.
drivers/virtio/virtio_mem.c ← virtio-mem 드라이버 구현 (3156줄)
include/uapi/linux/virtio_mem.h ← virtio 프로토콜 정의
mm/memory_hotplug.c ← add_memory_driver_managed(), remove_memory()
# virtio-mem 모듈 상태 확인
lsmod | grep virtio_mem
# virtio 디바이스 목록에서 mem 디바이스 확인
lspci | grep -i virtio
# 메모리 핫플러그 상태
cat /sys/devices/system/memory/block_size_bytes
# 현재 온라인된 메모리 블록 수
ls /sys/devices/system/memory/ | grep memory | wc -l
# /proc/iomem에서 virtio_mem 리소스 확인
grep -i virtio_mem /proc/iomem
# 게스트 메모리 블록 상태 확인
for f in /sys/devices/system/memory/memory*/state; do echo "$f: $(cat $f)"; done
# dmesg에서 virtio-mem 관련 로그
dmesg | grep -i "virtio.*mem"
# 메모리 그룹 (memory group) 확인
cat /sys/kernel/debug/memory_groups 2>/dev/null || echo "CONFIG_DEBUG_MEMORY_GROUPS 미활성화"
# 오프라인된 메모리 블록 수 확인
cat /sys/devices/system/memory/removable
# ZONE_MOVABLE 메모리 크기 확인
grep -i movable /proc/zoneinfo | head -5
# 메모리 블록 크기
cat /sys/devices/system/memory/block_size_bytes
# 자동 온라인 블록 정책
cat /sys/devices/system/memory/auto_online_blocks 2>/dev/null
# memory_hotplug 모듈 파라미터
cat /sys/module/memory_hotplug/parameters/online_policy 2>/dev/null
cat /sys/module/memory_hotplug/parameters/memmap_on_memory 2>/dev/null
# 메모리 압력과 회수 상태
cat /proc/pressure/memory
cat /proc/buddyinfo
cat /proc/vmstat | grep -E 'allocstall|pgscan|pgsteal|pgfault|pgmajfault'
# 물리 주소 배치와 NUMA 배치
cat /proc/iomem | grep -Ei 'virtio|System RAM'
lsmem 2>/dev/null
numactl -H 2>/dev/null
drivers/virtio/virtio_mem.c:102 — virtio-mem 드라이버의 메인 구조체
struct virtio_mem {
struct virtio_device *vdev; // Virtio 디바이스 포인터
bool unplug_all_required; // 시작 시 모든 메모리 언플러그 필요 여부
struct work_struct wq; // plug/unplug 요청 처리 워크큐
atomic_t wq_active; // 워크큐 활성 상태
struct virtqueue *vq; // 게스트→호스트 virtqueue
wait_queue_head_t host_resp; // 호스트 응답 대기 큐
uint64_t plugged_size; // 현재 플러그된 메모리 크기
uint64_t requested_size; // 호스트가 요청한 목표 크기
uint64_t device_block_size; // 장치 블록 크기 (호스트와의 통신 단위)
int nid; // NUMA 노드 ID
uint64_t addr; // 물리 시작 주소
uint64_t region_size; // 최대 리전 크기
uint64_t usable_region_size; // 사용 가능한 리전 크기
struct resource *parent_resource; // 부모 리소스 (드라이버 관리 메모리)
int mgid; // 메모리 그룹 ID
atomic64_t offline_size; // 오프라인된 메모리 크기
uint64_t offline_threshold; // 오프라인 임계값 (기본 1GB)
bool in_sbm; // SBM 모드 여부 (false면 BBM)
union {
struct { /* SBM 전용 필드 */ } sbm;
struct { /* BBM 전용 필드 */ } bbm;
};
struct mutex hotplug_mutex; // 핫플러그 동기화 뮤텍스
struct hrtimer retry_timer; // 재시도 타이머 (50초~300초)
struct notifier_block memory_notifier; // 메모리 온라인/오프라인 알림
struct notifier_block pm_notifier; // 전원 관리 알림
};
offline_threshold: 오프라인된 메모리가 이 임계값을 초과하면 더 이상 메모리를 추가하지 않습니다. OOM을 방지하기 위한 장치입니다.in_sbm: sb_size < memory_block_size_bytes()이고 force_bbm이 false이면 SBM 모드가 선택됩니다.drivers/virtio/virtio_mem.c:67 — SBM에서 메모리 블록의 상태
enum virtio_mem_sbm_mb_state {
VIRTIO_MEM_SBM_MB_UNUSED = 0, // 미사용, 재사용 가능
VIRTIO_MEM_SBM_MB_PLUGGED, // 플러그됨, Linux에 미추가
VIRTIO_MEM_SBM_MB_OFFLINE, // 완전 플러그, Linux 추가, 오프라인
VIRTIO_MEM_SBM_MB_OFFLINE_PARTIAL, // 부분 플러그, Linux 추가, 오프라인
VIRTIO_MEM_SBM_MB_KERNEL, // 완전 플러그, 커널 존 온라인
VIRTIO_MEM_SBM_MB_KERNEL_PARTIAL, // 부분 플러그, 커널 존 온라인
VIRTIO_MEM_SBM_MB_MOVABLE, // 완전 플러그, ZONE_MOVABLE 온라인
VIRTIO_MEM_SBM_MB_MOVABLE_PARTIAL, // 부분 플러그, ZONE_MOVABLE 온라인
VIRTIO_MEM_SBM_MB_COUNT
};
_PARTIAL 상태는 일부 서브블록만 플러그된 상태를 나타냅니다.drivers/virtio/virtio_mem.c:90 — BBM에서 빅블록의 상태
enum virtio_mem_bbm_bb_state {
VIRTIO_MEM_BBM_BB_UNUSED = 0, // 미사용
VIRTIO_MEM_BBM_BB_PLUGGED, // 플러그됨, Linux에 미추가
VIRTIO_MEM_BBM_BB_ADDED, // 플러그됨, Linux에 추가됨
VIRTIO_MEM_BBM_BB_FAKE_OFFLINE, // 모든 온라인 메모리가 fake-offline
VIRTIO_MEM_BBM_BB_COUNT
};
include/uapi/linux/virtio_mem.h:191 — 호스트와 공유하는 설정 구조체
struct virtio_mem_config {
__le64 block_size; // 블록 크기 (변경 불가)
__le16 node_id; // ACPI PXM 노드 ID
__le64 addr; // 시작 주소 (변경 불가)
__le64 region_size; // 최대 리전 크기 (변경 불가)
__le64 usable_region_size; // 현재 사용 가능 크기 (동적 확장/축소)
__le64 plugged_size; // 현재 플러그된 크기
__le64 requested_size; // 요청된 목표 크기
};
usable_region_size: region_size까지 동적으로 커질 수 있으며, UNPLUG_ALL 요청 시 축소됩니다.include/uapi/linux/virtio_mem.h:126,180 — 게스트-호스트 통신 메시지
struct virtio_mem_req_plug {
__virtio64 addr;
__virtio16 nb_blocks;
__virtio16 padding[3];
};
struct virtio_mem_req_unplug {
__virtio64 addr;
__virtio16 nb_blocks;
__virtio16 padding[3];
};
struct virtio_mem_req_state {
__virtio64 addr;
__virtio16 nb_blocks;
__virtio16 padding[3];
};
struct virtio_mem_req {
__virtio16 type;
__virtio16 padding[3];
union {
struct virtio_mem_req_plug plug;
struct virtio_mem_req_unplug unplug;
struct virtio_mem_req_state state;
} u;
};
struct virtio_mem_resp_state {
__virtio16 state;
};
struct virtio_mem_resp {
__virtio16 type;
__virtio16 padding[3];
union {
struct virtio_mem_resp_state state;
} u;
};
VIRTIO_MEM_RESP_NACK: 플러그 요청이 거부됨 (요청 크기 초과 등)VIRTIO_MEM_RESP_BUSY: 장치가 지금 처리 불가 (재시도 필요)drivers/virtio/virtio_mem.c:2935 — 드라이버 프로브 함수
static int virtio_mem_probe(struct virtio_device *vdev)
{
struct virtio_mem *vm;
int rc;
BUILD_BUG_ON(sizeof(struct virtio_mem_req) != 24);
BUILD_BUG_ON(sizeof(struct virtio_mem_resp) != 10);
vdev->priv = vm = kzalloc_obj(*vm);
if (!vm)
return -ENOMEM;
init_waitqueue_head(&vm->host_resp);
vm->vdev = vdev;
INIT_WORK(&vm->wq, virtio_mem_run_wq);
mutex_init(&vm->hotplug_mutex);
INIT_LIST_HEAD(&vm->next);
spin_lock_init(&vm->removal_lock);
hrtimer_setup(&vm->retry_timer, virtio_mem_timer_expired, CLOCK_MONOTONIC,
HRTIMER_MODE_REL);
vm->retry_timer_ms = VIRTIO_MEM_RETRY_TIMER_MIN_MS;
vm->in_kdump = is_kdump_kernel();
/* virtqueue 등록 */
rc = virtio_mem_init_vq(vm);
if (rc)
goto out_free_vm;
/* 디바이스 설정을 읽고 초기화 */
rc = virtio_mem_init(vm);
if (rc)
goto out_del_vq;
/* kdump가 아니면 config update를 걸어 requested_size를 처리 */
if (!vm->in_kdump) {
atomic_set(&vm->config_changed, 1);
queue_work(system_freezable_wq, &vm->wq);
}
return 0;
out_del_vq:
vdev->config->del_vqs(vdev);
out_free_vm:
kfree(vm);
vdev->priv = NULL;
return rc;
}
virtio_mem_init_kdump()이 /proc/vmcore 처리를 담당하고, 실제 (un)plug는 하지 않습니다.drivers/virtio/virtio_mem.c:2415 — 워크큐 메인 처리 함수
static void virtio_mem_run_wq(struct work_struct *work)
{
struct virtio_mem *vm = container_of(work, struct virtio_mem, wq);
uint64_t diff;
int rc;
if (unlikely(vm->in_kdump)) {
dev_warn_once(&vm->vdev->dev,
"unexpected workqueue run in kdump kernel\n");
return;
}
hrtimer_cancel(&vm->retry_timer);
if (vm->broken)
return;
atomic_set(&vm->wq_active, 1);
retry:
rc = 0;
/* 남아 있는 상태가 있으면 먼저 정리한다. */
if (unlikely(vm->unplug_all_required))
rc = virtio_mem_send_unplug_all_request(vm);
if (atomic_read(&vm->config_changed)) {
atomic_set(&vm->config_changed, 0);
virtio_mem_refresh_config(vm);
}
/* 이전 실행에서 남은 블록을 정리한다. */
if (!rc)
rc = virtio_mem_cleanup_pending_mb(vm);
if (!rc && vm->requested_size != vm->plugged_size) {
if (vm->requested_size > vm->plugged_size) {
diff = vm->requested_size - vm->plugged_size;
rc = virtio_mem_plug_request(vm, diff);
} else {
diff = vm->plugged_size - vm->requested_size;
rc = virtio_mem_unplug_request(vm, diff);
}
}
/* 이미 완전히 unplug된 Linux 메모리 블록은 계속 제거를 시도한다. */
if (!rc && vm->in_sbm && vm->sbm.have_unplugged_mb)
rc = -EBUSY;
switch (rc) {
case 0:
vm->retry_timer_ms = VIRTIO_MEM_RETRY_TIMER_MIN_MS;
break;
case -ENOSPC:
/* 주소 정렬이나 물리 한계 때문에 더 이상 추가할 수 없다. */
break;
case -ETXTBSY:
/* 하이퍼바이저가 잠시 처리할 수 없다. */
case -EBUSY:
/* unplug에 쓸 수 있는 메모리를 지금은 비울 수 없다. */
case -ENOMEM:
/* 메모리가 부족하니 나중에 다시 시도한다. */
hrtimer_start(&vm->retry_timer, ms_to_ktime(vm->retry_timer_ms),
HRTIMER_MODE_REL);
break;
case -EAGAIN:
/* 설정이 바뀌었으니 즉시 다시 시도한다. */
goto retry;
default:
/* 알 수 없는 오류는 장치를 broken으로 표시한다. */
dev_err(&vm->vdev->dev,
"unknown error, marking device broken: %d\n", rc);
vm->broken = true;
}
atomic_set(&vm->wq_active, 0);
}
drivers/virtio/virtio_mem.c:1922,2306 — SBM/BBM별 플러그/언플러그 요청 분기
static int virtio_mem_plug_request(struct virtio_mem *vm, uint64_t diff)
{
if (vm->in_sbm)
return virtio_mem_sbm_plug_request(vm, diff); // 서브블록 단위
return virtio_mem_bbm_plug_request(vm, diff); // 빅블록 단위
}
SBM 언플러그 우선순위:
1. OFFLINE_PARTIAL → 2. OFFLINE → 3. MOVABLE_PARTIAL → 4. KERNEL_PARTIAL → 5. MOVABLE → 6. KERNEL
BBM 언플러그 우선순위:
1. 완전 오프라인 블록 → 2. ZONE_MOVABLE 블록 → 3. 나머지
virtio_mem_init_hotplug()는 mhp_get_pluggable_range(true)로 실제로 주소를 쓸 수 있는 범위를 먼저 자르고, sb_size / bb_size를 계산한 뒤 register_memory_notifier(), register_pm_notifier(), register_virtio_mem_device()를 차례로 붙입니다. 이 단계가 끝나면 memory_hotplug.c의 온라인 콜백 경로가 virtio_mem_online_page_cb()를 거치게 되고, virtio-mem 범위는 virtio_mem_online_page()에서 fake-offline 여부를 다시 판단합니다.
drivers/virtio/virtio_mem.c:1358 — 메모리 온라인 시 호출되는 콜백
static void virtio_mem_online_page_cb(struct page *page, unsigned int order)
{
const unsigned long addr = page_to_phys(page);
struct virtio_mem *vm;
rcu_read_lock();
list_for_each_entry_rcu(vm, &virtio_mem_devices, next) {
/* onlining 되는 페이지는 memory block 경계를 넘지 않는다. */
if (!virtio_mem_contains_range(vm, addr, PFN_PHYS(1 << order)))
continue;
/* virtio_mem_set_fake_offline()는 sleep할 수 있다. */
rcu_read_unlock();
virtio_mem_online_page(vm, page, order);
return;
}
rcu_read_unlock();
/* virtio-mem 메모리가 아니면 일반 온라인 경로로 넘긴다. */
generic_online_page(page, order);
}
virtio_mem_init_hotplug()는 mhp_get_pluggable_range(true)로 실제로 주소를 쓸 수 있는 범위를 먼저 자르고, register_memory_notifier()와 register_virtio_mem_device()를 연결한 뒤 set_online_page_callback()가 잡은 virtio_mem_online_page_cb()가 페이지 온라인 경로를 가로채게 만듭니다. 그래서 새 페이지는 먼저 virtio-mem 범위인지 확인되고, 범위 안이면 virtio_mem_online_page()를 거쳐 fake-offline 여부를 판단하며, 범위 밖이면 generic_online_page()로 넘어갑니다.
호스트: requested_size 변경
→ virtio_mem_config_changed()
→ workqueue 시작
→ virtio_mem_run_wq()
→ virtio_mem_refresh_config() // usable_region_size, requested_size 갱신
→ virtio_mem_plug_request()
[SBM] → virtio_mem_sbm_plug_request()
→ virtio_mem_sbm_plug_any_sb() // 기존 블록에 서브블록 추가
→ virtio_mem_sbm_plug_and_add_mb() // 새 블록 생성 및 추가
→ virtio_mem_sbm_plug_sb() → virtio_mem_send_plug_request() → virtqueue
→ virtio_mem_sbm_add_mb() → add_memory_driver_managed()
[BBM] → virtio_mem_bbm_plug_request()
→ virtio_mem_bbm_plug_and_add_bb()
→ virtio_mem_bbm_plug_bb() → virtqueue
→ virtio_mem_bbm_add_bb() → add_memory_driver_managed()
호스트: requested_size 변경
→ virtio_mem_run_wq()
→ virtio_mem_unplug_request()
[SBM] → virtio_mem_sbm_unplug_request()
→ virtio_mem_sbm_unplug_any_sb()
→ virtio_mem_sbm_unplug_any_sb_online() // 온라인 블록
→ virtio_mem_fake_offline() // alloc_contig_range()로 할당 해제
→ virtio_mem_sbm_unplug_sb() → virtqueue
→ virtio_mem_sbm_unplug_any_sb_offline() // 오프라인 블록
→ virtio_mem_sbm_unplug_mb() → virtqueue
→ virtio_mem_sbm_remove_mb() → remove_memory()
[BBM] → virtio_mem_bbm_unplug_request()
→ virtio_mem_bbm_offline_remove_and_unplug_bb()
→ virtio_mem_fake_offline() (전체 BB 대상)
→ virtio_mem_bbm_offline_and_remove_bb() → offline_and_remove_memory()
→ virtio_mem_bbm_unplug_bb() → virtqueue
메모리 온라인 (sysfs 또는 자동)
→ virtio_mem_online_page_cb()
→ virtio_mem_online_page()
[SBM] → 서브블록 플러그 상태 확인
→ plugged → generic_online_page()
→ unplugged → virtio_mem_set_fake_offline()
[BBM] → BB 상태 확인
→ FAKE_OFFLINE이면 → virtio_mem_set_fake_offline()
→ 아니면 → generic_online_page()
| 항목 | virtio-mem | virtio-balloon |
|---|---|---|
| 동작 원리 | DIMM 동적 크기 조절 (핫플러그) | 페이지 할당/해제 (inflation) |
| granularity | 메모리 블록/서브블록 | 4KB 페이지 |
| 메모리 관리 | 커널 핫플러그 인프라 사용 | balloon_dev_info 목록 관리 |
| NUMA 인식 | NUMA 노드별 장치 할당 | NUMA 인식 제한적 |
| 오프라인 | fake-offline → 제거 | 페이지를 호스트에 반환 |
| 영구 메모리 | 불가 (언플러그 시 제거) | 불가 |
| kdump 지원 | vmcore 처리 지원 | 기본 지원 |
| Free Page Reporting | 불가 (별도 메커니즘) | virtio-balloon VQ로 지원 |
| 복잡도 | 높음 (SBM/BBM, 상태 머신) | 상대적으로 낮음 |
| 항목 | SBM (Sub Block Mode) | BBM (Big Block Mode) |
|---|---|---|
| granularity | 서브블록 (pageblock 크기) | 빅블록 (>= 메모리 블록) |
| 선택 조건 | sb_size < memory_block_size | sb_size >= memory_block_size 또는 force_bbm |
| 세밀도 | 높음 (메모리 블록 내 부분 할당) | 낮음 (블록 전체) |
| 상태 추적 | mb_states + sb_states 비트맵 | bb_states 바이트 배열 |
| 오프라인 | 서브블록별 fake-offline | 섹션 단위 fake-offline |
| 메모리 제거 | 서브블록 전부 언플러그 후 블록 제거 | 빅블록 전체 오프라인 후 제거 |
| 메모리 그룹 | 메모리 블록 단위 | 빅블록 단위 |
| 플래그 | 값 | 설명 |
|---|---|---|
| VIRTIO_MEM_F_ACPI_PXM | 0 | node_id가 ACPI PXM으로 유효 |
| VIRTIO_MEM_F_UNPLUGGED_INACCESSIBLE | 1 | 언플러그된 메모리 접근 불가 |
| VIRTIO_MEM_F_PERSISTENT_SUSPEND | 2 | 서스펜트+.Resume 시 플러그 유지 |
| 항목 | virtio-mem에서 보는 값 | 확인 명령 | ||
|---|---|---|---|---|
| 메모리 블록 크기 | 온라인/오프라인의 기준 단위 | `cat /sys/devices/system/memory/block_size_bytes` | ||
| 자동 온라인 | 새 블록이 자동으로 온라인되는지 | `cat /sys/devices/system/memory/auto_online_blocks 2>/dev/null` | ||
| 온라인 정책 | `contig-zones` / `auto-movable` | `cat /sys/module/memory_hotplug/parameters/online_policy 2>/dev/null` | ||
| memmap 배치 | `struct page`를 메모리 위에 둘지 여부 | `cat /sys/module/memory_hotplug/parameters/memmap_on_memory 2>/dev/null` | ||
| 플러그 가능 범위 | 장치가 실제로 쓸 수 있는 물리 범위 | `cat /proc/iomem | grep -Ei 'virtio | System RAM'` |
virtio-mem은 메모리를 실제로 제거하기 전에 "fake-offline" 과정을 거칩니다:
1. virtio_mem_fake_offline(): alloc_contig_range()로 물리 페이지를 연속 할당
2. virtio_mem_set_fake_offline(): 페이지에 PG_offline 플래그 설정
3. adjust_managed_page_count(): 관리 페이지 수 감소
4. virtqueue로 언플러그 요청 전송
5. 응답 ACK 시 virtio_mem_fake_online()로 페이지 복원 또는 완전 제거
// drivers/virtio/virtio_mem.c:154
#define VIRTIO_MEM_DEFAULT_OFFLINE_THRESHOLD (1024 * 1024 * 1024) // 1GB
// 오프라인된 메모리가 임계값을 초과하면 메모리 추가 중단
static bool virtio_mem_could_add_memory(struct virtio_mem *vm, uint64_t size)
{
return atomic64_read(&vm->offline_size) + size <= vm->offline_threshold;
}