전체 흐름
메모리 회수는 하나의 직선형 호출 체인이 아닙니다. 할당 경로에서 실행되는 direct reclaim과 node별 백그라운드 스레드 kswapd는 서로 다른 진입점에서 shrink_node()에 도달합니다. kswapd가 direct reclaim의 다음 함수라는 해석은 틀립니다.
shrink_node()에 합류호출자 문맥
kswapd 문맥
각 줄의 화살표는 해당 줄의 호출 순서입니다. 두 줄 사이에는 화살표가 없습니다. 직접 경로 · 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가 없습니다.
GitHub 원본:
대표 코드
shrink_inactive_list() 시작 부분
{
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 파일 머리말
// 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만 실행하는 것은 아닙니다.
| 조건·지점 | 호출 경로 | 따로 확인할 것 |
|---|---|---|
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 + 비-root | lru_gen_shrink_lruvec() 뒤 반환 | vmscan.c 6273–6287; classic list 스캔과 구분 |
| MGLRU가 꺼진 classic LRU | get_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의 다음 상태는 아닙니다.
| 객체·상태 | 역할 | 코드 관계 |
|---|---|---|
scan_control | 호출별 회수 목표, 우선순위, I/O·swap·unmap 허용 조건과 통계 | try_to_free_pages() 또는 balance_pgdat()에서 준비 |
lruvec | node와 memcg에 연결된 LRU 상태 | shrink_node_memcgs()가 mem_cgroup_lruvec()로 선택 |
| folio의 referenced/dirty/writeback 등 | 개별 후보의 유지·활성화·writeback·해제 판단에 쓰이는 상태 | shrink_folio_list()에서 조건별로 검사 |
slab shrinker | folio LRU와 별개의 object-cache 회수 인터페이스 | classic 경로의 shrink_slab(); MGLRU 전역 경로도 shrink_one()에서 호출 |
실행 단계와 가능한 결과
기존의 “압박 → LRU scan → writeback → free”는 모든 folio의 필수 경로가 아닙니다. 스캔해도 유지·활성화할 수 있고, writeback 중에는 건너뛰거나 기다릴 수 있으며, demotion이 가능하면 다른 node로 옮길 수도 있습니다. clean folio에는 writeback 단계가 필요하지 않습니다.
shrink_folio_list()의 대표적인 갈림길상자는 동시에 순서대로 실행하는 단계가 아니라 조건별 가능한 결과입니다. 다섯 칸으로 모든 코드 분기를 열거한 것은 아닙니다. 고정 원문 시작.
| 조건의 예 | 가능한 결과 | 고정 원문 |
|---|---|---|
| 참조됨·언맵 불가·pin 등 | keep 또는 activate; 현재 반복에서 회수하지 않음 | 1734–1753 · 1860–1871 |
| 이미 writeback 중 | activate, wait 후 재시도 등 조건에 따라 다름 | 1776–1857 |
| dirty file folio | direct reclaim은 대개 직접 writepage하지 않고 mark/activate; 허용된 조건에서만 pageout | 1972–2041 |
| demotion 가능 | 우선 다른 node로 이동 시도; 곧바로 free로 그리지 않음 | 1874–1883 |
| mapping/refcount 조건 충족 | nr_reclaimed 증가와 해제 경로 | 2088–2122 |
불변 조건과 흔한 오해
direct reclaim은 할당 경로의 호출자가 실행하고, kswapd는 별도 커널 스레드입니다. 두 경로가 shrink_node()에 합류해도 호출 문맥과 scan_control 조건은 같지 않습니다.
v6.6 shrink_node()의 lru_gen_enabled() && root_reclaim(sc)는 곧바로 MGLRU 경로로 갑니다. memcg 경로의 분기는 shrink_lruvec()에서도 확인합니다.
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행).
계측과 검증
두 진입 경로의 활동과 실제 회수량을 분리해 봅니다. try_to_free_pages()는 직접 회수 begin/end tracepoint를, kswapd()는 wake tracepoint를 사용합니다. shrink_inactive_list()는 scan과 steal 계수를 따로 올립니다.
| 관찰 | 확인 가능한 점 | 주의 |
|---|---|---|
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_*증가를 구분했는가?