Ryotta's Linux 7.0 MM

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

MGLRU 개선사항 (Multi-Gen LRU)

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"
MGLRU 세대 구조
MGLRU 호출 흐름

핵심 자료구조

struct lru_gen_folio (핵심 세대 관리 구조체)

// 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: 파일 디스크립터 기반 접근 횟수를 분류하는 단계 수
  • struct lru_gen_mm_state (MM 워크 상태)

    // 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];
    };

    struct lru_gen_mm_walk (페이지 테이블 워크 컨텍스트)

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

    struct lru_gen_memcg (memcg별 세대 관리)

    // 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를 무작위로 분산하여 확장성 확보
  • Bloom Filter 관련

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

    folio->flags 내 MGLRU 비트 배치

    // 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: 파일 디스크립터 기반 접근 횟수 저장

  • 핵심 함수

    lru_gen_age_node() — kswapd에서 에이징 트리거

    // 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);
        }
    }
  • kswapd에 의해 호출됨
  • lru_gen_min_ttl: 보호할 작업 집합의 최소 시간 (jiffies)
  • 모든 memcg가 min_ttl 미만이면 OOM Killer 실행
  • try_to_inc_min_seq() — 최소 세대 증가

    // 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;
    }
  • 회수 경로에서 빈 세대를 스킵하여 min_seq를 증가시킴
  • anon/file 간 동기화를 통해 편향 회수 방지
  • inc_max_seq() — 최대 세대 증가 (에이징 완료)

    // 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: 메모리 배리어로 이전 수정사항이 보장되도록 함
  • try_to_inc_max_seq() — MM 워크 포함 최대 세대 증가

    // 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;
    }
  • MMU가 지원하면 페이지 테이블을 직접 워크하여 accessed 비트를 확인
  • should_walk_mmu(): arch_has_hw_pte_young() && LRU_GEN_MM_WALK cap 확인
  • evict_folios() — 세대 기반 회수

    // 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 등)
  • clean folio는 재시도 로직으로 효율적 회수 보장
  • lru_gen_look_around() — rmap 기반 공간적 지역성 활용

    // 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을 워크할 때 호출됨
  • 현재 PTE 주변의 인접 PTE를 스캔하여 캐시 효율적으로 핫 페이지를 승격
  • Bloom Filter에 PMD 엔트리를 추가하여 eviction ↔ aging 간 피드백 루프 형성
  • walk_pmd_range() — 페이지 테이블 워크 (에이징 경로)

    // 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);
        }
    }
  • 두 번의 패스: 첫 번째는 PTE 테이블에 도달 (PMD 락 불필요), 두 번째는 필요시 PMD accessed 비트 클리어
  • Bloom Filter로 중복 스캔 방지
  • pmd_young() 필터링으로 접근되지 않은 PMD 스킵
  • lru_gen_add_folio() / lru_gen_del_folio() — folio 세대 관리

    // 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는 "활성"으로 분류
  • sysfs 인터페이스 — enabled / min_ttl_ms

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

    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)           // 새 세대 생성

    회수 경로 (direct reclaim / kswapd)

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

    rmap 기반 look_around 피드백

    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로 활용)

    조건별 비교

    MGLRU vs 기존 Active/Inactive LRU

    비교 항목기존 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 LRU 동작

    이벤트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 offliningOLD (오래된 세대)`MEMCG_LRU_OLD`

    일상 비유

    MGLRU는 도서관의 책 분류 시스템과 비슷합니다.

  • 기존 LRU: "최근에 빌린 책"과 "오래전에 빌린 책" 2개 서가만 있음. 하루에 100권을 빌렸다면 "최근" 서가가 너무 커져서 실제로 오래된 책을 찾기 어려움
  • MGLRU: "오늘 빌린 책", "이번 주 빌린 책", "이번 달 빌린 책", "그 이전 책" 4개 서가로 구분. 각 서가의 크기가 작으므로 오래된 책을 빠르게 찾을 수 있음
  • Bloom Filter: "이번 주에 빌린 책이 있는 서가" 미리 확인. 없으면 서가를 뒤지지 않음 (PMD 단위로 필터링)
  • rmap look_around: 한 권의 책을 찾으면서 인접한 책도 같이 확인. 같은 선반의 책들이 비슷한 주제일 가능성이 높으므로 (공간적 지역성)
  • memcg LRU: 각 "이용자 그룹"(컨테이너)별로 별도의 서가를 운영. 한 그룹이 책을 독점하지 못하도록 균등하게 관리

  • 관련 문서

  • 메모리 관리 개요
  • 페이지 회수 (vmscan)
  • Workingset
  • Memory Cgroup
  • Folio / Page Cache
  • Reverse Mapping
  • NUMA