Ryotta's Linux 7.0 MM

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

# Shrinker (슬래브/캐시 회수 프레임워크)

관련 소스: mm/shrinker.c, include/linux/shrinker.h, include/linux/list_lru.h

개요 (Overview)

Shrinker는 리눅스 커널의 메모리 회수 프레임워크에서 slab 캐시, dentry 캐시, inode 캐시 등 다양한 캐시 오브젝트를 압축(reclaim)하는 인터페이스를 제공합니다. 메모리 부족 상황에서 kswapd나 direct reclaim 경로를 통해 호출되며, 각 캐시는 count_objectsscan_objects 콜백을 등록하여 자체적인 회수 로직을 구현합니다.

Linux 7.0에서는 memcg(memory cgroup) 인식 shrinker와 NUMA 인식 shrinker를 지원하며, RCU 기반의 lockless 알고리즘으로 동시성 성능을 보장합니다. list_lru 프레임워크와 통합되어 dentry/inode 캐시 회수에 활용되며, shrink_slab()이 최종 진입점으로 동작합니다.

도서관으로 비유하면 shrinker는 반납대 뒤쪽에서 참고서와 카드목록을 정리하는 직원에 가깝습니다. 서가에 다시 꽂아두면 되는 책처럼 파일 기반 페이지는 디스크에서 다시 읽을 수 있고, 개인 노트처럼 익명 페이지는 swap이 있어야 다시 살릴 수 있습니다.

메모리 압박은 보통 kswapd가 WMARK_HIGH/WMARK_LOW/WMARK_MIN 같은 zone 워터마크를 기준으로 백그라운드 정리를 시작하거나, 할당 경로가 직접 reclaim으로 들어가면서 눈에 띕니다. 이때 shrink_node()가 LRU를 먼저 정리하고, 그 다음 shrink_slab()이 dentry/inode/slab 같은 캐시를 비우는 식으로 이어집니다. MGLRU가 켜진 경우에도 이 캐시 회수 단계 자체는 같은 위치에 남습니다.

소스 파일 경로:
  mm/shrinker.c              ← 핵심 구현
  include/linux/shrinker.h   ← struct shrinker, shrink_control 정의
  include/linux/list_lru.h   ← list_lru와 shrinker 통합
  include/linux/memcontrol.h ← memcg별 shrinker_info 정의

빠른 점검 명령

# 등록된 shrinker 목록 확인
ls /sys/kernel/debug/shrinker/

# 특정 shrinker의 회수 통계 확인
cat /sys/kernel/debug/shrinker/<name>/nr_deferred

# slab 캐시 현황 (SLUB)
cat /proc/slabinfo | head -20

# memcg별 slab 사용량
cat /sys/fs/cgroup/memory/<cgroup>/slabinfo

# 메모리 회수 관련 vmstat
grep -E 'slabs_scanned|slabs_freed' /proc/vmstat

# slabtop으로 실시간 모니터링
slabtop -o -s c

# 특정 캐시의 shrinker 동작 추적
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_shrink_slab_start/enable
cat /sys/kernel/debug/tracing/trace_pipe

# shrinker 등록 해제 시 RCU 대기 확인
cat /proc/meminfo | grep -E 'Slab|SReclaimable|SUnreclaim'

# 메모리 압박 지표 확인
cat /proc/pressure/memory

# kswapd / direct reclaim 통계 확인
grep -E 'pgscan_kswapd|pgsteal_kswapd|pgscan_direct|pgsteal_direct|allocstall' /proc/vmstat

# zone 워터마크 확인
cat /proc/zoneinfo | grep -E 'Node|pages free|min|low|high'

# 회수 추세를 1초 간격으로 보기
vmstat 1 10

# 슬랩 캐시에서 dentry / inode 상태를 빠르게 보기
grep -E 'dentry|inode' /proc/slabinfo | head -10

# 파일 캐시 회수 압력 확인
cat /proc/sys/vm/vfs_cache_pressure

핵심 자료구조

struct shrinker (`include/linux/shrinker.h:82-118`)

struct shrinker {
    unsigned long (*count_objects)(struct shrinker *,
                                   struct shrink_control *sc);
    unsigned long (*scan_objects)(struct shrinker *,
                                  struct shrink_control *sc);

    long batch;
    int seeks;
    unsigned flags;

    refcount_t refcount;
    struct completion done;
    struct rcu_head rcu;

    void *private_data;

    struct list_head list;
#ifdef CONFIG_MEMCG
    int id;
#endif
#ifdef CONFIG_SHRINKER_DEBUG
    int debugfs_id;
    const char *name;
    struct dentry *debugfs_entry;
#endif
    atomic_long_t *nr_deferred;
};

반환값 정의 (`include/linux/shrinker.h:58-59`)

#define SHRINK_STOP (~0UL)
#define SHRINK_EMPTY (~0UL - 1)

struct shrink_control (`include/linux/shrinker.h:34-56`)

struct shrink_control {
    gfp_t gfp_mask;                   /* 현재 할당 컨텍스트 */
    int nid;                           /* NUMA 노드 ID */
    unsigned long nr_to_scan;          /* 스캔할 오브젝트 수 */
    unsigned long nr_scanned;          /* 실제 스캔된 오브젝트 수 */
    struct mem_cgroup *memcg;          /* 대상 memcg */
};

struct shrinker_info_unit (`include/linux/shrinker.h:16-19`)

/* memcg별 shrinker 비트맵 단위 — 64개 shrinker를 하나의 unit으로 관리 */
struct shrinker_info_unit {
    atomic_long_t nr_deferred[SHRINKER_UNIT_BITS];  /* shrinker별 미연기 작업 수 */
    DECLARE_BITMAP(map, SHRINKER_UNIT_BITS);         /* 회수 대상 shrinker 비트맵 */
};

struct shrinker_info (`include/linux/shrinker.h:21-25`)

/* memcg의 노드별 shrinker 관리 정보 */
struct shrinker_info {
    struct rcu_head rcu;
    int map_nr_max;                         /* 등록된 shrinker 최대 수 */
    struct shrinker_info_unit *unit[];       /* 유연한 배열 (flexible array) */
};

플래그 정의 (`include/linux/shrinker.h:121-132`)

#define SHRINKER_REGISTERED   BIT(0)   /* shrinker_list에 등록됨 */
#define SHRINKER_ALLOCATED    BIT(1)   /* shrinker_alloc()으로 할당됨 */
#define SHRINKER_NUMA_AWARE   BIT(2)   /* NUMA별 개별 회수 */
#define SHRINKER_MEMCG_AWARE  BIT(3)   /* memcg별 개별 회수 */
#define SHRINKER_NONSLAB      BIT(4)   /* slab이 아닌 non-slab shrinker */

핵심 함수

1. `do_shrink_slab()` — 핵심 회수 엔진 (`mm/shrinker.c:371-466`)

static unsigned long do_shrink_slab(struct shrink_control *shrinkctl,
                                    struct shrinker *shrinker, int priority)
{
    unsigned long freed = 0;
    unsigned long long delta;
    long total_scan;
    long freeable;
    long nr;
    long new_nr;
    long batch_size = shrinker->batch ? shrinker->batch
                                      : SHRINK_BATCH;
    long scanned = 0, next_deferred;

    freeable = shrinker->count_objects(shrinker, shrinkctl);
    if (freeable == 0 || freeable == SHRINK_EMPTY)
        return freeable;

    nr = xchg_nr_deferred(shrinker, shrinkctl);

    if (shrinker->seeks) {
        delta = freeable >> priority;
        delta *= 4;
        do_div(delta, shrinker->seeks);
    } else {
        delta = freeable / 2;
    }

    total_scan = nr >> priority;
    total_scan += delta;
    total_scan = min(total_scan, (2 * freeable));

    trace_mm_shrink_slab_start(shrinker, shrinkctl, nr,
                               freeable, delta, total_scan, priority);

    while (total_scan >= batch_size ||
           total_scan >= freeable) {
        unsigned long ret;
        unsigned long nr_to_scan = min(batch_size, total_scan);

        shrinkctl->nr_to_scan = nr_to_scan;
        shrinkctl->nr_scanned = nr_to_scan;
        ret = shrinker->scan_objects(shrinker, shrinkctl);
        if (ret == SHRINK_STOP)
            break;
        freed += ret;
        count_vm_events(SLABS_SCANNED, shrinkctl->nr_scanned);
        total_scan -= shrinkctl->nr_scanned;
        scanned += shrinkctl->nr_scanned;

        cond_resched();
    }

    next_deferred = max_t(long, (nr + delta - scanned), 0);
    next_deferred = min(next_deferred, (2 * freeable));

    new_nr = add_nr_deferred(next_deferred, shrinker, shrinkctl);

    trace_mm_shrink_slab_end(shrinker, shrinkctl->nid, freed, nr, new_nr, total_scan);
    return freed;
}

분기 로직:

  • count_objects()SHRINK_EMPTY 반환 → 즉시 종료 (오브젝트 없음)
  • seeks > 0freeable >> priority * 4 / seeks 계산 (IO 캐시 보호)
  • seeks == 0freeable / 2 (IO 불필요 캐시는 공격적 회수)
  • scan_objects()SHRINK_STOP 반환 → 현재 회수 컨텍스트 중단
  • total_scan < batch_size && total_scan < freeable → while 루프 탈출
  • 2. `shrink_slab()` — 최종 진입점 (`mm/shrinker.c:614-676`)

    unsigned long shrink_slab(gfp_t gfp_mask, int nid, struct mem_cgroup *memcg,
    	                          int priority)
    {
    	unsigned long ret, freed = 0;
    	struct shrinker *shrinker;
    
    	/* memcg 비-root이면 memcg 전용 경로로 분기한다. */
    	if (!mem_cgroup_disabled() && !mem_cgroup_is_root(memcg))
    		return shrink_slab_memcg(gfp_mask, nid, memcg, priority);
    
    	/* 글로벌 shrink는 RCU 보호 아래 shrinker_list를 순회한다. */
    	rcu_read_lock();
    	list_for_each_entry_rcu(shrinker, &shrinker_list, list) {
    		struct shrink_control sc = {
    			.gfp_mask = gfp_mask,
    			.nid = nid,
    			.memcg = memcg,
    		};
    
    		if (!shrinker_try_get(shrinker))
    			continue;
    
    		rcu_read_unlock();
    
    		ret = do_shrink_slab(&sc, shrinker, priority);
    		if (ret == SHRINK_EMPTY)
    			ret = 0;
    		freed += ret;
    
    		rcu_read_lock();
    		shrinker_put(shrinker);
    	}
    
    	rcu_read_unlock();
    	cond_resched();
    	return freed;
    }

    3. `shrink_slab_memcg()` — memcg 전용 회수 (`mm/shrinker.c:469-585`)

    memcg별 비트맵(shrinker_info)을 순회하며 do_shrink_slab()을 호출합니다. SHRINK_EMPTY 발생 시 이중 검사(double-check) 로직으로 경합을 방지합니다.

    4. `shrinker_alloc()` — shrinker 동적 할당 (`mm/shrinker.c:678-736`)

    SHRINKER_MEMCG_AWARE 플래그 시 shrinker_memcg_alloc()으로 IDR 할당, 미시작 시 fallback으로 non-memcg 경로 수행.

    5. `shrinker_register()` — shrinker 등록 (`mm/shrinker.c:738-759`)

    shrinker_list에 추가하고 debugfs 등록, 초기 refcount=1 설정.

    6. `shrinker_free()` — shrinker 해제 (`mm/shrinker.c:769-811`)

    초기 refcount 해제 → completion 대기 → list 제거 → RCU 콜백으로 메모리 해제.


    호출 흐름

    메모리 압박 발생
      │
      ├─ kswapd() → balance_pgdat() [mm/vmscan.c:6950-7056]
      │     └─ shrink_node() [mm/vmscan.c:6039-6105]
      │          ├─ lru_gen_shrink_node() [MGLRU 켜짐]
      │          └─ shrink_lruvec() [mm/vmscan.c:5772-5788]
      │               └─ shrink_slab()
      │
      └─ direct reclaim → try_to_free_pages() [mm/vmscan.c:6566-6606]
            └─ do_try_to_free_pages()
                 └─ shrink_node()
                      └─ shrink_lruvec()
                           └─ shrink_slab()

    메모리 압박이 들어오면 먼저 LRU 페이지 회수가 시작되고, 파일 기반 페이지와 익명 페이지의 비중을 조정한 뒤, 슬래브 계열 캐시는 그 다음 단계에서 줄어듭니다. shrink_slab()은 이 전체 회수 흐름의 마지막 캐시 정리 단계입니다.

    shrink_lruvec()get_scan_count()로 anon/file 비중을 나누고, inactive list부터 active list로 압력을 옮기면서 회수량을 맞춥니다.

    메모리 회수 트리거 (kswapd / direct reclaim / drop_slab)
      │
      ├─ shrink_slab(gfp_mask, nid, memcg, priority)       [mm/shrinker.c:614]
      │     │
      │     ├─ [memcg 비-root] → shrink_slab_memcg()        [mm/shrinker.c:469]
      │     │     │
      │     │     ├─ rcu_read_lock() + shrinker_info 획득
      │     │     ├─ for_each_set_bit(offset, unit->map)
      │     │     │     │
      │     │     │     ├─ idr_find(&shrinker_idr, shrinker_id)
      │     │     │     ├─ shrinker_try_get(shrinker)
      │     │     │     └─ do_shrink_slab()                 [mm/shrinker.c:371]
      │     │     │           │
      │     │     │           ├─ count_objects() ← 사용자 콜백
      │     │     │           ├─ xchg_nr_deferred()
      │     │     │           ├─ total_scan 계산 (priority 기반)
      │     │     │           ├─ while (total_scan >= batch_size)
      │     │     │           │     └─ scan_objects() ← 사용자 콜백
      │     │     │           └─ add_nr_deferred() (이월)
      │     │     │
      │     │     └─ [SHRINK_EMPTY 시 이중 검사]
      │     │           ├─ clear_bit() + smp_mb__after_atomic()
      │     │           ├─ do_shrink_slab() 재호출
      │     │           └─ 비어있으면 그대로, 아니면 set_shrinker_bit()
      │     │
      │     └─ [root memcg] → 글로벌 shrink 경로
      │           ├─ rcu_read_lock()
      │           ├─ list_for_each_entry_rcu(shrinker_list)
      │           │     ├─ shrinker_try_get()
      │           │     ├─ do_shrink_slab()
      │           │     └─ shrinker_put()
      │           └─ rcu_read_unlock()
      │
      └─ 반환: 회수된 오브젝트 수

    shrinker 등록/해제 흐름:

    shrinker_alloc(flags, fmt, ...)    [mm/shrinker.c:678]
      ├─ [SHRINKER_MEMCG_AWARE] → shrinker_memcg_alloc()
      │     └─ idr_alloc() + expand_shrinker_info()
      └─ [non-memcg] → nr_deferred 배열 할당
    
    shrinker_register(shrinker)        [mm/shrinker.c:738]
      ├─ list_add_tail_rcu(shrinker_list)
      ├─ SHRINKER_REGISTERED 플래그 설정
      ├─ shrinker_debugfs_add()
      └─ refcount_set(1)
    
    shrinker_free(shrinker)            [mm/shrinker.c:769]
      ├─ shrinker_put() → refcount--
      ├─ wait_for_completion(done)
      ├─ list_del_rcu() + shrinker_debugfs_detach()
      └─ call_rcu() → shrinker_free_rcu_cb() → kfree()

    조건별 비교

    메모리 압박 진입점 비교

    진입점시작 조건주된 흐름특징
    kswapd워터마크 도달, 백그라운드 회수 필요`balance_pgdat()` → `shrink_node()` → `shrink_slab()`다른 작업을 막지 않도록 비동기적으로 움직임
    direct reclaim할당 경로에서 즉시 회수 필요`try_to_free_pages()` → `shrink_node()` → `shrink_slab()`현재 호출자 스레드가 직접 기다림
    drop_caches관리자가 수동 해제`drop_slab()` 계열로 캐시 정리파일시스템 캐시를 강제로 줄일 때 사용

    회수 대상별 차이

    대상먼저 줄어드는가다시 채우는 방법shrinker와의 관계
    파일 기반 페이지그렇다파일에서 다시 읽음LRU 회수의 주요 대상
    익명 페이지아니다swap에서 다시 가져옴swap이 있어야 회수 가능
    dentry / inode / slab 캐시상황에 따라 다름필요 시 재생성`count_objects()` / `scan_objects()`로 회수

    Shrinker 플래그별 동작

    플래그동작사용 시점
    `SHRINKER_NUMA_AWARE`NUMA 노드별 `nr_deferred` 분리파일 시스템 inode 캐시 등
    `SHRINKER_MEMCG_AWARE`memcg별 `shrinker_info` 비트맵 관리컨테이너 환경 slab 캐시
    `SHRINKER_NONSLAB`memcg offlines에서도 회수 허용non-slab 캐시 (DAMON 등)
    `SHRINKER_REGISTERED``shrinker_list`에 등록됨내부 플래그
    `SHRINKER_ALLOCATED``shrinker_alloc()`으로 할당됨내부 플래그

    회수 경로 비교

    경로진입 조건순회 대상동시성 제어
    글로벌 shrinkroot memcg`shrinker_list` 전체RCU + refcount
    memcg shrink비-root memcgmemcg별 `shrinker_info` 비트맵RCU + 비트맵
    drop_slab`/proc/sys/vm/drop_caches`모든 memcg 순회별도 lock 불필요

    count_objects 반환값 처리

    반환값의미후속 동작
    `0`회수 불가 (알 수 없음)스캔 스킵
    `SHRINK_EMPTY`오브젝트 0개 (확실)즉시 종료, memcg 시 비트맵 클리어
    양수회수 가능 오브젝트 수total_scan 계산 후 scan_objects 호출

    관련 문서

  • 메모리 관리 개요
  • 페이지 회수
  • SLUB 할당자
  • Memory Cgroup
  • List LRU
  • vmpressure

  • SVG 다이어그램

    Shrinker 회수 호출 흐름

    Shrinker 회수 호출 흐름

    Shrinker 자료구조 관계도

    Shrinker 자료구조 관계도