Ryotta's Linux 7.0 MM

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

# Free Page Reporting

개요 (Overview)

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

핵심 자료구조

struct page_reporting_dev_info

드라이버가 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          /* 보고 진행 중 — 실제 처리 중 */
};

PAGE_REPORTING_CAPACITY

scatterlist의 크기 — 한 번에 드라이버에 전달할 최대 페이지 수입니다.

/* include/linux/page_reporting.h:9 */
#define PAGE_REPORTING_CAPACITY		32

PageReported 플래그

Buddy Allocator에서 이미 보고된 페이지를 추적하는 페이지 플래그입니다.

/* include/linux/page-flags.h:682-688 */
/*
 * PageReported()는 Buddy Allocator 내부에서 보고된 빈 페이지를 추적합니다.
 * zone lock으로 보호해야 비트 설정/해제 경쟁을 막을 수 있습니다.
 */
__PAGEFLAG(Reported, reported, PF_NO_COMPOUND)

page_reporting_notify_free — 핫 경로 진입점

__free_one_page()에서 호출되는 인라인 함수로, free 페이지가 발생할 때 page_reporting에 알립니다.

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();
}

핵심 함수

0. `__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();
}

1. `__page_reporting_request()`

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 == IDLEREQUESTED로 변경 후 2초 지연 워크 예약
  • 2. `page_reporting_cycle()`

    특정 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() 콜백 호출
  • 3. `page_reporting_process_zone()`

    단일 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;
    }

    4. `page_reporting_process()`

    지연 워크 핸들러 — 모든 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 복귀 시도
  • 보고 진행 중 새로운 요청이 들어와 state가 REQUESTED로 변경된 경우 → 추가 2초 지연 후 재실행
  • 5. `page_reporting_register()` / `page_reporting_unregister()`

    드라이버의 등록/해제 함수입니다.

    /* 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);
    }

    호출 흐름

    Free Page Reporting 호출 흐름
    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 반환

    조건별 비교

    드라이버별 report 콜백 비교

    항목Virtio BalloonHyper-V Balloon
    소스 파일`drivers/virtio/virtio_balloon.c``drivers/hv/hv_balloon.c`
    콜백 함수`virtballoon_free_page_report()``hv_free_page_report()`
    등록 시 orderARM64_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()` 호출REQUESTED2초 지연 워크 예약
    REQUESTED`__page_reporting_request()` 호출REQUESTED (유지)중복 예약 방지
    ACTIVE보고 사이클 완료 + 요청 없음IDLE워크 큐 정지
    ACTIVE보고 사이클 완료 + 새로운 요청REQUESTED2초 후 재실행
    ACTIVE`__page_reporting_request()` 호출ACTIVE (유지)이미 진행 중

    free list 순회 대상 비교

    조건처리 여부설명
    `PageReported(page)`건너뜀이미 호스트에 보고된 페이지
    `is_migrate_isolate(mt)`건너뜀격리된 migratetype (Compaction 사용)
    `order < page_reporting_order`해당 없음최소 order 미만은 보고 대상 아님
    `!zone_watermark_ok()`해당 zone 건너뜀메모리 부족 시 보고 불가
    `budget < 0`순회 중단free list 예산 초과

    관련 문서

  • 메모리 관리 개요
  • Buddy Allocator
  • VMA / mmap
  • Virtio Balloon
  • DMA Pool