Linux 7.0에서 개선된 Multi-Gen LRU(MGLRU) 메커니즘 분석
Multi-Gen LRU(MGLRU)는 기존 Active/Inactive 2개 리스트 기반 페이지 회수 메커니즘을 대폭 개선한 LRU 알고리즘입니다. Linux 5.18에서 Yu Zhao가 도입하였으며, Linux 7.0에서 성능과 정확도 면에서 지속적인 개선이 이루어졌습니다.
기존 LRU는 Active/Inactive 2개의 리스트만으로 페이지의 "뜨거움/차가움"을 판단했으므로, 대용량 메모리 시스템에서 정확도가 떨어지는 문제가 있었습니다. MGLRU는 여러 "세대(generation)"으로 구분하여 관리함으로써, 페이지 접근 시간에 기반한 정밀한 분류를 가능케 합니다. 또한 Bloom Filter와 PTE 워크를 활용하여 에이징 비용을 최소화하고, memcg 단위의 공정한 회수를 보장합니다.
소스 파일: mm/vmscan.c, include/linux/mm_inline.h, include/linux/mmzone.h, include/linux/mm_types.h
# MGLRU 활성화 상태 확인
cat /sys/kernel/mm/lru_gen/enabled
# MGLRU 디버그 정보 (세대별 페이지 수, Bloom Filter 상태 등)
cat /sys/kernel/debug/lru_gen
# 세대별 통계 상세 출력
cat /sys/kernel/debug/lru_gen | head -20
# MGLRU 관련 커널 파라미터
cat /proc/cmdline | grep -o 'lru_gen[^ ]*'
# VM 통계에서 MGLRU 관련 카운터 확인
grep -E 'pgscan_kswapd|pgsteal_kswapd|pgscan_direct|pgsteal_direct|workingset_refault|workingset_activate' /proc/vmstat
# memcg별 MGLRU 상태 확인
cat /sys/fs/cgroup/memory/*.lru_gen 2>/dev/null
# working set 보호 시간 확인
cat /sys/kernel/mm/lru_gen/min_ttl_ms
# working set 보호 시간 설정 (밀리초)
echo 1000 > /sys/kernel/mm/lru_gen/min_ttl_ms
# MGLRU 비활성화 (커널 파라미터)
# boot: lru_gen=0 또는 sysfs: echo 0x0000 > /sys/kernel/mm/lru_gen/enabled
# 기존 LRU vs MGLRU 전환 상태 확인
dmesg | grep -i "multi-gen"
// include/linux/mmzone.h:490-518
struct lru_gen_folio {
/* 에이징이 가장 젊은 세대 번호 (anon/file 공유) */
unsigned long max_seq;
/* 회수 대상 가장 오래된 세대 번호 (anon/file 별도) */
unsigned long min_seq[ANON_AND_FILE];
/* 각 세대의 생성 시점 (jiffies) */
unsigned long timestamps[MAX_NR_GENS];
/* 세대별 folio 리스트: folios[gen][type][zone] */
struct list_head folios[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
/* 세대별 페이지 수 (eventually consistent) */
long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
/* refault된 folio의 지수이동평균 */
unsigned long avg_refaulted[ANON_AND_FILE][MAX_NR_TIERS];
/* evicted+protected의 지수이동평균 */
unsigned long avg_total[ANON_AND_FILE][MAX_NR_TIERS];
/* 보호된 folio 수 (LRU lock 하에서만 수정) */
unsigned long protected[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
/* 회수된 folio 수 (lock 없이 수정 가능) */
atomic_long_t evicted[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
/* refault 카운트 */
atomic_long_t refaulted[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
/* MGLRU 활성화 여부 */
bool enabled;
/* 이 lru_gen_folio가 속한 memcg 세대 */
u8 gen;
/* 이 lru_gen_folio가 속한 리스트 세그먼트 */
u8 seg;
/* 글로벌 회수용 노드별 lru_gen_folio 리스트 */
struct hlist_nulls_node list;
};
MIN_NR_GENS = 2: "second chance"를 보장하기 위한 최소 세대 수 (folio가 fault 후 최소 2번의 에이징을 거침)MAX_NR_GENS = 4: active/inactive LRU의 2배 카테고리를 지원 (folio->flags에서 order_base_2(5)=3비트 사용)MAX_NR_TIERS = 4: 파일 디스크립터 기반 접근 횟수를 분류하는 단계 수// include/linux/mmzone.h:531-542
struct lru_gen_mm_state {
/* 현재 반복이 동기화된 시퀀스 번호 */
unsigned long seq;
/* 현재 반복이 이어갈 위치 */
struct list_head *head;
/* 마지막 반복이 종료된 위치 */
struct list_head *tail;
/* Bloom 필터 (더블 버퍼링) */
unsigned long *filters[NR_BLOOM_FILTERS];
/* 디버깅용 mm 통계 */
unsigned long stats[NR_HIST_GENS][NR_MM_STATS];
};
// include/linux/mmzone.h:544-559
struct lru_gen_mm_walk {
/* 회수 중인 lruvec */
struct lruvec *lruvec;
/* 현재 시퀀스 (오래된 값일 수 있음) */
unsigned long seq;
/* 다음 스캔할 주소 */
unsigned long next_addr;
/* 배치된 페이지 수 */
int nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
/* 배치된 mm 통계 */
int mm_stats[NR_MM_STATS];
/* 총 배치된 항목 수 */
int batched;
int swappiness;
bool force_scan;
};
// include/linux/mmzone.h:606-615
struct lru_gen_memcg {
/* 노드별 memcg 세대 카운터 */
unsigned long seq;
/* memcg 수: nr_memcgs[MEMCG_NR_GENS] */
unsigned long nr_memcgs[MEMCG_NR_GENS];
/* memcg별 fifo 리스트: fifo[gen][bin] */
struct hlist_nulls_head fifo[MEMCG_NR_GENS][MEMCG_NR_BINS];
/* 보호용 락 */
spinlock_t lock;
};
MEMCG_NR_GENS = 3: 두 개의 유효 세대(old/young) + stale 값 방지용MEMCG_NR_BINS = 8: memcg를 무작위로 분산하여 확장성 확보// include/linux/mmzone.h:528-529
#define NR_BLOOM_FILTERS 2
#define BLOOM_FILTER_SHIFT 15 // mm/vmscan.c:2790
// mm/vmscan.c:2807-2821 — Bloom Filter 테스트
static bool test_bloom_filter(struct lru_gen_mm_state *mm_state,
unsigned long seq, void *item)
{
int key[2];
unsigned long *filter;
int gen = filter_gen_from_seq(seq);
filter = READ_ONCE(mm_state->filters[gen]);
if (!filter)
return true;
get_item_key(item, key);
return test_bit(key[0], filter) && test_bit(key[1], filter);
}
// include/linux/mmzone.h:425-448
#define LRU_GEN_MASK ((BIT(LRU_GEN_WIDTH) - 1) << LRU_GEN_PGOFF)
#define LRU_REFS_MASK ((BIT(LRU_REFS_WIDTH) - 1) << LRU_REFS_PGOFF)
#define LRU_REFS_FLAGS (LRU_REFS_MASK | BIT(PG_referenced))
LRU_GEN_MASK: folio->flags에서 세대 번호를 저장 (gen+1 형태)LRU_REFS_MASK: 파일 디스크립터 기반 접근 횟수 저장// mm/vmscan.c:4154-4188
static void lru_gen_age_node(struct pglist_data *pgdat,
struct scan_control *sc)
{
struct mem_cgroup *memcg;
unsigned long min_ttl = READ_ONCE(lru_gen_min_ttl);
bool reclaimable = !min_ttl;
VM_WARN_ON_ONCE(!current_is_kswapd());
set_initial_priority(pgdat, sc);
memcg = mem_cgroup_iter(NULL, NULL, NULL);
do {
struct lruvec *lruvec = mem_cgroup_lruvec(memcg, pgdat);
mem_cgroup_calculate_protection(NULL, memcg);
if (!reclaimable)
reclaimable = lruvec_is_reclaimable(lruvec, sc, min_ttl);
} while ((memcg = mem_cgroup_iter(NULL, memcg, NULL)));
/* 모든 memcg의 세대가 min_ttl보다 젊으면 OOM 실행 */
if (!reclaimable && mutex_trylock(&oom_lock)) {
struct oom_control oc = { .gfp_mask = sc->gfp_mask };
out_of_memory(&oc);
mutex_unlock(&oom_lock);
}
}
lru_gen_min_ttl: 보호할 작업 집합의 최소 시간 (jiffies)// mm/vmscan.c:3899-3954
static bool try_to_inc_min_seq(struct lruvec *lruvec, int swappiness)
{
int gen, type, zone;
bool success = false;
bool seq_inc_flag = false;
struct lru_gen_folio *lrugen = &lruvec->lrugen;
DEFINE_MIN_SEQ(lruvec);
/* 가장 오래진 빈 세대를 찾아 min_seq 증가 */
for_each_evictable_type(type, swappiness) {
while (min_seq[type] + MIN_NR_GENS <= lrugen->max_seq) {
gen = lru_gen_from_seq(min_seq[type]);
for (zone = 0; zone < MAX_NR_ZONES; zone++) {
if (!list_empty(&lrugen->folios[gen][type][zone]))
goto next;
}
min_seq[type]++;
seq_inc_flag = true;
}
next: ;
}
if (!seq_inc_flag)
return success;
/* anon/file 간 동기화 유지 */
if (swappiness && swappiness <= MAX_SWAPPINESS) {
unsigned long seq = lrugen->max_seq - MIN_NR_GENS;
if (min_seq[LRU_GEN_ANON] > seq && min_seq[LRU_GEN_FILE] < seq)
min_seq[LRU_GEN_ANON] = seq;
else if (min_seq[LRU_GEN_FILE] > seq && min_seq[LRU_GEN_ANON] < seq)
min_seq[LRU_GEN_FILE] = seq;
}
for_each_evictable_type(type, swappiness) {
if (min_seq[type] <= lrugen->min_seq[type])
continue;
reset_ctrl_pos(lruvec, type, true);
WRITE_ONCE(lrugen->min_seq[type], min_seq[type]);
success = true;
}
return success;
}
// mm/vmscan.c:3956-4019
static bool inc_max_seq(struct lruvec *lruvec, unsigned long seq,
int swappiness)
{
bool success;
int prev, next;
int type, zone;
struct lru_gen_folio *lrugen = &lruvec->lrugen;
restart:
if (seq < READ_ONCE(lrugen->max_seq))
return false;
spin_lock_irq(&lruvec->lru_lock);
success = seq == lrugen->max_seq;
if (!success)
goto unlock;
/* MAX_NR_GENS에 도달한 경우 min_seq 먼저 증가 */
for (type = 0; type < ANON_AND_FILE; type++) {
if (get_nr_gens(lruvec, type) != MAX_NR_GENS)
continue;
if (inc_min_seq(lruvec, type, swappiness))
continue;
spin_unlock_irq(&lruvec->lru_lock);
cond_resched();
goto restart;
}
/* active/inactive LRU 크기 업데이트 (호환성) */
prev = lru_gen_from_seq(lrugen->max_seq - 1);
next = lru_gen_from_seq(lrugen->max_seq + 1);
for (type = 0; type < ANON_AND_FILE; type++) {
for (zone = 0; zone < MAX_NR_ZONES; zone++) {
enum lru_list lru = type * LRU_INACTIVE_FILE;
long delta = lrugen->nr_pages[prev][type][zone] -
lrugen->nr_pages[next][type][zone];
if (!delta) continue;
__update_lru_size(lruvec, lru, zone, delta);
__update_lru_size(lruvec, lru + LRU_ACTIVE, zone, -delta);
}
}
for (type = 0; type < ANON_AND_FILE; type++)
reset_ctrl_pos(lruvec, type, false);
WRITE_ONCE(lrugen->timestamps[next], jiffies);
smp_store_release(&lrugen->max_seq, lrugen->max_seq + 1);
unlock:
spin_unlock_irq(&lruvec->lru_lock);
return success;
}
smp_store_release: 메모리 배리어로 이전 수정사항이 보장되도록 함// mm/vmscan.c:4021-4073
static bool try_to_inc_max_seq(struct lruvec *lruvec, unsigned long seq,
int swappiness, bool force_scan)
{
bool success;
struct lru_gen_mm_walk *walk;
struct mm_struct *mm = NULL;
struct lru_gen_folio *lrugen = &lruvec->lrugen;
struct lru_gen_mm_state *mm_state = get_mm_state(lruvec);
if (!mm_state)
return inc_max_seq(lruvec, seq, swappiness);
if (seq <= READ_ONCE(mm_state->seq))
return false;
/* MMU에 하드웨어 accessed 비트가 없으면 폴백 */
if (!should_walk_mmu()) {
success = iterate_mm_list_nowalk(lruvec, seq);
goto done;
}
walk = set_mm_walk(NULL, true);
if (!walk) {
success = iterate_mm_list_nowalk(lruvec, seq);
goto done;
}
walk->lruvec = lruvec;
walk->seq = seq;
walk->swappiness = swappiness;
walk->force_scan = force_scan;
do {
success = iterate_mm_list(walk, &mm);
if (mm)
walk_mm(mm, walk);
} while (mm);
done:
if (success) {
success = inc_max_seq(lruvec, seq, swappiness);
WARN_ON_ONCE(!success);
}
return success;
}
should_walk_mmu(): arch_has_hw_pte_young() && LRU_GEN_MM_WALK cap 확인// mm/vmscan.c:4686-4775
static int evict_folios(unsigned long nr_to_scan, struct lruvec *lruvec,
struct scan_control *sc, int swappiness)
{
int type, scanned, reclaimed;
LIST_HEAD(list), LIST_HEAD(clean);
struct folio *folio, *next;
struct reclaim_stat stat;
struct lru_gen_folio *lrugen = &lruvec->lrugen;
spin_lock_irq(&lruvec->lru_lock);
scanned = isolate_folios(nr_to_scan, lruvec, sc, swappiness, &type, &list);
scanned += try_to_inc_min_seq(lruvec, swappiness);
if (evictable_min_seq(lrugen->min_seq, swappiness) + MIN_NR_GENS >
lrugen->max_seq)
scanned = 0;
spin_unlock_irq(&lruvec->lru_lock);
if (list_empty(&list))
return scanned;
retry:
reclaimed = shrink_folio_list(&list, pgdat, sc, &stat, false, memcg);
/* 회수 실패한 folio 처리 */
list_for_each_entry_safe_reverse(folio, next, &list, lru) {
if (!folio_evictable(folio)) {
list_del(&folio->lru);
folio_putback_lru(folio);
continue;
}
/* clean folio는 재시도 */
if (!skip_retry && !folio_test_active(folio) && !folio_mapped(folio) &&
!folio_test_dirty(folio) && !folio_test_writeback(folio)) {
list_move(&folio->lru, &clean);
continue;
}
/* 회수 실패 folio를 가장 오래진 세대에 추가하지 않음 */
if (lru_gen_folio_seq(lruvec, folio, false) == min_seq[type])
set_mask_bits(&folio->flags.f, LRU_REFS_FLAGS, BIT(PG_active));
}
/* ... 나머지 처리 ... */
list_splice_init(&clean, &list);
if (!list_empty(&list)) {
skip_retry = true;
goto retry;
}
return scanned;
}
isolate_folios(): 가장 오래진 세대에서 회수 대상 folio 격리shrink_folio_list(): 실제로 folio를 회수 (unmap, writeback 등)// mm/vmscan.c:4201-4295
bool lru_gen_look_around(struct page_vma_mapped_walk *pvmw)
{
int i, young = 1;
bool dirty;
unsigned long start, end;
struct lru_gen_mm_walk *walk;
struct folio *last = NULL;
pte_t *pte = pvmw->pte;
unsigned long addr = pvmw->address;
struct vm_area_struct *vma = pvmw->vma;
struct folio *folio = pfn_folio(pvmw->pfn);
lockdep_assert_held(pvmw->ptl);
if (!ptep_clear_young_notify(vma, addr, pte))
return false;
if (spin_is_contended(pvmw->ptl))
return true;
/* 현재 folio의 주변 PTE를 스캔하여 접근 패턴 파악 */
start = max(addr & PMD_MASK, vma->vm_start);
end = min(addr | ~PMD_MASK, vma->vm_end - 1) + 1;
/* 스캔 범위 제한 */
if (end - start > MIN_LRU_BATCH * PAGE_SIZE) {
if (addr - start < MIN_LRU_BATCH * PAGE_SIZE / 2)
end = start + MIN_LRU_BATCH * PAGE_SIZE;
else if (end - addr < MIN_LRU_BATCH * PAGE_SIZE / 2)
start = end - MIN_LRU_BATCH * PAGE_SIZE;
else {
start = addr - MIN_LRU_BATCH * PAGE_SIZE / 2;
end = addr + MIN_LRU_BATCH * PAGE_SIZE / 2;
}
}
lazy_mmu_mode_enable();
pte -= (addr - start) / PAGE_SIZE;
for (i = 0, addr = start; addr != end; i++, addr += PAGE_SIZE) {
unsigned long pfn;
pte_t ptent = ptep_get(pte + i);
pfn = get_pte_pfn(ptent, vma, addr, pgdat);
if (pfn == -1) continue;
folio = get_pfn_folio(pfn, memcg, pgdat);
if (!folio) continue;
if (!ptep_clear_young_notify(vma, addr, pte + i))
continue;
if (last != folio) {
walk_update_folio(walk, last, gen, dirty);
last = folio;
dirty = false;
}
if (pte_dirty(ptent))
dirty = true;
young++;
}
walk_update_folio(walk, last, gen, dirty);
lazy_mmu_mode_disable();
/* rmap walker → page table walker 피드백: Bloom Filter 업데이트 */
if (mm_state && suitable_to_scan(i, young))
update_bloom_filter(mm_state, max_seq, pvmw->pmd);
return true;
}
shrink_folio_list()가 rmap을 워크할 때 호출됨// mm/vmscan.c:3660-3731
static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
struct mm_walk *args)
{
int i;
pmd_t *pmd;
unsigned long next, addr;
struct vm_area_struct *vma;
DECLARE_BITMAP(bitmap, MIN_LRU_BATCH);
struct lru_gen_mm_walk *walk = args->private;
struct lru_gen_mm_state *mm_state = get_mm_state(walk->lruvec);
pmd = pmd_offset(pud, start & PUD_MASK);
restart:
vma = args->vma;
for (i = pmd_index(start), addr = start; addr != end; i++, addr = next) {
pmd_t val = pmdp_get_lockless(pmd + i);
next = pmd_addr_end(addr, end);
if (!pmd_present(val) || is_huge_zero_pmd(val)) {
walk->mm_stats[MM_LEAF_TOTAL]++;
continue;
}
if (pmd_trans_huge(val)) {
/* THP 처리 */
walk->mm_stats[MM_LEAF_TOTAL]++;
if (pfn != -1)
walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first);
continue;
}
/* 하드웨어 accessed 비트가 없으면 PMD young 비트로 필터링 */
if (!walk->force_scan && should_clear_pmd_young() &&
!mm_has_notifiers(args->mm)) {
if (!pmd_young(val))
continue;
walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first);
}
/* Bloom Filter로 이미 스캔된 PMD 스킵 */
if (!walk->force_scan && !test_bloom_filter(mm_state, walk->seq, pmd + i))
continue;
walk->mm_stats[MM_NONLEAF_FOUND]++;
if (!walk_pte_range(&val, addr, next, args))
continue;
walk->mm_stats[MM_NONLEAF_ADDED]++;
/* 다음 세대를 위해 Bloom Filter에 유지 */
update_bloom_filter(mm_state, walk->seq + 1, pmd + i);
}
}
pmd_young() 필터링으로 접근되지 않은 PMD 스킵// include/linux/mm_inline.h:254-304
static inline bool lru_gen_add_folio(struct lruvec *lruvec,
struct folio *folio, bool reclaiming)
{
unsigned long seq;
unsigned long flags;
int gen = folio_lru_gen(folio);
int type = folio_is_file_lru(folio);
int zone = folio_zonenum(folio);
struct lru_gen_folio *lrugen = &lruvec->lrugen;
if (folio_test_unevictable(folio) || !lrugen->enabled)
return false;
seq = lru_gen_folio_seq(lruvec, folio, reclaiming);
gen = lru_gen_from_seq(seq);
flags = (gen + 1UL) << LRU_GEN_PGOFF;
set_mask_bits(&folio->flags.f, LRU_GEN_MASK | BIT(PG_active), flags);
lru_gen_update_size(lruvec, folio, -1, gen);
if (reclaiming)
list_add_tail(&folio->lru, &lrugen->folios[gen][type][zone]);
else
list_add(&folio->lru, &lrugen->folios[gen][type][zone]);
return true;
}
static inline bool lru_gen_del_folio(struct lruvec *lruvec,
struct folio *folio, bool reclaiming)
{
unsigned long flags;
int gen = folio_lru_gen(folio);
if (gen < 0)
return false;
flags = !reclaiming && lru_gen_is_active(lruvec, gen) ?
BIT(PG_active) : 0;
flags = set_mask_bits(&folio->flags.f, LRU_GEN_MASK, flags);
gen = ((flags & LRU_GEN_MASK) >> LRU_GEN_PGOFF) - 1;
lru_gen_update_size(lruvec, folio, gen, -1);
list_del(&folio->lru);
return true;
}
folio_lru_gen(): folio->flags에서 세대 번호 추출 (gen+1 형태이므로 -1 필요)lru_gen_is_active(): max_seq 또는 max_seq-1 세대에 속한 folio는 "활성"으로 분류// mm/vmscan.c:5222-5238
static ssize_t min_ttl_ms_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
return sysfs_emit(buf, "%u\n", jiffies_to_msecs(READ_ONCE(lru_gen_min_ttl)));
}
/* 워크셋 보호 시간을 밀리초 단위로 저장한다. */
static ssize_t min_ttl_ms_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t len)
{
unsigned int msecs;
if (kstrtouint(buf, 0, &msecs))
return -EINVAL;
WRITE_ONCE(lru_gen_min_ttl, msecs_to_jiffies(msecs));
return len;
}
// mm/vmscan.c:5243-5284
static ssize_t enabled_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
unsigned int caps = 0;
if (get_cap(LRU_GEN_CORE))
caps |= BIT(LRU_GEN_CORE);
if (should_walk_mmu())
caps |= BIT(LRU_GEN_MM_WALK);
if (should_clear_pmd_young())
caps |= BIT(LRU_GEN_NONLEAF_YOUNG);
return sysfs_emit(buf, "0x%04x\n", caps);
}
/* y/n 또는 비트마스크 값을 받아 각 기능을 켜거나 끈다. */
static ssize_t enabled_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t len)
{
int i;
unsigned int caps;
if (tolower(*buf) == 'n')
caps = 0;
else if (tolower(*buf) == 'y')
caps = -1;
else if (kstrtouint(buf, 0, &caps))
return -EINVAL;
for (i = 0; i < NR_LRU_GEN_CAPS; i++) {
bool enabled = caps & BIT(i);
if (i == LRU_GEN_CORE)
lru_gen_change_state(enabled);
else if (enabled)
static_branch_enable(&lru_gen_caps[i]);
else
static_branch_disable(&lru_gen_caps[i]);
}
return len;
}
min_ttl_ms: 마지막 N ms 동안의 working set을 보호enabled: 0x0000 형식 비트마스크로 LRU_GEN_CORE, LRU_GEN_MM_WALK, LRU_GEN_NONLEAF_YOUNG을 제어kswapd_ksoftirqd()
└── balance_pgdat()
└── kswapd_shrink_node()
└── lru_gen_age_node() // mm/vmscan.c:4154
├── set_initial_priority() // 초기 우선순위 결정
├── mem_cgroup_iter() // 모든 memcg 순회
│ └── lruvec_is_reclaimable() // 회수 가능 여부 확인
└── out_of_memory() // min_ttl 초과 시 OOM
try_to_inc_max_seq() // mm/vmscan.c:4021
├── iterate_mm_list() // MM 리스트 순회
│ └── walk_mm() // mm/vmscan.c:3775
│ └── walk_pud_range()
│ └── walk_pmd_range() // mm/vmscan.c:3660
│ ├── test_bloom_filter() // Bloom Filter로 스킵 판단
│ ├── walk_pte_range() // PTE 스캔
│ │ └── folio_update_gen() // 세대 승격
│ └── update_bloom_filter() // 다음 세대용 등록
└── inc_max_seq() // mm/vmscan.c:3956
├── inc_min_seq() // 필요 시 최소 세대 증가
└── smp_store_release(&max_seq) // 새 세대 생성
shrink_node()
└── shrink_lruvec()
└── lru_gen_shrink_node() // mm/vmscan.c:5038
├── set_mm_walk()
├── shrink_one() / shrink_many()
│ └── try_to_shrink_lruvec() // mm/vmscan.c:4868
│ └── while loop:
│ ├── get_nr_to_scan() // mm/vmscan.c:4811
│ │ ├── should_run_aging() // 에이징 필요 판단
│ │ └── try_to_inc_max_seq() // 에이징 실행
│ └── evict_folios() // mm/vmscan.c:4686
│ ├── isolate_folios() // 세대에서 folio 격리
│ ├── try_to_inc_min_seq() // 빈 세대 정리
│ └── shrink_folio_list() // 실제 회수
└── clear_mm_walk()
shrink_folio_list()
└── try_to_unmap()
└── rmap_walk()
└── rmap_walk_anon() / rmap_walk_file()
└── lru_gen_look_around() // mm/vmscan.c:4201
├── ptep_clear_young_notify() // 현재 PTE accessed 비트 클리어
├── 주변 PTE 스캔 (PMD 범위)
│ └── ptep_clear_young_notify() // 주변 PTE accessed 확인
└── update_bloom_filter() // PMD를 Bloom Filter에 추가
(→ walk_pmd_range에서 test_bloom_filter로 활용)
| 비교 항목 | 기존 LRU (Active/Inactive) | MGLRU |
|---|---|---|
| **리스트 구조** | 2개 (Active, Inactive) | 4세대 × 2타입 × N존 |
| **에이징 메커니즘** | PG_referenced 비트만 | PTE accessed + Bloom Filter + rmap look_around |
| **second chance** | 1번만 확인 | MIN_NR_GENS=2로 최소 2번 확인 |
| **파일 디스크립터 접근** | 구분 없음 | MAX_NR_TIERS=4로 다단계 분류 |
| **대용량 메모리** | 리스트 길이 비례 비용 | 세대 수 고정 (4), 스캔 비용 감소 |
| **memcg 공정성** | mem_cgroup_iter 순차 순회 | 2세대 × 8빈 memcg LRU |
| **Active/Inactive 호환** | 직접 매핑 | lru_gen_is_active()로 시뮬레이션 |
| **Bloom Filter** | 없음 | PMD 기반 false positive 최소화 스캔 |
| 조건 | 동작 | 코드 참조 |
|---|---|---|
| **폴트 시 (page table access)** | MIN_NR_GENS ~ MAX_NR_GENS 범위에 배치 | `lru_gen_folio_seq()` mm_inline.h:220 |
| **파일 디스크립터 1회 접근** | PG_referenced 설정, tier 0→1 | `lru_gen_inc_refs()` mm_inline.h:136 |
| **파일 디스크립터 N회 접근** | LRU_REFS_WIDTH 비트 설정 | `lru_tier_from_refs()` mm_inline.h:136 |
| **PG_workingset 설정** | 최대 세대에 가까운 곳으로 승격 | `folio_inc_gen()` |
| **에이징 완료 (inc_max_seq)** | 새 세대 생성, 이전 세대는 inactive 분류 | `inc_max_seq()` vmscan.c:3956 |
| **회수 실패** | 가장 오래진 세대에 재배치 (PG_active 설정) | `evict_folios()` vmscan.c:4742 |
| 이벤트 | memcg 이동 | seg 업데이트 |
|---|---|---|
| soft limit 초과 | HEAD (현재 세대 머리) | `MEMCG_LRU_HEAD` |
| low threshold 첫 시도 | TAIL (현재 세대 꼬리) | `MEMCG_LRU_TAIL` |
| offlin된 memcg 첫 시도 | TAIL | `MEMCG_LRU_TAIL` |
| offlin된 memcg 두 번째 시도 | YOUNG (젊은 세대) | `MEMCG_LRU_YOUNG` |
| min threshold 도달 | YOUNG | `MEMCG_LRU_YOUNG` |
| 에이징 완료 | YOUNG | `MEMCG_LRU_YOUNG` |
| memcg offlining | OLD (오래된 세대) | `MEMCG_LRU_OLD` |
MGLRU는 도서관의 책 분류 시스템과 비슷합니다.