# Shrinker (슬래브/캐시 회수 프레임워크)
관련 소스:mm/shrinker.c,include/linux/shrinker.h,include/linux/list_lru.h
Shrinker는 리눅스 커널의 메모리 회수 프레임워크에서 slab 캐시, dentry 캐시, inode 캐시 등 다양한 캐시 오브젝트를 압축(reclaim)하는 인터페이스를 제공합니다. 메모리 부족 상황에서 kswapd나 direct reclaim 경로를 통해 호출되며, 각 캐시는 count_objects와 scan_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 {
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;
};
#define SHRINK_STOP (~0UL)
#define SHRINK_EMPTY (~0UL - 1)
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 */
};
/* 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 비트맵 */
};
/* memcg의 노드별 shrinker 관리 정보 */
struct shrinker_info {
struct rcu_head rcu;
int map_nr_max; /* 등록된 shrinker 최대 수 */
struct shrinker_info_unit *unit[]; /* 유연한 배열 (flexible array) */
};
#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 */
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 > 0 → freeable >> priority * 4 / seeks 계산 (IO 캐시 보호)seeks == 0 → freeable / 2 (IO 불필요 캐시는 공격적 회수)scan_objects()가 SHRINK_STOP 반환 → 현재 회수 컨텍스트 중단total_scan < batch_size && total_scan < freeable → while 루프 탈출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;
}
memcg별 비트맵(shrinker_info)을 순회하며 do_shrink_slab()을 호출합니다. SHRINK_EMPTY 발생 시 이중 검사(double-check) 로직으로 경합을 방지합니다.
SHRINKER_MEMCG_AWARE 플래그 시 shrinker_memcg_alloc()으로 IDR 할당, 미시작 시 fallback으로 non-memcg 경로 수행.
shrinker_list에 추가하고 debugfs 등록, 초기 refcount=1 설정.
초기 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_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()`으로 할당됨 | 내부 플래그 |
| 경로 | 진입 조건 | 순회 대상 | 동시성 제어 |
|---|---|---|---|
| 글로벌 shrink | root memcg | `shrinker_list` 전체 | RCU + refcount |
| memcg shrink | 비-root memcg | memcg별 `shrinker_info` 비트맵 | RCU + 비트맵 |
| drop_slab | `/proc/sys/vm/drop_caches` | 모든 memcg 순회 | 별도 lock 불필요 |
| 반환값 | 의미 | 후속 동작 |
|---|---|---|
| `0` | 회수 불가 (알 수 없음) | 스캔 스킵 |
| `SHRINK_EMPTY` | 오브젝트 0개 (확실) | 즉시 종료, memcg 시 비트맵 클리어 |
| 양수 | 회수 가능 오브젝트 수 | total_scan 계산 후 scan_objects 호출 |