# Free Page Reporting
Free Page Reporting는 가상화 환경에서 호스트(서버)가 게스트 VM의 빈 페이지를 인지하고 회수할 수 있게 해주는 메커니즘입니다. 게스트 커널이 "더 이상 사용하지 않는 페이지"를 호스트에 알리면, 호스트는 해당 페이지를 물리 메모리에서 제거하여 다른 VM이나 호스트 자체에 재할당할 수 있습니다. 이를 통해 과적(overcommit) 환경에서 실제 물리 메모리 사용량을 줄이고, 메모리 효율성을 높입니다.
이 서브시스템은 Virtio Balloon 드라이버와 Hyper-V Balloon 드라이버가 사용합니다. 게스트 커널 측에서는 page_reporting.c가 Buddy Allocator의 free list를 순회하며 보고되지 않은(reported) 페이지를 수집하고, scatterlist를 통해 드라이버 콜백(report())으로 전달합니다. 드라이버는 해당 페이지를 호스트에 알리고, 알림이 완료된 후 페이지는 Buddy Allocator로 반환됩니다. CONFIG_PAGE_REPORTING 커널 옵션으로 활성화되며, page_reporting_order 모듈 파라미터로 보고할 페이지의 최소 order를 제어할 수 있습니다.
일상적으로 보면, 커널이 "아직 비어 있는 책"에 반납 예정 표식을 붙여 묶어 두고, 일정 시간 동안 모은 뒤 한꺼번에 창고 담당자에게 넘기는 작업에 가깝습니다. 그래서 이 기능은 단순히 free list를 훑는 경로가 아니라, 보고되지 않은 페이지를 모아 배치(batch)로 처리하는 지연 파이프라인으로 이해하면 좋습니다.
소스 파일:
mm/page_reporting.c
mm/page_reporting.h (내부 헤더)
include/linux/page_reporting.h (공개 API)
include/linux/page-flags.h (PageReported 플래그)
drivers/virtio/virtio_balloon.c (Virtio 소비자)
drivers/hv/hv_balloon.c (Hyper-V 소비자)
# page_reporting 모듈 파라미터 확인
cat /sys/module/page_reporting/parameters/page_reporting_order 2>/dev/null || echo "모듈 미로드 또는 비활성화"
# 커널 컨피그 확인
grep CONFIG_PAGE_REPORTING /boot/config-$(uname -r) 2>/dev/null || zcat /proc/config.gz 2>/dev/null | grep CONFIG_PAGE_REPORTING
# Virtio Balloon 드라이버 로드 상태 확인
lsmod | grep virtio_balloon
# Hyper-V Balloon 드라이버 로드 상태 확인
lsmod | grep hv_balloon
# 동적 키 상태 확인 (page_reporting 활성화 여부)
cat /sys/kernel/debug/dynamic_debug/control 2>/dev/null | grep page_reporting || echo "dynamic debug 미지원"
# Buddy Allocator free list 상태 (zone별)
cat /proc/buddyinfo
# 메모리 사용 현황
free -h
# 메모리 핵심 지표
grep -E 'MemTotal|MemFree|MemAvailable|AnonPages|Cached' /proc/meminfo
# 페이지 폴트와 회수 추세
grep -E 'pgfault|pgmajfault|pgscan|pgsteal' /proc/vmstat
# 대표 슬랩 캐시 확인
grep -E '^(kmalloc-|dentry|inode_cache|task_struct)' /proc/slabinfo
# Balloon 통계 확인 (Virtio)
cat /proc/vmstat | grep -i balloon
# 메모리 압력 확인 (PSI)
cat /proc/pressure/memory
드라이버가 page_reporting 서브시스템에 등록할 때 사용하는 장치 정보 구조체입니다.
/* include/linux/page_reporting.h:11-24 */
struct page_reporting_dev_info {
/* 페이지를 "reported" 상태로 만드는 드라이버 콜백 함수 */
int (*report)(struct page_reporting_dev_info *prdev,
struct scatterlist *sg, unsigned int nents);
/* 지연된 작업(delayed work) 구조체 — 비동기 보고 처리 */
struct delayed_work work;
/* 현재 보고 상태: IDLE(0), REQUESTED(1), ACTIVE(2) */
atomic_t state;
/* 보고할 페이지의 최소 order */
unsigned int order;
};
page_reporting.c 내부에서 사용하는 상태 값입니다.
/* mm/page_reporting.c:53-57 */
enum {
PAGE_REPORTING_IDLE = 0, /* 유휴 상태 — 보고 작업 없음 */
PAGE_REPORTING_REQUESTED, /* 보고 요청됨 — 워크 큐 예약 대기 */
PAGE_REPORTING_ACTIVE /* 보고 진행 중 — 실제 처리 중 */
};
scatterlist의 크기 — 한 번에 드라이버에 전달할 최대 페이지 수입니다.
/* include/linux/page_reporting.h:9 */
#define PAGE_REPORTING_CAPACITY 32
Buddy Allocator에서 이미 보고된 페이지를 추적하는 페이지 플래그입니다.
/* include/linux/page-flags.h:682-688 */
/*
* PageReported()는 Buddy Allocator 내부에서 보고된 빈 페이지를 추적합니다.
* zone lock으로 보호해야 비트 설정/해제 경쟁을 막을 수 있습니다.
*/
__PAGEFLAG(Reported, reported, PF_NO_COMPOUND)
__free_one_page()에서 호출되는 인라인 함수로, free 페이지가 발생할 때 page_reporting에 알립니다.
/* mm/page_reporting.h:33-45 */
static inline void page_reporting_notify_free(unsigned int order)
{
/* __free_one_page()의 핫 경로에서 호출됨 */
if (!static_branch_unlikely(&page_reporting_enabled))
return;
/* page_reporting_order 미만이면 무시 */
if (order < page_reporting_order)
return;
/* __page_reporting_notify() 호출 (약간의 오버헤드 있음) */
__page_reporting_notify();
}
RCU로 등록된 장치를 찾아 보고 요청을 시작합니다.
/* mm/page_reporting.c:87-102 */
void __page_reporting_notify(void)
{
struct page_reporting_dev_info *prdev;
/* RCU로 pr_dev_info를 보호한다. */
rcu_read_lock();
prdev = rcu_dereference(pr_dev_info);
if (likely(prdev))
__page_reporting_request(prdev);
rcu_read_unlock();
}
pr_dev_info의 원자적 상태를 확인하고, 유휴 상태이면 지연된 워크를 예약합니다.
/* mm/page_reporting.c:60-84 */
static void
__page_reporting_request(struct page_reporting_dev_info *prdev)
{
unsigned int state;
/* 이미 요청 상태이면 무시 */
state = atomic_read(&prdev->state);
if (state == PAGE_REPORTING_REQUESTED)
return;
/* 이미 활성 상태(IDLE이 아닌 경우)이면 무시 */
state = atomic_xchg(&prdev->state, PAGE_REPORTING_REQUESTED);
if (state != PAGE_REPORTING_IDLE)
return;
/* 2초 지연 후 워크 실행 — 큐가 충분히 쌓이도록 대기 */
schedule_delayed_work(&prdev->work, PAGE_REPORTING_DELAY);
}
분기 로직:
state == REQUESTED → 이미 예약됨, 즉시 반환state != IDLE (ACTIVE) → 보고 진행 중, 추가 예약 불필요state == IDLE → REQUESTED로 변경 후 2초 지연 워크 예약특정 zone의 특정 order/migratetype free list에서 보고되지 않은 페이지를 수집하고 드라이버에 전달하는 핵심 사이클 함수입니다.
/* mm/page_reporting.c:145-257 */
static int
page_reporting_cycle(struct page_reporting_dev_info *prdev, struct zone *zone,
unsigned int order, unsigned int mt,
struct scatterlist *sgl, unsigned int *offset)
{
struct free_area *area = &zone->free_area[order];
struct list_head *list = &area->free_list[mt];
unsigned int page_len = PAGE_SIZE << order;
struct page *page, *next;
long budget;
int err = 0;
/* free area가 비어 있으면 처리할 것이 없으므로 이 free_list는 건너뛴다. */
if (list_empty(list))
return err;
spin_lock_irq(&zone->lock);
/* 예산 계산: free 페이지의 1/16 + 여유분 */
budget = DIV_ROUND_UP(area->nr_free, PAGE_REPORTING_CAPACITY * 16);
/* 보고되지 않은 페이지를 SG list에 추가하며 free list를 순회한다. */
list_for_each_entry_safe(page, next, list, lru) {
/* 이미 보고된 페이지는 건너뛴다. */
if (PageReported(page))
continue;
/* 예산을 모두 소진하면 추가 처리가 필요하다는 상태로 바꾸고 빠져나간다. */
if (budget < 0) {
atomic_set(&prdev->state, PAGE_REPORTING_REQUESTED);
next = page;
break;
}
/* SG list에 넣을 공간이 있으면 페이지를 격리해서 담는다. */
if (*offset) {
if (!__isolate_free_page(page, order)) {
next = page;
break;
}
/* SG list에 페이지를 추가한다. */
--(*offset);
sg_set_page(&sgl[*offset], page, page_len, 0);
continue;
}
/* 보고되지 않은 첫 페이지를 free list의 머리로 옮긴 뒤 zone lock을 해제한다. */
if (!list_is_first(&page->lru, list))
list_rotate_to_front(&page->lru, list);
/* 보고 처리를 기다리기 전에 lock을 해제한다. */
spin_unlock_irq(&zone->lock);
/* 로컬 리스트에 있는 페이지들을 처리하기 시작한다. */
err = prdev->report(prdev, sgl, PAGE_REPORTING_CAPACITY);
/* 전체 리스트를 보고했으므로 offset을 재설정한다. */
*offset = PAGE_REPORTING_CAPACITY;
/* report 함수 호출을 반영해 budget을 갱신한다. */
budget--;
/* zone lock을 다시 잡고 처리를 이어간다. */
spin_lock_irq(&zone->lock);
/* SG list에서 보고된 페이지를 비운다. */
page_reporting_drain(prdev, sgl, PAGE_REPORTING_CAPACITY, !err);
/* lock을 놓는 동안 이전 next는 유효하지 않으므로 next를 free list의 첫 엔트리로 다시 맞춘다. */
next = list_first_entry(list, struct page, lru);
/* 에러가 있으면 중단한다. */
if (err)
break;
}
/* 남은 페이지가 있으면 free list의 머리로 돌린다. */
if (!list_entry_is_head(next, list, lru) && !list_is_first(&next->lru, list))
list_rotate_to_front(&next->lru, list);
spin_unlock_irq(&zone->lock);
return err;
}
분기 로직:
PageReported(page) → 이미 보고됨, 건너뜀budget < 0 → 해당 free list 처리 예산 초과, 상태를 REQUESTED로 복귀*offset > 0 → SG list에 공간 있음, __isolate_free_page()로 페이지 격리 후 SG list에 추가*offset == 0 → SG list 가득 참, lock 해제 후 report() 콜백 호출단일 zone을 대상으로 모든 order/migratetype 조합에 대해 보고 사이클을 실행합니다.
/* mm/page_reporting.c:259-305 */
static int
page_reporting_process_zone(struct page_reporting_dev_info *prdev,
struct scatterlist *sgl, struct zone *zone)
{
unsigned int order, mt, leftover, offset = PAGE_REPORTING_CAPACITY;
unsigned long watermark;
int err = 0;
/* 보고가 진행될 수 있도록 최소 watermark를 계산한다. */
watermark = low_wmark_pages(zone) +
(PAGE_REPORTING_CAPACITY << page_reporting_order);
/* 자유 페이지가 충분하지 않으면 요청을 취소한다. */
if (!zone_watermark_ok(zone, 0, watermark, 0, ALLOC_CMA))
return err;
/* 모든 order/migratetype 조합을 순회한다. */
for (order = page_reporting_order; order < NR_PAGE_ORDERS; order++) {
for (mt = 0; mt < MIGRATE_TYPES; mt++) {
/* isolate free list는 건너뛴다. */
if (is_migrate_isolate(mt))
continue;
err = page_reporting_cycle(prdev, zone, order, mt,
sgl, &offset);
if (err)
return err;
}
}
/* 남은 페이지를 보고한다. */
leftover = PAGE_REPORTING_CAPACITY - offset;
if (leftover) {
sgl = &sgl[offset];
err = prdev->report(prdev, sgl, leftover);
/* 남은 페이지를 모두 보고한 뒤 free list로 돌려보낸다. */
spin_lock_irq(&zone->lock);
page_reporting_drain(prdev, sgl, leftover, !err);
spin_unlock_irq(&zone->lock);
}
return err;
}
지연 워크 핸들러 — 모든 zone을 순회하며 보고 작업을 수행합니다.
/* mm/page_reporting.c:307-347 */
static void page_reporting_process(struct work_struct *work)
{
struct delayed_work *d_work = to_delayed_work(work);
struct page_reporting_dev_info *prdev =
container_of(d_work, struct page_reporting_dev_info, work);
int err = 0, state = PAGE_REPORTING_ACTIVE;
/* 상태를 ACTIVE로 변경 — 보고 진행 중임을 표시 */
atomic_set(&prdev->state, state);
/* SG list 할당 */
sgl = kmalloc_objs(*sgl, PAGE_REPORTING_CAPACITY);
if (!sgl)
goto err_out;
sg_init_table(sgl, PAGE_REPORTING_CAPACITY);
/* 모든 zone 순회 */
for_each_zone(zone) {
err = page_reporting_process_zone(prdev, sgl, zone);
if (err)
break;
}
kfree(sgl);
err_out:
/* IDLE로 복귀 시도 — 그 사이 새 요청이 들어왔으면 REQUESTED 유지 */
state = atomic_cmpxchg(&prdev->state, state, PAGE_REPORTING_IDLE);
if (state == PAGE_REPORTING_REQUESTED)
schedule_delayed_work(&prdev->work, PAGE_REPORTING_DELAY);
}
분기 로직:
atomic_cmpxchg로 IDLE 복귀 시도드라이버의 등록/해제 함수입니다.
/* mm/page_reporting.c:352-417 (핵심) */
int page_reporting_register(struct page_reporting_dev_info *prdev)
{
mutex_lock(&page_reporting_mutex);
/* 이미 등록된 장치가 있으면 -EBUSY */
if (rcu_dereference_protected(pr_dev_info,
lockdep_is_held(&page_reporting_mutex))) {
err = -EBUSY;
goto err_out;
}
/* order 결정: 파라미터 → 드라이버 → pageblock_order 순 */
if (page_reporting_order == -1) {
if (prdev->order > 0 && prdev->order <= MAX_PAGE_ORDER)
page_reporting_order = prdev->order;
else
page_reporting_order = pageblock_order;
}
/* 상태 및 워크 초기화 */
atomic_set(&prdev->state, PAGE_REPORTING_IDLE);
INIT_DELAYED_WORK(&prdev->work, &page_reporting_process);
/* 초기 보고 요청 */
__page_reporting_request(prdev);
/* RCU로 전역 포인터 할당 */
rcu_assign_pointer(pr_dev_info, prdev);
/* static key 활성화 — 핫 경로 체크 활성화 */
if (!static_key_enabled(&page_reporting_enabled)) {
static_branch_enable(&page_reporting_enabled);
pr_info("Free page reporting enabled\n");
}
mutex_unlock(&page_reporting_mutex);
return err;
}
void page_reporting_unregister(struct page_reporting_dev_info *prdev)
{
mutex_lock(&page_reporting_mutex);
if (prdev == rcu_dereference_protected(pr_dev_info,
lockdep_is_held(&page_reporting_mutex))) {
/* RCU 해제 후 동기화 */
RCU_INIT_POINTER(pr_dev_info, NULL);
synchronize_rcu();
/* 지연된 작업 취소 및 동기 대기 */
cancel_delayed_work_sync(&prdev->work);
}
mutex_unlock(&page_reporting_mutex);
}
Buddy: __free_one_page()
└─ page_reporting_notify_free(order) [mm/page_reporting.h:33]
└─ __page_reporting_notify() [mm/page_reporting.c:87]
└─ __page_reporting_request(prdev) [mm/page_reporting.c:60]
└─ schedule_delayed_work() [2초 지연 예약]
└─ page_reporting_process() [mm/page_reporting.c:307]
└─ page_reporting_process_zone() [mm/page_reporting.c:259]
├─ zone_watermark_ok() 체크
└─ page_reporting_cycle() [mm/page_reporting.c:145]
├─ __isolate_free_page() → SG list 수집
├─ prdev->report() → 드라이버 콜백
└─ page_reporting_drain() → free list 반환
| 항목 | Virtio Balloon | Hyper-V Balloon |
|---|---|---|
| 소스 파일 | `drivers/virtio/virtio_balloon.c` | `drivers/hv/hv_balloon.c` |
| 콜백 함수 | `virtballoon_free_page_report()` | `hv_free_page_report()` |
| 등록 시 order | ARM64_64K: 5, 기본: 미설정 | 0 (파라미터 사용) |
| 동작 | 페이지를 VIRTIO_BALLOON_S_GUEST_MEM 핀으로 알림 | Hyper-V Hypercall로 GPA 범위 전송 |
| 최대 용량 | `PAGE_REPORTING_CAPACITY` (32) | `HV_MEMORY_HINT_MAX_GPA_PAGE_RANGES` |
| 현재 상태 | 이벤트 | 다음 상태 | 동작 |
|---|---|---|---|
| IDLE | `__page_reporting_request()` 호출 | REQUESTED | 2초 지연 워크 예약 |
| REQUESTED | `__page_reporting_request()` 호출 | REQUESTED (유지) | 중복 예약 방지 |
| ACTIVE | 보고 사이클 완료 + 요청 없음 | IDLE | 워크 큐 정지 |
| ACTIVE | 보고 사이클 완료 + 새로운 요청 | REQUESTED | 2초 후 재실행 |
| ACTIVE | `__page_reporting_request()` 호출 | ACTIVE (유지) | 이미 진행 중 |
| 조건 | 처리 여부 | 설명 |
|---|---|---|
| `PageReported(page)` | 건너뜀 | 이미 호스트에 보고된 페이지 |
| `is_migrate_isolate(mt)` | 건너뜀 | 격리된 migratetype (Compaction 사용) |
| `order < page_reporting_order` | 해당 없음 | 최소 order 미만은 보고 대상 아님 |
| `!zone_watermark_ok()` | 해당 zone 건너뜀 | 메모리 부족 시 보고 불가 |
| `budget < 0` | 순회 중단 | free list 예산 초과 |