Ryotta's Linux 7.0 MM

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

♻️ 페이지 회수 (vmscan)

관련 소스: mm/vmscan.c, mm/swap.c, mm/page_alloc.c, mm/shrinker.c, include/linux/mmzone.h
관련 문서: Swap / zswap · OOM Killer · 메모리 Compaction · Folio / Page Cache · SLUB 할당자 · DAMON · 메모리 관리 개요

개요 (Overview)

페이지 회수(Page Reclaim)는 시스템의 가용 메모리가 부족해질 때 커널이 LRU(Least Recently Used) 리스트에서 적극적으로 페이지를 선택하여 해제하거나 스왑으로 내보내는 메커니즘입니다. Linux는 크게 두 가지 경로로 동작합니다: kswapd는 백그라운드에서 watermark 복구를 위해 주기적으로 회수를 수행하고, direct reclaim은 할당 시점에 프로세스가 직접 회수를 수행합니다. 핵심 소스 파일은 mm/vmscan.c이며, LRU 리스트 관리 보조 코드는 mm/swap.c에 위치합니다.

Linux 6.1+에서는 MGLRU(Multi-Gen LRU)가 기본 활성화되어 기존의 active/inactive 2-list 구조를 개선한 세대(generation) 기반 회수로 전환되었습니다. scan_control 구조체가 회수 세션의 모든 파라미터를 담당하며, shrink_lruvec()shrink_inactive_list()shrink_folio_list() 체인을 통해 실제 페이지 해제가 이루어집니다.

일상 비유로 보면 페이지 회수는 도서관에서 오래 빌려가지 않은 책부터 서가에서 빼는 작업과 비슷합니다. 파일 기반 페이지는 원본 파일이라는 서고가 있어 필요하면 다시 가져오면 되지만, 익명 페이지는 프로세스의 작업 메모리라서 스왑이라는 창고에 보관한 뒤 비워야 합니다. kswapd는 서가가 비기 전에 미리 정리하는 직원이고, direct reclaim은 방문자가 새 책을 꽂으려는 순간 직접 자리를 만드는 상황입니다.

소스 파일 경로:
  mm/vmscan.c          — 핵심 회수 로직 (scan_control, shrink_*, kswapd)
  mm/swap.c            — LRU 리스트 관리 (lru_add, lru_activate, folio_batch)
  mm/page_alloc.c      — direct reclaim 진입 전후의 할당 slowpath
  mm/shrinker.c        — shrink_slab()과 shrinker 콜백 실행
  mm/swap_state.c      — swap-in/out, 페이지 캐시
  mm/page_writeback.c  — dirty 페이지 writeback 관리
  include/linux/swap.h — swap 관련 주요 선언
  include/linux/mmzone.h — lruvec, zone 정의

빠른 점검 명령

# 1. 시스템 메모리 회수 관련 vmstat 카운터
cat /proc/vmstat | grep -E 'pgscan|pgsteal|pgscan_direct|pgscan_kswapd|pgsteal_direct|pgsteal_kswapd|pgrefill|pgactivate|pgdeactivate|allocstall|pgfault|pgmajfault'

# 2. PSI(Pressure Stall Information) — 메모리 압력 실시간 관찰
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

# 3. zone별 회수 가능 페이지 수 확인
cat /proc/zoneinfo | grep -E 'Node|zone|min|low|high|managed|nr_free|inactive|active|scanned|steal|refill|activate|deactivate' | head -40

# 4. memcg별 회수 통계
cat /proc/meminfo | grep -E 'AnonPages|Cached|SwapTotal|SwapFree|Dirty|Writeback|Inactive|Active|Slab|SReclaimable|SUnreclaim|Shmem'

# 5. swappiness 설정값 확인 (0~200)
cat /proc/sys/vm/swappiness

# 6. kswapd 활성 상태 관찰
ps aux | grep kswapd

# 7. 워터마크 관련 설정
cat /proc/sys/vm/min_free_kbytes
cat /proc/sys/vm/watermark_boost_factor
cat /proc/sys/vm/watermark_scale_factor

# 8. MGLRU 활성화 상태와 최소 TTL 설정 확인
cat /sys/kernel/mm/lru_gen/enabled 2>/dev/null || true
cat /sys/kernel/mm/lru_gen/min_ttl_ms 2>/dev/null || true

# 9. 회수 효율: 스캔 대비 회수량 비교
cat /proc/vmstat | grep -E 'pgscan|pgsteal|pgscan_skip|pgrefill|workingset'

# 10. swap, zswap, 메모리 티어링 관련 카운터
cat /proc/vmstat | grep -E 'pswpin|pswpout|zswp|pgdemote|pgpromote'

# 11. cgroup v2 메모리 이벤트와 OOM/pressure 확인
grep -R . /sys/fs/cgroup/*/memory.events 2>/dev/null | head -20

# 12. direct reclaim 지연을 시간 축으로 관찰
vmstat 1 10

핵심 자료구조

`struct scan_control` — 회수 세션의 전체 상태

vmscan.c:74-182에 정의된 scan_control은 하나의 회수 세션(reclaim session)이 사용하는 모든 파라미터와 상태를 관리합니다. try_to_free_pages()에서 초기화되고, do_try_to_free_pages()의 priority 루프 동안 공유됩니다.

/* mm/vmscan.c:74-182 */
struct scan_control {
	/* shrink_list()가 회수해야 할 페이지 수 */
	unsigned long nr_to_reclaim;

	/*
	 * 호출자가 허용한 노드의 노드마스크. NULL이면 모든 노드를
	 * 스캔합니다.
	 */
	nodemask_t	*nodemask;

	/*
	 * 한도에 도달한 메모리 컨트롤러. 따라서 이 회수 호출의
	 * 주요 대상이 됩니다.
	 */
	struct mem_cgroup *target_mem_cgroup;

	/*
	 * 익명/파일 LRU 간 스캔 압력 균형
	 */
	unsigned long	anon_cost;
	unsigned long	file_cost;

	/* 능동적 회수를 위한 swappiness 값. 항상 sc_swappiness()를 사용하세요! */
	int *proactive_swappiness;

	/* 회수 과정에서 활성 folio를 비활성화할 수 있는지? */
#define DEACTIVATE_ANON 1
#define DEACTIVATE_FILE 2
	unsigned int may_deactivate:2;
	unsigned int force_deactivate:1;
	unsigned int skipped_deactivate:1;

	/* zone_reclaim_mode, 회수 부스팅 */
	unsigned int may_writepage:1;

	/* zone_reclaim_mode */
	unsigned int may_unmap:1;

	/* zone_reclaim_mode, 회수 부스팅, 컨트롤러 제약 */
	unsigned int may_swap:1;

	/* 회수 과정에서 cache_trim_mode 활성화를 허용하지 않을지? */
	unsigned int no_cache_trim_mode:1;

	/* cache_trim_mode가 최소 한 번 실패했는지? */
	unsigned int cache_trim_mode_failed:1;

	/* 사용자 공간에서 호출한 능동적 회수 */
	unsigned int proactive:1;

	/*
	 * OOM 위협이 없는 한 memory.low 이하의 컨트롤러 메모리는 보호됩니다.
	 * 컨트롤러가 memory.low 설정으로 인해 축소된 힘으로 회수되거나
	 * 완전히 건너뛰어지고 아무것도 회수되지 않으면, OOM을 피하기 위해
	 * 보호된 메모리를 회수하는 한 사이클을 더 수행합니다.
	 */
	unsigned int memcg_low_reclaim:1;
	unsigned int memcg_low_skipped:1;

	/* 공유 컨트롤러 트리 순회 실패, 전체 트리 재스캔 */
	unsigned int memcg_full_walk:1;

	unsigned int hibernation_mode:1;

	/* 하나의 존이 컴팩션 준비 완료 */
	unsigned int compaction_ready:1;

	/* 현재 노드에 쉽게 회수 가능한 냉장 캐시가 있음 */
	unsigned int cache_trim_mode:1;

	/* 현재 노드의 파일 folio가 위험할 정도로 적음 */
	unsigned int file_is_tiny:1;

	/* 하위 티어 메모리로 하향 대신 항상 폐기 */
	unsigned int no_demotion:1;

	/* 할당 순서 */
	s8 order;

	/* (total_size >> priority)페이지를 한 번에 스캔 */
	s8 priority;

	/* 회수를 위해 folio를 격리할 최대 존 */
	s8 reclaim_idx;

	/* 이 컨텍스트의 GFP 마스크 */
	gfp_t gfp_mask;

	/* 스캔된 비활성 페이지 수만큼 증가 */
	unsigned long nr_scanned;

	/* shrink_zones() 호출 중 지금까지 해제된 페이지 수 */
	unsigned long nr_reclaimed;

	struct {
		unsigned int dirty;
		unsigned int unqueued_dirty;
		unsigned int congested;
		unsigned int writeback;
		unsigned int immediate;
		unsigned int file_taken;
		unsigned int taken;
	} nr;

	/* for recording the reclaimed slab by now */
	struct reclaim_state reclaim_state;
};

`struct cpu_fbatches` — per-CPU LRU 배치 처리

swap.c:50-66에서 정의된 이 구조체는 LRU 리스트 작업을 per-CPU 로컬 배치로 묶어 락 경합을 줄입니다.

/* mm/swap.c:50-66 */
struct cpu_fbatches {
	/*
	 * The following folio batches are grouped together because they are protected
	 * by disabling preemption (and interrupts remain enabled).
	 */
	local_lock_t lock;
	struct folio_batch lru_add;
	struct folio_batch lru_deactivate_file;
	struct folio_batch lru_deactivate;
	struct folio_batch lru_lazyfree;
#ifdef CONFIG_SMP
	struct folio_batch lru_activate;
#endif
	/* 인터럽트 비활성화가 필요한 배치 보호 */
	local_lock_t lock_irq;
	struct folio_batch lru_move_tail;
};

LRU 리스트 계층

lruvec
├── lists[LRU_INACTIVE_ANON]   ← 비활성 익명 페이지
├── lists[LRU_ACTIVE_ANON]     ← 활성 익명 페이지
├── lists[LRU_INACTIVE_FILE]   ← 비활성 파일 기반 페이지
├── lists[LRU_ACTIVE_FILE]     ← 활성 파일 기반 페이지
├── anon_cost                  ← 익명 LRU 스캔 비용
├── file_cost                  ← 파일 LRU 스캔 비용
└── lrugen (MGLRU)             ← 세대 기반 회수용 구조
    ├── max_seq                 ← 최신 세대 번호
    ├── min_seq[ANON_AND_FILE]  ← 각 타입별 최소 세대
    ├── nr_pages[gen][type][zone]
    └── timestamps[gen]

Linux 7.0의 LRU 리스트 열거값은 include/linux/mmzone.h:316-345에 있으며, 회수 대상 네 리스트와 LRU_UNEVICTABLE을 분리합니다.

/* include/linux/mmzone.h:316-345 */
enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
	LRU_UNEVICTABLE,
	NR_LRU_LISTS
};

enum vmscan_throttle_state {
	VMSCAN_THROTTLE_WRITEBACK,
	VMSCAN_THROTTLE_ISOLATED,
	VMSCAN_THROTTLE_NOPROGRESS,
	VMSCAN_THROTTLE_CONGESTED,
	NR_VMSCAN_THROTTLE,
};

#define for_each_lru(lru) for (lru = 0; lru < NR_LRU_LISTS; lru++)

#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)

static inline bool is_file_lru(enum lru_list lru)
{
	return (lru == LRU_INACTIVE_FILE || lru == LRU_ACTIVE_FILE);
}

static inline bool is_active_lru(enum lru_list lru)
{
	return (lru == LRU_ACTIVE_ANON || lru == LRU_ACTIVE_FILE);
}

struct lruvec는 memcg와 NUMA node 조합별 LRU 단위입니다. 전통 LRU의 lists[], anon/file 회수 비용, refault 기록과 MGLRU의 lrugen이 같은 구조체 안에서 만납니다.

/* include/linux/mmzone.h:669-698 */
struct lruvec {
	struct list_head		lists[NR_LRU_LISTS];
	/* per lruvec lru_lock for memcg */
	spinlock_t			lru_lock;
	/*
	 * These track the cost of reclaiming one LRU - file or anon -
	 * over the other. As the observed cost of reclaiming one LRU
	 * increases, the reclaim scan balance tips toward the other.
	 */
	unsigned long			anon_cost;
	unsigned long			file_cost;
	/* LRU 움직임에 의해 구동되는 비거주 페이지 나이 */
	atomic_long_t			nonresident_age;
	/* 마지막 회수 주기 시점의 재폴트 수 */
	unsigned long			refaults[ANON_AND_FILE];
	/* 다양한 lruvec 상태 플래그 (enum lruvec_flags) */
	unsigned long			flags;
#ifdef CONFIG_LRU_GEN
	/* 세대로 분할된 회수 가능 페이지 */
	struct lru_gen_folio		lrugen;
#ifdef CONFIG_LRU_GEN_WALKS_MMU
	/* lru_gen_mm_list를 동시 순회하기 위해 */
	struct lru_gen_mm_state		mm_state;
#endif
#endif /* CONFIG_LRU_GEN */
#ifdef CONFIG_MEMCG
	struct pglist_data *pgdat;
#endif
	struct zswap_lruvec_state zswap_lruvec_state;
};

핵심 함수

1. `try_to_free_pages()` — Direct Reclaim 진입점

/* mm/vmscan.c:6566 */
unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
                gfp_t gfp_mask, nodemask_t *nodemask)
  • 역할: 페이지 할당기가 watermark 미달 시 직접 호출하는 회수 진입점
  • 초기화: scan_controlSWAP_CLUSTER_MAX 회수 목표, DEF_PRIORITY 스캔 우선순위로 설정
  • 흐름: throttle_direct_reclaim()do_try_to_free_pages() → 반환
  • try_to_free_pages()는 할당자의 GFP 컨텍스트를 current_gfp_context()로 정리하고, may_writepage, may_unmap, may_swap을 모두 허용한 상태로 시작합니다. throttle_direct_reclaim()이 fatal signal을 감지하면 OOM 판단으로 이어지지 않도록 1을 반환합니다.

    /* mm/vmscan.c:6566-6607 */
    unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
    				gfp_t gfp_mask, nodemask_t *nodemask)
    {
    	unsigned long nr_reclaimed;
    	struct scan_control sc = {
    		.nr_to_reclaim = SWAP_CLUSTER_MAX,
    		.gfp_mask = current_gfp_context(gfp_mask),
    		.reclaim_idx = gfp_zone(gfp_mask),
    		.order = order,
    		.nodemask = nodemask,
    		.priority = DEF_PRIORITY,
    		.may_writepage = 1,
    		.may_unmap = 1,
    		.may_swap = 1,
    	};
    
    	/*
    	 * scan_control uses s8 fields for order, priority, and reclaim_idx.
    	 * Confirm they are large enough for max values.
    	 */
    	BUILD_BUG_ON(MAX_PAGE_ORDER >= S8_MAX);
    	BUILD_BUG_ON(DEF_PRIORITY > S8_MAX);
    	BUILD_BUG_ON(MAX_NR_ZONES > S8_MAX);
    
    	/*
    	 * Do not enter reclaim if fatal signal was delivered while throttled.
    	 * 1 is returned so that the page allocator does not OOM kill at this
    	 * point.
    	 */
    	if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask))
    		return 1;
    
    	set_task_reclaim_state(current, &sc.reclaim_state);
    	trace_mm_vmscan_direct_reclaim_begin(order, sc.gfp_mask);
    
    	nr_reclaimed = do_try_to_free_pages(zonelist, &sc);
    
    	trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);
    	set_task_reclaim_state(current, NULL);
    
    	return nr_reclaimed;
    }

    2. `do_try_to_free_pages()` — Priority 기반 회수 루프

    /* mm/vmscan.c:6344 */
    static unsigned long do_try_to_free_pages(struct zonelist *zonelist,
                          struct scan_control *sc)
  • 역할: priority를 0까지 줄여가며 shrink_zones()를 반복 호출
  • 우선순위 루프: sc->priorityDEF_PRIORITY(12)에서 시작하여 매 루프마다 감소
  • - priority가 높으면 소수의 페이지만 스캔 (빠르게 종료)

    - priority가 낮으면 전체 LRU를 깊이 스캔 (많은 회수)

  • 재시도 조건:
  • - memcg_full_walk: memcg 전체 순회 실패 시 재시도

    - skipped_deactivate: 비활성화 건너뛴 경우 강제 비활성화로 재시도

    - memcg_low_skipped: memory.low 보호 건너긴 경우 재시도

    priority 루프는 회수 목표를 채우거나 compaction 준비가 끝날 때 멈춥니다. 아무 페이지도 회수하지 못하면 memcg partial walk, active list 강제 비활성화, memory.low 보호 해제를 차례로 재시도합니다.

    /* mm/vmscan.c:6351-6438 */
    retry:
    	delayacct_freepages_start();
    
    	if (!cgroup_reclaim(sc))
    		__count_zid_vm_events(ALLOCSTALL, sc->reclaim_idx, 1);
    
    	do {
    		if (!sc->proactive)
    			vmpressure_prio(sc->gfp_mask, sc->target_mem_cgroup,
    					sc->priority);
    		sc->nr_scanned = 0;
    		shrink_zones(zonelist, sc);
    
    		if (sc->nr_reclaimed >= sc->nr_to_reclaim)
    			break;
    
    		if (sc->compaction_ready)
    			break;
    	} while (--sc->priority >= 0);
    
    	last_pgdat = NULL;
    	for_each_zone_zonelist_nodemask(zone, z, zonelist, sc->reclaim_idx,
    					sc->nodemask) {
    		if (zone->zone_pgdat == last_pgdat)
    			continue;
    		last_pgdat = zone->zone_pgdat;
    
    		snapshot_refaults(sc->target_mem_cgroup, zone->zone_pgdat);
    
    		if (cgroup_reclaim(sc)) {
    			struct lruvec *lruvec;
    
    			lruvec = mem_cgroup_lruvec(sc->target_mem_cgroup,
    						   zone->zone_pgdat);
    			clear_bit(LRUVEC_CGROUP_CONGESTED, &lruvec->flags);
    		}
    	}
    
    	delayacct_freepages_end();
    
    	if (sc->nr_reclaimed)
    		return sc->nr_reclaimed;
    
    	/* 컴팩션 시도를 위해 회수를 중단했는지? OOM하지 않음 */
    	if (sc->compaction_ready)
    		return 1;
    
    	if (!sc->memcg_full_walk) {
    		sc->priority = initial_priority;
    		sc->memcg_full_walk = 1;
    		goto retry;
    	}
    
    	if (sc->skipped_deactivate) {
    		sc->priority = initial_priority;
    		sc->force_deactivate = 1;
    		sc->skipped_deactivate = 0;
    		goto retry;
    	}
    
    	/* 미사용 컨트롤러 예약이 있는지? OOM하지 말고 재시도 */
    	if (sc->memcg_low_skipped) {
    		sc->priority = initial_priority;
    		sc->force_deactivate = 0;
    		sc->memcg_low_reclaim = 1;
    		sc->memcg_low_skipped = 0;
    		goto retry;
    	}
    
    	return 0;

    3. `shrink_node()` — 노드 단위 회수

    /* mm/vmscan.c:6039 */
    static void shrink_node(pg_data_t *pgdat, struct scan_control *sc)
  • 역할: 단일 NUMA 노드에 대해 모든 memcg의 LRU를 순회하며 회수
  • MGLRU 분기: lru_gen_enabled() && root_reclaim(sc)이면 lru_gen_shrink_node()로 전환
  • 핵심 호출: prepare_scan_control()shrink_node_memcgs()flush_reclaim_state()
  • 성능: memcg 순회 시 partial walk(direct) vs full walk(kswapd) 차이
  • 4. `shrink_lruvec()` — LRU 벡터 단위 회수

    /* mm/vmscan.c:5772 */
    static void shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
  • 역할: 하나의 lruvec(= memcg + node 조합)에서 어떤 LRU 리스트를 얼마나 스캔할지 결정하고 실행
  • MGLRU 분기: lru_gen_enabled() && !root_reclaim(sc)이면 lru_gen_shrink_lruvec()로 전환
  • 스캔 비율 결정: get_scan_count()에서 anon/file 비율을 swappiness, cost, 메모리 구성을 기반으로 계산
  • 비례 회수: proportional_reclaim 플래그로 anon/file 스캔 비율을 균형있게 유지
  • shrink_lruvec()의 내부 루프는 get_scan_count()가 채운 배열을 SWAP_CLUSTER_MAX 단위로 쪼개 shrink_list()에 넘깁니다. active LRU는 조건이 맞을 때 먼저 inactive로 내리고, inactive LRU만 shrink_inactive_list()에서 회수 대상이 됩니다.

    /* mm/vmscan.c:2249-2261 */
    static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
    				 struct lruvec *lruvec, struct scan_control *sc)
    {
    	if (is_active_lru(lru)) {
    		if (sc->may_deactivate & (1 << is_file_lru(lru)))
    			shrink_active_list(nr_to_scan, lruvec, sc, lru);
    		else
    			sc->skipped_deactivate = 1;
    		return 0;
    	}
    
    	return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
    }

    5. `shrink_inactive_list()` — 비활성 LRU에서 실제 회수

    /* mm/vmscan.c:1977 */
    static unsigned long shrink_inactive_list(unsigned long nr_to_scan,
            struct lruvec *lruvec, struct scan_control *sc,
            enum lru_list lru)
  • 역할: 비활성 LRU에서 페이지를 격리(isolate)하고 shrink_folio_list()로 실제 회수 수행
  • 흐름:
  • 1. too_many_isolated() 체크 → 과도 격리 시 throttle

    2. lru_add_drain() → per-CPU 배치 비우기

    3. isolate_lru_folios() → LRU에서 folio 격리

    4. shrink_folio_list() → folio별로 lock/dirty/writeback/references 검사 후 회수

    5. move_folios_to_lru() → 미회수 folio를 LRU로 복귀

    6. lru_note_cost_unlock_irq() → anon/file cost 갱신

    격리된 folio 수는 NR_ISOLATED_ANON/FILE, PGSCAN_, PGSTEAL_ 카운터에 반영됩니다. dirty folio가 모두 I/O 큐에 올라가지 못한 상태라면 flusher를 깨우고, 오래된 cgroup v1 writeback에서는 throttle을 걸어 과도한 OOM 진입을 피합니다.

    /* mm/vmscan.c:1991-2039 */
    while (unlikely(too_many_isolated(pgdat, file, sc))) {
    	if (stalled)
    		return 0;
    
    	/* wait a bit for the reclaimer. */
    	stalled = true;
    	reclaim_throttle(pgdat, VMSCAN_THROTTLE_ISOLATED);
    
    	/* 곧 종료되어 메모리를 해제할 예정. 지금 즉시 반환 */
    	if (fatal_signal_pending(current))
    		return SWAP_CLUSTER_MAX;
    }
    
    	lru_add_drain();
    
    	spin_lock_irq(&lruvec->lru_lock);
    
    	nr_taken = isolate_lru_folios(nr_to_scan, lruvec, &folio_list,
    				     &nr_scanned, sc, lru);
    
    	__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
    	item = PGSCAN_KSWAPD + reclaimer_offset(sc);
    	if (!cgroup_reclaim(sc))
    		__count_vm_events(item, nr_scanned);
    	count_memcg_events(lruvec_memcg(lruvec), item, nr_scanned);
    	__count_vm_events(PGSCAN_ANON + file, nr_scanned);
    
    	spin_unlock_irq(&lruvec->lru_lock);
    
    	if (nr_taken == 0)
    		return 0;
    
    	nr_reclaimed = shrink_folio_list(&folio_list, pgdat, sc, &stat, false,
    					 lruvec_memcg(lruvec));
    
    	spin_lock_irq(&lruvec->lru_lock);
    	move_folios_to_lru(lruvec, &folio_list);
    
    	mod_lruvec_state(lruvec, PGDEMOTE_KSWAPD + reclaimer_offset(sc),
    					stat.nr_demoted);
    	__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
    	item = PGSTEAL_KSWAPD + reclaimer_offset(sc);
    	if (!cgroup_reclaim(sc))
    		__count_vm_events(item, nr_reclaimed);
    	count_memcg_events(lruvec_memcg(lruvec), item, nr_reclaimed);
    	__count_vm_events(PGSTEAL_ANON + file, nr_reclaimed);
    
    	lru_note_cost_unlock_irq(lruvec, file, stat.nr_pageout,
    					nr_scanned - nr_reclaimed);

    6. `shrink_folio_list()` — 개별 folio 회수

    /* mm/vmscan.c:1083 */
    static unsigned int shrink_folio_list(struct list_head *folio_list,
            struct pglist_data *pgdat, struct scan_control *sc,
            struct reclaim_stat *stat, bool ignore_references,
            struct mem_cgroup *memcg)
  • 역할: 격리된 folio 목록을 순회하며 각 folio의 잠금, 더티, writeback, 참조 횟수를 검사하여 회수 여부 결정
  • 핵심 분기:
  • - folio_trylock() 실패 → keep

    - writeback 중 + kswapd + PGDAT_WRITEBACKnr_immediate 카운트, activate

    - writeback 중 + legacy memcg → writeback 완료 대기

    - folio_check_references() → ACTIVATE / KEEP / RECLAIM 분기

    - 익명 페이지 + swap 가능 → folio_alloc_swap() → pageout 또는 activate

    - 파일 기반 + clean → __remove_mapping() 으로 즉시 회수

    - dirty → pageout()으로 writeback 시작 후 회수 시도

    익명 folio가 swapbacked이고 아직 swapcache가 아니면 먼저 swap 슬롯을 확보합니다. 큰 folio는 그대로 swap 슬롯을 잡지 못할 때 분할 후 다시 시도하며, 실패하면 active LRU로 되돌립니다.

    /* mm/vmscan.c:1281-1338 */
    /*
     * Anonymous process memory has backing store?
     * Try to allocate it some swap space here.
     * Lazyfree folio could be freed directly
     */
    if (folio_test_anon(folio) && folio_test_swapbacked(folio) &&
    		!folio_test_swapcache(folio)) {
    	if (!(sc->gfp_mask & __GFP_IO))
    		goto keep_locked;
    	if (folio_maybe_dma_pinned(folio))
    		goto keep_locked;
    	if (folio_test_large(folio)) {
    		/* cannot split folio, skip it */
    		if (folio_expected_ref_count(folio) !=
    		    folio_ref_count(folio) - 1)
    			goto activate_locked;
    		/*
    		 * Split partially mapped folios right away.
    		 * We can free the unmapped pages without IO.
    		 */
    		if (data_race(!list_empty(&folio->_deferred_list) &&
    		    folio_test_partially_mapped(folio)) &&
    		    split_folio_to_list(folio, folio_list))
    			goto activate_locked;
    	}
    	if (folio_alloc_swap(folio)) {
    		int __maybe_unused order = folio_order(folio);
    
    		if (!folio_test_large(folio))
    			goto activate_locked_split;
    		/* 일반 페이지로 스왑 대체 */
    		if (split_folio_to_list(folio, folio_list))
    			goto activate_locked;
    #ifdef CONFIG_TRANSPARENT_HUGEPAGE
    		if (nr_pages >= HPAGE_PMD_NR) {
    			count_memcg_folio_events(folio,
    				THP_SWPOUT_FALLBACK, 1);
    			count_vm_event(THP_SWPOUT_FALLBACK);
    		}
    #endif
    		count_mthp_stat(order, MTHP_STAT_SWPOUT_FALLBACK);
    		if (folio_alloc_swap(folio))
    			goto activate_locked_split;
    	}
    	/*
    	 * Normally the folio will be dirtied in unmap because
    	 * its pte should be dirty. A special case is MADV_FREE
    	 * page. The page's pte could have dirty bit cleared but
    	 * the folio's SwapBacked flag is still set because
    	 * clearing the dirty bit and SwapBacked flag has no
    	 * lock protected. For such folio, unmap will not set
    	 * dirty bit for it, so folio reclaim will not write the
    	 * folio out. This can cause data corruption when the
    	 * folio is swapped in later. Always setting the dirty
    	 * flag for the folio solves the problem.
    	 */
    	folio_mark_dirty(folio);
    }

    dirty folio는 pageout()으로 writeback을 시작합니다. clean 상태가 되거나 이미 clean인 파일 folio는 buffer release 후 __remove_mapping()으로 address_space에서 떼어내고 free_unref_folios() 배치로 반환합니다.

    /* mm/vmscan.c:1395-1464 */
    mapping = folio_mapping(folio);
    if (folio_test_dirty(folio)) {
    	if (folio_is_file_lru(folio)) {
    		/*
    		 * Immediately reclaim when written back.
    		 * Similar in principle to folio_deactivate()
    		 * except we already have the folio isolated
    		 * and know it's dirty
    		 */
    		node_stat_mod_folio(folio, NR_VMSCAN_IMMEDIATE,
    				nr_pages);
    		if (!folio_test_reclaim(folio))
    			folio_set_reclaim(folio);
    
    		goto activate_locked;
    	}
    
    	if (references == FOLIOREF_RECLAIM_CLEAN)
    		goto keep_locked;
    	if (!may_enter_fs(folio, sc->gfp_mask))
    		goto keep_locked;
    	if (!sc->may_writepage)
    		goto keep_locked;
    
    	/*
    	 * Folio is dirty. Flush the TLB if a writable entry
    	 * potentially exists to avoid CPU writes after I/O
    	 * starts and then write it out here.
    	 */
    	try_to_unmap_flush_dirty();
    	switch (pageout(folio, mapping, &plug, folio_list)) {
    	case PAGE_KEEP:
    		goto keep_locked;
    	case PAGE_ACTIVATE:
    		/*
    		 * If shmem folio is split when writeback to swap,
    		 * the tail pages will make their own pass through
    		 * this function and be accounted then.
    		 */
    		if (nr_pages > 1 && !folio_test_large(folio)) {
    			sc->nr_scanned -= (nr_pages - 1);
    			nr_pages = 1;
    		}
    		goto activate_locked;
    	case PAGE_SUCCESS:
    		if (nr_pages > 1 && !folio_test_large(folio)) {
    			sc->nr_scanned -= (nr_pages - 1);
    			nr_pages = 1;
    		}
    		stat->nr_pageout += nr_pages;
    
    		if (folio_test_writeback(folio))
    			goto keep;
    		if (folio_test_dirty(folio))
    			goto keep;
    
    		/*
    		 * A synchronous write - probably a ramdisk.  Go
    		 * ahead and try to reclaim the folio.
    		 */
    		if (!folio_trylock(folio))
    			goto keep;
    		if (folio_test_dirty(folio) ||
    		    folio_test_writeback(folio))
    			goto keep_locked;
    		mapping = folio_mapping(folio);
    		fallthrough;
    	case PAGE_CLEAN:
    		; /* try to free the folio below */
    	}
    }
    /* mm/vmscan.c:1511-1543 */
    if (folio_test_anon(folio) && !folio_test_swapbacked(folio)) {
    	/* follow __remove_mapping for reference */
    	if (!folio_ref_freeze(folio, 1))
    		goto keep_locked;
    	/*
    	 * The folio has only one reference left, which is
    	 * from the isolation. After the caller puts the
    	 * folio back on the lru and drops the reference, the
    	 * folio will be freed anyway. It doesn't matter
    	 * which lru it goes on. So we don't bother checking
    	 * the dirty flag here.
    	 */
    	count_vm_events(PGLAZYFREED, nr_pages);
    	count_memcg_folio_events(folio, PGLAZYFREED, nr_pages);
    } else if (!mapping || !__remove_mapping(mapping, folio, true,
    						 sc->target_mem_cgroup))
    	goto keep_locked;
    
    	folio_unlock(folio);
    free_it:
    	/*
    	 * Folio may get swapped out as a whole, need to account
    	 * all pages in it.
    	 */
    	nr_reclaimed += nr_pages;
    
    	folio_unqueue_deferred_split(folio);
    	if (folio_batch_add(&free_folios, folio) == 0) {
    		mem_cgroup_uncharge_folios(&free_folios);
    		try_to_unmap_flush();
    		free_unref_folios(&free_folios);
    	}
    	continue;

    7. `kswapd()` — 백그라운드 회수 데몬

    /* mm/vmscan.c:7280 */
    static int kswapd(void *p)
  • 역할: 각 NUMA 노드당 하나씩 생성되는 커널 스레드. watermark 복구를 위해 백그라운드에서 회수 수행
  • 루프: kswapd_try_to_sleep()balance_pgdat() → 반복
  • sleep 조건: prepare_kswapd_sleep()가 모든 존의 free 페이지가 high watermark를 초과할 때 true 반환
  • 동작: priority를 점진적으로 줄여가며 shrink_node() 호출, compaction 필요 시 kcompactd 깨움
  • kswapd()PF_MEMALLOC | PF_KSWAPD를 설정해 회수 중 재귀적인 일반 할당 경로에 갇히지 않도록 합니다. wakeup 쪽에서 전달한 order와 zone index를 읽고, balance_pgdat()으로 node 전체를 high watermark 쪽으로 되돌립니다.

    /* mm/vmscan.c:7299-7346 */
    tsk->flags |= PF_MEMALLOC | PF_KSWAPD;
    set_freezable();
    
    WRITE_ONCE(pgdat->kswapd_order, 0);
    WRITE_ONCE(pgdat->kswapd_highest_zoneidx, MAX_NR_ZONES);
    atomic_set(&pgdat->nr_writeback_throttled, 0);
    for ( ; ; ) {
    	bool was_frozen;
    
    	alloc_order = reclaim_order = READ_ONCE(pgdat->kswapd_order);
    	highest_zoneidx = kswapd_highest_zoneidx(pgdat,
    						highest_zoneidx);
    
    kswapd_try_sleep:
    	kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order,
    				highest_zoneidx);
    
    	/* 새로운 order와 highest_zoneidx 읽기 */
    	alloc_order = READ_ONCE(pgdat->kswapd_order);
    	highest_zoneidx = kswapd_highest_zoneidx(pgdat,
    						highest_zoneidx);
    	WRITE_ONCE(pgdat->kswapd_order, 0);
    	WRITE_ONCE(pgdat->kswapd_highest_zoneidx, MAX_NR_ZONES);
    
    	if (kthread_freezable_should_stop(&was_frozen))
    		break;
    
    	/*
    	 * We can speed up thawing tasks if we don't call balance_pgdat
    	 * after returning from the refrigerator
    	 */
    	if (was_frozen)
    		continue;
    
    	trace_mm_vmscan_kswapd_wake(pgdat->node_id, highest_zoneidx,
    					alloc_order);
    	reclaim_order = balance_pgdat(pgdat, alloc_order,
    					highest_zoneidx);
    	if (reclaim_order < alloc_order)
    		goto kswapd_try_sleep;
    }

    8. `get_scan_count()` — anon/file 스캔 비율 결정

    /* mm/vmscan.c:2527 */
    static void get_scan_count(struct lruvec *lruvec, struct scan_control *sc,
                   unsigned long *nr)
  • 역할: 4개 LRU 리스트(inactive/active × anon/file)에 대해 스캔할 페이지 수 결정
  • scan_balance 분기:
  • - SCAN_FILE: swap 불가 또는 swappiness=0 → 파일만 스캔

    - SCAN_ANON: file_is_tiny → 익명만 스캔

    - SCAN_EQUAL: priority=0 + swappiness>0 → anon/file 동일 비율

    - SCAN_FRACT: 기본값. swappiness와 anon/file cost 비율에 따라 비례 스캔

    get_scan_count()는 swap 가능성, memcg swappiness, OOM 근접도, 파일 캐시 부족 여부를 먼저 보며, 기본 경로에서는 최근 회수 비용(anon_cost, file_cost)과 swappiness를 섞어 비율을 계산합니다.

    /* mm/vmscan.c:2538-2592 */
    /* 스왑 공간이 없으면 익명 folio 스캔을 시도하지 않음 */
    if (!sc->may_swap || !can_reclaim_anon_pages(memcg, pgdat->node_id, sc)) {
    	scan_balance = SCAN_FILE;
    	goto out;
    }
    
    /*
     * Global reclaim will swap to prevent OOM even with no
     * swappiness, but memcg users want to use this knob to
     * disable swapping for individual groups completely when
     * using the memory controller's swap limit feature would be
     * too expensive.
     */
    if (cgroup_reclaim(sc) && !swappiness) {
    	scan_balance = SCAN_FILE;
    	goto out;
    }
    
    /* 익명 메모리에만 사용자 공간에서 호출한 능동적 회수 */
    if (swappiness == SWAPPINESS_ANON_ONLY) {
    	WARN_ON_ONCE(!sc->proactive);
    	scan_balance = SCAN_ANON;
    	goto out;
    }
    
    /*
     * Do not apply any pressure balancing cleverness when the
     * system is close to OOM, scan both anon and file equally
     * (unless the swappiness setting disagrees with swapping).
     */
    if (!sc->priority && swappiness) {
    	scan_balance = SCAN_EQUAL;
    	goto out;
    }
    
    /*
     * If the system is almost out of file pages, force-scan anon.
     */
    if (sc->file_is_tiny) {
    	scan_balance = SCAN_ANON;
    	goto out;
    }
    
    /*
     * If there is enough inactive page cache, we do not reclaim
     * anything from the anonymous working right now to make sure
     * a streaming file access pattern doesn't cause swapping.
     */
    if (sc->cache_trim_mode) {
    	scan_balance = SCAN_FILE;
    	goto out;
    }
    
    scan_balance = SCAN_FRACT;
    calculate_pressure_balance(sc, swappiness, fraction, &denominator);

    호출 흐름

    페이지 회수 호출 흐름 다이어그램

    Direct Reclaim 경로

    __alloc_pages() (page_alloc.c)
      └─ __alloc_pages_slowpath()
           └─ __alloc_pages_direct_reclaim()              [page_alloc.c:4437]
                └─ try_to_free_pages()                    [vmscan.c:6566]
                     ├─ throttle_direct_reclaim()         [vmscan.c:6486]
                     └─ do_try_to_free_pages()            [vmscan.c:6344]
                           └─ priority 루프 (12 → 0):
                                └─ shrink_zones()          [vmscan.c:6221]
                                     └─ for_each_zone:
                                          └─ shrink_node() [vmscan.c:6039]
                                               ├─ prepare_scan_control() [vmscan.c:2317]
                                               └─ shrink_node_memcgs()   [vmscan.c:5960]
                                                    └─ for_each_memcg:
                                                         ├─ shrink_lruvec()     [vmscan.c:5772]
                                                         │    ├─ get_scan_count() [vmscan.c:2527]
                                                         │    └─ for_each_lru:
                                                         │         └─ shrink_list() [vmscan.c:2249]
                                                         │              ├─ [active] shrink_active_list()  [vmscan.c:2098]
                                                         │              └─ [inactive] shrink_inactive_list() [vmscan.c:1977]
                                                         │                   ├─ isolate_lru_folios()      [vmscan.c:1710]
                                                         │                   ├─ shrink_folio_list()        [vmscan.c:1083]
                                                         │                   │    ├─ folio_check_references()
                                                         │                   │    ├─ folio_alloc_swap() (익명)
                                                         │                   │    ├─ pageout() (dirty)
                                                         │                   │    └─ __remove_mapping() (clean)
                                                         │                   └─ move_folios_to_lru()       [vmscan.c:1895]
                                                         └─ shrink_slab() [vmscan.c:6022 → mm/shrinker.c:614]

    할당 slowpath 쪽에서는 direct reclaim 전후로 PSI memstall을 기록합니다. 회수 후에도 할당이 실패하면 highatomic reserve와 per-CPU page list를 비운 뒤 한 번 더 freelist를 확인합니다.

    /* mm/page_alloc.c:4435-4468 */
    /* 직접 회수에 진입하는 진짜 느린 할당 경로 */
    static inline struct page *
    __alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
    		unsigned int alloc_flags, const struct alloc_context *ac,
    		unsigned long *did_some_progress)
    {
    	struct page *page = NULL;
    	unsigned long pflags;
    	bool drained = false;
    
    	psi_memstall_enter(&pflags);
    	*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
    	if (unlikely(!(*did_some_progress)))
    		goto out;
    
    retry:
    	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    
    	/*
    	 * If an allocation failed after direct reclaim, it could be because
    	 * pages are pinned on the per-cpu lists or in high alloc reserves.
    	 * Shrink them and try again
    	 */
    	if (!page && !drained) {
    		unreserve_highatomic_pageblock(ac, false);
    		drain_all_pages(NULL);
    		drained = true;
    		goto retry;
    	}
    out:
    	psi_memstall_leave(&pflags);
    
    	return page;
    }

    Kswapd 경로

    kswapd()                              [vmscan.c:7280]
      └─ for(;;):
           ├─ kswapd_try_to_sleep()       [vmscan.c:7183]
           │    └─ prepare_kswapd_sleep() [vmscan.c:6838]
           │         └─ zone_watermark_ok() 체크 → sleep/wake
           └─ balance_pgdat()             [vmscan.c:6950]
                └─ priority 루프:
                     └─ shrink_node() → (위와 동일)

    MGLRU 경로

    shrink_node() [lru_gen_enabled()]
      └─ lru_gen_shrink_node() [vmscan.c:5038]
           └─ evict_folios()    [vmscan.c:4686]
                ├─ get_nr_to_scan()      [vmscan.c:4811]
                ├─ try_to_inc_max_seq()  [vmscan.c:4021] — page table walk로 세대 에이징
                ├─ read_ctrl_pos()       [vmscan.c:3159] — 세대별 스캔 위치 결정
                └─ walk_pmd_range()      [vmscan.c:3660] — PTE 스캔

    조건별 비교

    Direct Reclaim vs kswapd

    항목Direct Reclaimkswapd
    **진입 시점**할당 시 watermark 미달백그라운드 스레드
    **지연(latency)**높음 (할당 지연에 직접 기여)낮음 (백그라운드)
    **PF_memalloc 플래그**없음 (일반 프로세스)있음 (PF_MEMALLOC)
    **throttle**`throttle_direct_reclaim()`없음
    **partial walk**가능 (공정성 고려)전체 walk
    **memcg 보호**`memory.low` 보호를 잠시 두고 재시도`memory.low` 건너뜀
    **스캔 우선순위**DEF_PRIORITY에서 시작점진적 우선순위 감소

    Watermark 트리거

    Zone watermark는 include/linux/mmzone.h:708-714WMARK_MIN, WMARK_LOW, WMARK_HIGH, WMARK_PROMO로 정의됩니다. 일반 회수 판단에서는 free 페이지가 low 아래로 내려가면 kswapd를 깨우고, min 아래에서는 할당자가 direct reclaim까지 수행합니다. high를 회복하면 kswapd는 다시 수면 상태로 들어갑니다.

    /* include/linux/mmzone.h:708-714 */
    enum zone_watermarks {
    	WMARK_MIN,
    	WMARK_LOW,
    	WMARK_HIGH,
    	WMARK_PROMO,
    	NR_WMARK
    };
    Watermark조건주된 동작확인 포인트
    `WMARK_HIGH`free > highkswapd가 회수를 멈추고 sleep 가능`/proc/zoneinfo`의 `high`와 `nr_free_pages`
    `WMARK_LOW`free < low`wakeup_kswapd()`로 백그라운드 회수 시작`pgscan_kswapd`, `pgsteal_kswapd` 증가
    `WMARK_MIN`free < min할당 slowpath에서 direct reclaim 가능`allocstall_*`, `pgscan_direct` 증가
    `WMARK_PROMO`tiering/NUMA 승격 판단느린 tier에서 빠른 tier로 page promotion 판단에 사용`/proc/zoneinfo`의 `promo` 계열 표시

    Classic LRU vs MGLRU

    항목Classic LRUMGLRU (Multi-Gen LRU)
    **리스트 구조**active/inactive 4개 리스트세대(generation) 기반 다중 리스트
    **에이징 메커니즘**referenced 비트 기반page table walk + Bloom filter
    **CPU 오버헤드**낮음 (LRU lock)높음 (page walk) BUT 효율적
    **정확도**최근 접근 추적 부정확spatial locality 활용으로 정확
    **MGLRU 코드 위치**-`vmscan.c:2659-5770`
    **활성화 조건**기본값`CONFIG_LRU_GEN_ENABLED`

    anon 페이지 vs 파일 기반 페이지 회수

    항목익명 페이지 (Anonymous)파일 기반 페이지 (File-backed)
    **회수 방법**swap으로 내보내기원본 파일에서 다시 읽을 수 있으므로 즉시 해제
    **swappiness 영향**높음 (0이면 회수 안 함)낮음 (기본적으로 우선 회수)
    **cost 비용**`lruvec->anon_cost``lruvec->file_cost`
    **writeback**swap device writebackfilesystem writeback
    **demotion**가능 (NUMA 티어링)불가

    회수 대상 우선순위

    전통 LRU에서 실제 스캔 순서는 get_scan_count()가 계산한 anon/file 비율과 active/inactive 상태에 따라 달라집니다. 운영 관점에서는 비용이 낮은 파일 캐시와 비활성 folio가 먼저 사라지고, 익명 active folio는 swap, demotion, 참조 비트 때문에 더 비싼 경로를 탑니다.

    대상비용대표 경로특징
    `LRU_INACTIVE_FILE`낮음clean이면 `__remove_mapping()` 후 free원본 파일에서 다시 읽을 수 있어 회수 우선 후보
    `LRU_ACTIVE_FILE`중간`shrink_active_list()`로 inactive 이동refault가 많으면 active 유지 경향
    `LRU_INACTIVE_ANON`높음`folio_alloc_swap()` 또는 demotionswap/zswap, NUMA tiering, DMA pin 여부가 결과를 좌우
    `LRU_ACTIVE_ANON`가장 높음먼저 inactive로 강등실제 working set일 가능성이 커 공격적 회수 시 지연 증가

    페이지 LRU 회수 vs Slab Shrinker

    vmscan은 사용자/파일 페이지뿐 아니라 dentry, inode, filesystem metadata 같은 커널 캐시도 함께 줄입니다. shrink_node_memcgs()는 각 memcg의 shrink_lruvec()shrink_slab()을 호출하므로, 페이지 캐시만 줄어드는 상황과 slab까지 같이 줄어드는 상황을 구분해 봐야 합니다.

    경로대상대표 함수관찰 지표
    페이지 LRUanon/file folio`shrink_lruvec()` → `shrink_folio_list()``pgscan_*`, `pgsteal_*`, `workingset_*`
    Slab shrinkerdentry/inode 등 shrinker 등록 캐시`shrink_slab()` → `do_shrink_slab()``/proc/slabinfo`, `SReclaimable`, `KReclaimable`
    OOM 전 단계LRU와 shrinker 모두 부족`out_of_memory()`로 이동`allocstall_*` 증가 후 `dmesg` OOM 로그

    회수 우선순위(priority) 동작

    priority스캔 비율동작사용 시점
    12 (DEF_PRIORITY)total/4096소규모 스캔빠른 종료 목표
    6total/64중간 스캔점진적 강화
    0total/1전체 LRU 스캔최후 수단

    MGLRU (Multi-Gen LRU) 상세

    Linux 6.1+에서 기본 활성화된 MGLRU는 기존의 active/inactive 2-level 구조를 개선합니다:

    세대(Generation) 개념

  • max_seq: 최신 세대 번호. 새롭게 접근된 페이지가 이 세대에 속함
  • min_seq[type]: 각 타입(anon/file)별로 가장 오래된 세대. 이 세대의 페이지가 먼저 회수 대상
  • min_type(swappiness): swappiness=0이면 file만, 200이면 anon만 회수
  • MIN_NR_GENS = 2, MAX_NR_GENS = 4
  • Bloom Filter

  • PTE walk에서 spatial locality를 활용하여 non-leaf PMD 엔트리를 필터링
  • BLOOM_FILTER_SHIFT = 15 → 2^15 비트 = 32KB per filter
  • lru_gen_look_around()에서 rmap walk 결과를 feedback
  • memcg LRU 관리

  • MEMCG_NR_GENS = 4, MEMCG_NR_BINS = 8
  • memcg별 lruvec를 세대/gen/bin 단위로 분리하여 순회 효율 향상
  • lru_gen_rotate_memcg()로 memcg 우선순위 동적 조절
  • Swappiness 동작

    vm_swappiness (기본값 60)은 anon/file 회수 비율을 제어합니다:

    swappiness동작
    0파일 기반만 스캔 (익명 회수 안 함, global reclaim에서는 예외)
    1-99swappiness 비율에 따라 anon/file 비례 스캔
    100anon과 file 동일 비율 스캔
    101-199memcg swappiness가 아닌 global swappiness 사용
    200`SWAPPINESS_ANON_ONLY` — 익명만 스캔 (proactive reclaim 전용)

    memcg에서는 memory.swap.max, memory.swap.current로 개별 스왑 한도를 설정할 수 있습니다.

    회수 관련 vmstat 카운터

    카운터의미확인 명령
    `pgscan_kswapd`kswapd가 스캔한 페이지 수`cat /proc/vmstat \grep pgscan`
    `pgscan_direct`direct reclaim이 스캔한 페이지 수`cat /proc/vmstat \grep pgscan`
    `pgsteal_kswapd`kswapd가 회수한 페이지 수`cat /proc/vmstat \grep pgsteal`
    `pgsteal_direct`direct reclaim이 회수한 페이지 수`cat /proc/vmstat \grep pgsteal`
    `allocstall`direct reclaim 진입 횟수`cat /proc/vmstat \grep allocstall`
    `pgrefill`active→inactive 전환 시 refault`cat /proc/vmstat \grep pgrefill`
    `pgactivate`folio가 active로 승격된 횟수`cat /proc/vmstat \grep pgactivate`
    `pgdeactivate`folio가 inactive로 강등된 횟수`cat /proc/vmstat \grep pgdeactivate`
    `pgrotated`inactive에서 active로 회전된 횟수`cat /proc/vmstat \grep pgrotated`
    `pgscan_skip`zone 필터링으로 건너뛴 횟수`cat /proc/vmstat \grep pgscan_skip`

    증상별 진단 기준

    단일 카운터만으로 원인을 단정하면 오진하기 쉽습니다. 할당, 회수, swap, slab, compaction 지표를 같은 시간대의 PSI와 함께 맞춰 보면 direct reclaim 지연인지, kswapd가 뒤처진 것인지, working set이 RAM보다 큰 것인지 분리할 수 있습니다.

    증상우선 확인해석다음 확인
    `allocstall_*` 증가`cat /proc/vmstat \grep allocstall`할당자가 직접 회수에 들어가 latency가 늘어남`/proc/pressure/memory`, `vmstat 1`
    `pgscan_kswapd`는 큰데 `pgsteal_kswapd`가 낮음`pgscan/pgsteal` 비율스캔 효율이 낮거나 hot working set을 건드림`workingset_refault`, MGLRU 상태
    `pgscan_direct` 증가`cat /proc/vmstat \grep pgscan_direct`kswapd가 low watermark 회복을 따라가지 못함`min_free_kbytes`, `watermark_scale_factor`
    `pswpout`와 major fault 증가`cat /proc/vmstat \grep -E 'pswp\pgmajfault'`익명 working set이 RAM을 초과하거나 swappiness 영향`memory.swap.*`, zswap/zram 설정
    `SReclaimable` 급증`grep -E 'SReclaimable\KReclaimable' /proc/meminfo`page LRU보다 shrinker 캐시가 압력을 만들 수 있음`/proc/slabinfo`, shrinker tracepoint
    high-order allocation 실패`dmesg \grep -i 'page allocation failure'`free 총량보다 연속 페이지 부족 가능`/proc/buddyinfo`, compaction 카운터

    관련 문서

  • Swap / zswap — swap과 zswap의 상세 분석
  • OOM Killer — 회수 실패 시 최후 수단
  • 메모리 Compaction — 외부 단편화 해결
  • Folio / Page Cache — 파일 기반 페이지 관리
  • SLUB 할당자 — slab 캐시와 shrinker 압력 해석
  • DAMON — 접근 패턴 기반 회수 최적화
  • 메모리 관리 개요 — 전체 조감도
  • Buddy Allocator — 회수된 페이지의 최종 반환