Memory · Linux v6.6 · source + diagram note

reclaim

Linux v6.6의 direct reclaim과 kswapd가 shrink_node()에 합류한 뒤, LRU·MGLRU 및 slab 회수로 분기하는 경로를 원문과 대조합니다.

전체 흐름

메모리 회수는 하나의 직선형 호출 체인이 아닙니다. 할당 경로에서 실행되는 direct reclaim과 node별 백그라운드 스레드 kswapd는 서로 다른 진입점에서 shrink_node()에 도달합니다. kswapd가 direct reclaim의 다음 함수라는 해석은 틀립니다.

그림 1. Linux v6.6의 두 진입 경로가 shrink_node()에 합류
직접 회수
호출자 문맥
try_to_free_pages()할당 경로가 동기 회수 요청
do_try_to_free_pages()우선순위별 반복
shrink_zones()대상 node 선택
shrink_node()공통 node 회수 진입
백그라운드
kswapd 문맥
kswapd()node별 커널 스레드
balance_pgdat()watermark·boost 확인
kswapd_shrink_node()회수 목표 설정
shrink_node()공통 node 회수 진입

각 줄의 화살표는 해당 줄의 호출 순서입니다. 두 줄 사이에는 화살표가 없습니다. 직접 경로 · kswapd 하위 경로 · kswapd 본문.

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: mm/vmscan.c
Linux v6.6: mm/workingset.c
Linux v6.6: mm/vmscan.c (shrinker registration and slab reclaim)

설명: mm/vmscan.c에서 두 진입 경로, MGLRU 분기와 folio별 결과를 확인합니다. refault/working-set 판단은 workingset.c를, slab shrinker 등록과 호출은 이 버전의 vmscan.c를 확인합니다. v6.6에는 mm/shrinker.c가 없습니다.

대표 코드

shrink_inactive_list() 시작 부분

원본 코드: mm/vmscan.c, 2560–2599행 일부
{
	return !(current->flags & PF_LOCAL_THROTTLE);
}

/*
 * shrink_inactive_list() is a helper for shrink_node().  It returns the number
 * of reclaimed pages
 */
static unsigned long shrink_inactive_list(unsigned long nr_to_scan,
		struct lruvec *lruvec, struct scan_control *sc,
		enum lru_list lru)
{
	LIST_HEAD(folio_list);
	unsigned long nr_scanned;
	unsigned int nr_reclaimed = 0;
	unsigned long nr_taken;
	struct reclaim_stat stat;
	bool file = is_file_lru(lru);
	enum vm_event_item item;
	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
	bool stalled = false;

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

		/* We are about to die and free our memory. Return now. */
		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,

읽는 법: 발췌 첫 세 줄은 앞선 helper의 끝이고, 이후는 shrink_inactive_list()의 시작입니다. shrink_node() 본문 자체가 아닙니다. 격리량 제한과 lru_lock 아래 folio 격리를 보여주며, 이 발췌는 호출과 반환 전에 끝납니다. v6.6 원문 이어 읽기.

mm/vmscan.c 파일 머리말

원본 코드: mm/vmscan.c, 1행 부근
// SPDX-License-Identifier: GPL-2.0
/*
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, Stephen Tweedie.
 *  kswapd added: 7.1.96  sct
 *  Removed kswapd_ctl limits, and swap out as many pages as needed
 *  to bring the system back to freepages.high: 2.4.97, Rik van Riel.
 *  Zone aware kswapd started 02/00, Kanoj Sarcar ([email protected]).
 *  Multiqueue VM started 5.8.00, Rik van Riel.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/mm.h>
#include <linux/sched/mm.h>
#include <linux/module.h>
#include <linux/gfp.h>
#include <linux/kernel_stat.h>
#include <linux/swap.h>
#include <linux/pagemap.h>
#include <linux/init.h>
#include <linux/highmem.h>
#include <linux/vmpressure.h>
#include <linux/vmstat.h>
#include <linux/file.h>
#include <linux/writeback.h>
#include <linux/blkdev.h>
#include <linux/buffer_head.h>	/* for buffer_heads_over_limit */
#include <linux/mm_inline.h>
#include <linux/backing-dev.h>
#include <linux/rmap.h>
#include <linux/topology.h>
#include <linux/cpu.h>
#include <linux/cpuset.h>
#include <linux/compaction.h>
#include <linux/notifier.h>
#include <linux/rwsem.h>
#include <linux/delay.h>
#include <linux/kthread.h>

읽는 법: 이 발췌는 저작권 머리말과 #include 목록이며 kswapd() 함수 본문이 아닙니다. 실제 kswapd()는 7713행부터 시작해 balance_pgdat()를 호출합니다. 원문 코드 조각은 그대로 둡니다.

함수별 분석

shrink_node()는 진입점을 선택하는 함수가 아니라 두 진입 경로가 호출하는 node 회수 함수입니다. 실제 분기는 lru_gen_enabled(), root_reclaim(sc), 그리고 shrink_lruvec() 안에서 다시 갈립니다. 모든 v6.6 빌드가 같은 classic active/inactive LRU만 실행하는 것은 아닙니다.

표 1. shrink_node() 이후의 v6.6 분기
조건·지점호출 경로따로 확인할 것
lru_gen_enabled() && root_reclaim(sc)lru_gen_shrink_node()로 곧바로 반환vmscan.c 6519–6528; 전역 MGLRU 경로
그 외 shrink_node()shrink_node_memcgs() → 각 lruvec의 shrink_lruvec()와 shrink_slab()vmscan.c 6461–6516; memcg 보호 조건 포함
shrink_lruvec()에서 MGLRU + 비-rootlru_gen_shrink_lruvec() 뒤 반환vmscan.c 6273–6287; classic list 스캔과 구분
MGLRU가 꺼진 classic LRUget_scan_count() → shrink_list() → 비활성 list의 shrink_inactive_list()vmscan.c 6289–6321 · 2568–2614

scan_control은 회수 목표·허용 가능한 I/O/언맵·우선순위와 누적 계수 등을 전달합니다. direct reclaim은 호출자가 회수 일을 수행하고 gfp_mask에 따라 지연·I/O 가능성이 달라집니다. kswapd의 balance_pgdat()는 watermark가 충족되면 회수를 건너뛸 수도 있습니다.

구조체와 필드

lruvec → scan_control → folio flags → shrinker는 호출 순서가 아닙니다. 회수 문맥, 대상 LRU, folio 상태, slab 회수 인터페이스가 각각 다른 역할을 맡습니다. 특히 shrinker가 folio flag의 다음 상태는 아닙니다.

표 2. 순서가 아니라 역할로 읽는 회수 객체
객체·상태역할코드 관계
scan_control호출별 회수 목표, 우선순위, I/O·swap·unmap 허용 조건과 통계try_to_free_pages() 또는 balance_pgdat()에서 준비
lruvecnode와 memcg에 연결된 LRU 상태shrink_node_memcgs()가 mem_cgroup_lruvec()로 선택
folio의 referenced/dirty/writeback 등개별 후보의 유지·활성화·writeback·해제 판단에 쓰이는 상태shrink_folio_list()에서 조건별로 검사
slab shrinkerfolio LRU와 별개의 object-cache 회수 인터페이스classic 경로의 shrink_slab(); MGLRU 전역 경로도 shrink_one()에서 호출

lruvec 선택과 classic slab 회수 · MGLRU 전역 경로의 slab 회수.

실행 단계와 가능한 결과

기존의 “압박 → LRU scan → writeback → free”는 모든 folio의 필수 경로가 아닙니다. 스캔해도 유지·활성화할 수 있고, writeback 중에는 건너뛰거나 기다릴 수 있으며, demotion이 가능하면 다른 node로 옮길 수도 있습니다. clean folio에는 writeback 단계가 필요하지 않습니다.

그림 2. shrink_folio_list()의 대표적인 갈림길
shrink_folio_list()격리한 folio를 조건별로 검사
유지·활성화참조됨, 언맵 불가, pinned 등
writeback 대기·재시도dirty/writeback과 허용 조건에 따라 보류
demote가능한 경우 다른 NUMA node로 이동
회수mapping 제거·참조 조건 충족 후 page 반환

상자는 동시에 순서대로 실행하는 단계가 아니라 조건별 가능한 결과입니다. 다섯 칸으로 모든 코드 분기를 열거한 것은 아닙니다. 고정 원문 시작.

표 3. folio 한 개가 반드시 free까지 가는 것은 아님
조건의 예가능한 결과고정 원문
참조됨·언맵 불가·pin 등keep 또는 activate; 현재 반복에서 회수하지 않음1734–1753 · 1860–1871
이미 writeback 중activate, wait 후 재시도 등 조건에 따라 다름1776–1857
dirty file foliodirect reclaim은 대개 직접 writepage하지 않고 mark/activate; 허용된 조건에서만 pageout1972–2041
demotion 가능우선 다른 node로 이동 시도; 곧바로 free로 그리지 않음1874–1883
mapping/refcount 조건 충족nr_reclaimed 증가와 해제 경로2088–2122

불변 조건과 흔한 오해

진입 문맥

direct reclaim은 할당 경로의 호출자가 실행하고, kswapd는 별도 커널 스레드입니다. 두 경로가 shrink_node()에 합류해도 호출 문맥과 scan_control 조건은 같지 않습니다.

MGLRU 분기

v6.6 shrink_node()의 lru_gen_enabled() && root_reclaim(sc)는 곧바로 MGLRU 경로로 갑니다. memcg 경로의 분기는 shrink_lruvec()에서도 확인합니다.

dirty·writeback

dirty folio가 항상 즉시 free되거나 direct reclaim에서 writepage되는 것은 아닙니다. may_writepage, may_enter_fs(), kswapd 문맥과 기존 writeback 상태를 확인합니다.

회수량

nr_scanned와 nr_reclaimed는 다릅니다. shrink_folio_list()의 회수량에는 다른 node로 강등한 양도 포함됩니다(2154–2155행). try_to_free_pages()가 0을 반환할 수 있고, kswapd는 watermark 충족 시 회수를 건너뛸 수 있습니다. 또한 throttle 중 fatal signal을 받은 경로의 반환값 1은 OOM 처리를 피하기 위한 값이지, 한 페이지 해제의 증거가 아닙니다(7065–7071행).

MGLRU 분기 · direct 반환·계측 · kswapd 균형 판단.

계측과 검증

두 진입 경로의 활동과 실제 회수량을 분리해 봅니다. try_to_free_pages()는 직접 회수 begin/end tracepoint를, kswapd()는 wake tracepoint를 사용합니다. shrink_inactive_list()는 scan과 steal 계수를 따로 올립니다.

표 4. 먼저 구분할 관찰값
관찰확인 가능한 점주의
mm_vmscan_direct_reclaim_begin/end할당 경로의 직접 회수 시작/끝과 반환 회수량begin만으로 실제 free를 단정하지 않음
mm_vmscan_kswapd_wake/sleep백그라운드 스레드의 깨어남·잠듦wake만으로 folio 회수를 단정하지 않음
/proc/vmstat의 pgscan_* / pgsteal_*스캔과 회수 계수 비교MGLRU/classic 및 memcg/global 문맥과 함께 해석
PSI memory메모리 stall 시간어느 folio가 회수됐는지 직접 식별하지 않음

대상 커널의 available_events, 이미 수집 중인 trace, /proc/vmstat를 읽어 두 진입 경로를 구분합니다. 이 예제는 전역 tracefs 설정을 변경하지 않습니다. scan/steal 계수 · 직접 회수 trace · kswapd trace.

추가 확인 사항

  • 관찰한 회수는 할당 호출자의 direct reclaim인가, node의 kswapd인가?
  • 해당 빌드에서 MGLRU가 켜져 있는가? global과 memcg 회수의 분기를 각각 확인했는가?
  • folio가 회수되지 않았을 때 keep·activate·writeback 대기·demotion 중 어떤 조건에 걸렸는가?
  • pgscan_* 증가와 pgsteal_* 증가를 구분했는가?