Slab Allocator (SLUB/SLOB)

Linux 커널의 Slab 할당자는 자주 사용되는 커널 객체(task_struct, inode 등)를 효율적으로 관리하는 캐싱 메커니즘입니다. SLUB, SLOB 구현과 kmem_cache API, Per-CPU 최적화, 성능 튜닝까지 종합적으로 다룹니다.

일상 비유: Slab 할당자는 식당의 미리 준비된 접시 스택과 비슷합니다. 손님이 올 때마다 창고에서 접시를 꺼내는(malloc) 대신, 자주 쓰는 크기의 접시를 미리 깨끗이 씻어 쌓아두면(slab cache) 훨씬 빠릅니다.

핵심 요약

  • 객체 캐싱 — 자주 생성/삭제되는 커널 객체를 미리 할당해 둡니다.
  • 내부 단편화 감소 — 동일 크기 객체만 관리하여 메모리 낭비를 줄입니다.
  • Per-CPU 캐시 — 각 CPU가 독립적인 프리리스트를 가져 lock contention을 제거합니다.
  • SLUB vs SLOB — SLUB은 일반 시스템용 기본 구현, SLOB은 임베디드용 경량 구현입니다.
  • kmalloc 백엔드 — kmalloc()은 내부적으로 Slab 할당자를 사용합니다.

단계별 이해

  1. 핵심 요소 확인
    이 문서에서 다루는 자료구조/API를 먼저 정리합니다.
  2. 처리 흐름 추적
    요청 시작부터 완료까지 실행 경로를 순서대로 확인합니다.
  3. 문제 지점 점검
    실패 경로, 경합 구간, 성능 병목을 체크합니다.

개요 (Overview)

Slab 할당자는 Buddy Allocator 위에 구축된 2차 할당자입니다:

ℹ️

왜 필요한가? 커널 객체는 빈번히 생성/삭제되지만, Buddy Allocator는 최소 4KB를 할당합니다. 192바이트 task_struct를 위해 4KB를 할당하면 95%가 낭비됩니다. Slab은 이런 작은 객체를 효율적으로 관리합니다.

구현 종류 (Implementations)

구현 파일 용도 특징
SLUB mm/slub.c 범용 시스템 (기본) 단순한 구조, 높은 성능, 디버깅 지원
SLOB mm/slob.c 임베디드 시스템 최소 메모리 오버헤드 (~4KB)
SLAB mm/slab.c 레거시 (5.16에서 제거) 복잡한 큐 관리, 많은 오버헤드
💡

현재 추세: 커널 5.16부터 SLAB이 제거되고 SLUB이 기본입니다. 대부분의 시스템은 SLUB을 사용하며, 초소형 임베디드만 SLOB을 선택합니다.

아키텍처 (Architecture)

Slab 할당자는 Buddy Allocator가 할당한 페이지를 받아 작은 객체로 분할·관리합니다. Per-CPU 캐시와 NUMA 노드 캐시의 두 계층으로 락 경합을 최소화하고 확장성을 확보합니다:

물리 메모리 (Physical RAM) 4KB 페이지 단위 — DIMM에 직접 매핑 Buddy Allocator (mm/page_alloc.c) 2^n 페이지 블록 관리 — alloc_pages() / __get_free_pages() Slab 할당자 / SLUB (mm/slub.c) kmem_cache — 페이지를 동일 크기 객체 슬랩으로 분할 관리 Per-CPU 캐시 (kmem_cache_cpu) freelist → [obj] → [obj] → NULL Lock-free 빠른 할당 (fast path) CPU당 1개 — lock contention 없음 NUMA 노드 캐시 (kmem_cache_node) partial 슬랩 목록 (spinlock 보호) Per-CPU miss 시 refill 소스 NUMA 노드당 1개 활성 슬랩 페이지 [obj₀][obj₁][obj₂]…[objₙ] — 동일 크기 객체 Partial 슬랩 페이지 [obj][free][obj]…[free] — 부분 사용/회수 대기
그림 1. Slab 할당자 계층 구조: Buddy Allocator 페이지를 받아 Per-CPU 캐시/NUMA 노드 캐시 두 계층으로 확장성을 확보합니다.

kmem_cache 구조

/* include/linux/slub_def.h */
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  /* Per-CPU 캐시 */
    unsigned long flags;
    unsigned long min_partial;
    unsigned int size;         /* 객체 크기 */
    unsigned int object_size;  /* 실제 객체 크기 */
    unsigned int offset;       /* Free pointer offset */
    struct kmem_cache_order_objects oo;
    struct kmem_cache_node *node[MAX_NUMNODES];
    const char *name;
};

/* Per-CPU 캐시 */
struct kmem_cache_cpu {
    void **freelist;      /* 빠른 할당을 위한 프리리스트 */
    unsigned long tid;   /* Transaction ID */
    struct page *page;   /* 현재 사용 중인 slab */
};

할당 경로 (Allocation Path)

/* Fast path (Per-CPU cache hit) */
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags)
{
    struct kmem_cache_cpu *c = this_cpu_ptr(s->cpu_slab);
    void *object;

    /* Fast path: freelist에서 즉시 할당 */
    object = c->freelist;
    if (likely(object)) {
        c->freelist = get_freepointer(s, object);
        return object;
    }

    /* Slow path: partial list 또는 새 slab */
    return __slab_alloc(s, flags, c);
}

할당 경로 흐름도

kmem_cache_alloc() Per-CPU freelist 비어있지 않음? YES fast path ~10 ns NO 현재 슬랩 freelist 사용 가능? YES slow path ~50 ns NO NUMA 노드 partial 슬랩 있음? YES refill + 반환 NO Buddy에서 새 페이지 할당? YES 새 슬랩 생성 + 반환 NO NULL 반환 (OOM / GFP 정책)
그림 2. SLUB 할당 경로 흐름도: fast path(~10 ns) → slow path → partial 목록 → 새 페이지 순으로 탐색합니다.

슬랩 레이아웃 (Slab Layout)

하나의 슬랩 페이지(또는 compound page) 안에서 객체들은 고정 크기로 순서대로 배치됩니다. 해제된 객체는 offset 위치에 다음 프리 객체 주소를 저장하여 프리리스트 체인을 형성합니다:

/*
 * 슬랩 페이지 레이아웃 예시 (32바이트 객체, 4KB 페이지)
 *
 * [페이지 4096B]
 * ┌──────────┬──────────┬──────────┬─── ···  ───┬──────────┐
 * │  obj[0]  │  obj[1]  │  obj[2]  │            │  obj[N]  │
 * │ (할당됨) │ (프리)   │ (할당됨) │  (할당됨)  │  (프리)  │
 * └──────────┴──────────┴──────────┴─── ···  ───┴──────────┘
 *                 │                                   │
 *  obj[1].offset  └── → obj[N].offset ── → NULL
 *  (freelist 체인: 해제된 객체끼리 연결)
 *
 * N = PAGE_SIZE / object_size = 4096 / 32 = 128개
 */

/* mm/slub.c - freelist 포인터 읽기 */
static inline void *get_freepointer(struct kmem_cache *s, void *object)
{
    object = kasan_reset_tag(object);
    return freelist_dereference(s, object + s->offset);
}

/* freelist 포인터 쓰기 (객체 해제 시) */
static inline void set_freepointer(struct kmem_cache *s,
                                    void *object, void *fp)
{
    unsigned long freeptr_addr = (unsigned long)object + s->offset;
    freelist_ptr_encode(s, fp, freeptr_addr);
}
⚠️

Use-After-Free 위험: 해제된 객체의 offset 위치 바이트가 freelist 포인터로 덮어씌워집니다. SLAB_POISON 플래그를 설정하면 해제 시 0x6b 패턴을 채워 use-after-free를 탐지할 수 있습니다. 프로덕션 환경에서는 KASAN이 더 정밀합니다.

객체 생명주기 (Object Lifecycle)

슬랩 객체는 할당 → 사용 → 해제 → 재할당의 사이클을 반복합니다. 슬랩이 완전히 비면 Buddy Allocator로 페이지를 반환하고, 새 객체가 필요하면 새 슬랩 페이지를 요청합니다:

Buddy Allocator alloc_pages() / __free_pages() 새 페이지 ctor() 호출 슬랩 초기화 [free][free][free]···[free] freelist 완전 구성 freelist 연결 Per-CPU freelist obj₀ → obj₁ → obj₂ → NULL lock-free 빠른 접근 kmem_cache_alloc() 할당됨 (사용 중) 커널 코드가 객체 사용 freelist에서 제거된 상태 kmem_cache_free() freelist 복귀 슬랩 반환 모든 객체 해제 시 __free_pages() 호출 슬랩 완전 비어있음 할당 (fast path) 해제 / 슬랩 반환 초기화 / 페이지 획득
그림 3. 슬랩 객체 생명주기: Buddy Allocator에서 페이지를 받아 Per-CPU freelist로 관리하고, 모든 객체가 해제되면 페이지를 반환합니다.

API (Application Programming Interface)

캐시 생성

/* include/linux/slab.h */
struct kmem_cache *kmem_cache_create(
    const char *name,
    unsigned int size,
    unsigned int align,
    slab_flags_t flags,
    void (*ctor)(void *)
);

/* 사용 예 */
struct kmem_cache *my_cache;

my_cache = kmem_cache_create(
    "my_object",
    sizeof(struct my_object),
    0,
    SLAB_HWCACHE_ALIGN,
    NULL
);

할당/해제

/* 객체 할당 */
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags);

/* 객체 해제 */
void kmem_cache_free(struct kmem_cache *s, void *x);

/* 캐시 삭제 */
void kmem_cache_destroy(struct kmem_cache *s);

/* 사용 예 */
struct my_object *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
kmem_cache_free(my_cache, obj);

GFP 플래그

슬랩 할당 시 두 번째 인자로 전달하는 GFP(Get Free Pages) 플래그는 할당 동작 방식을 제어합니다:

플래그 sleep 허용 사용 컨텍스트 설명
GFP_KERNEL 허용 프로세스 컨텍스트 메모리 회수/스왑 시도 가능 — 가장 일반적
GFP_ATOMIC 금지 인터럽트, spinlock 구간 Sleep 불가 — 긴급 예비 메모리에서 할당 시도
GFP_NOWAIT 금지 약한 실시간 코드 메모리 부족 시 즉시 실패 (reclaim 없음)
GFP_NOIO 부분 I/O 경로 내부 I/O 없이 메모리 확보 — 데드락 방지
GFP_NOFS 부분 파일시스템 내부 파일시스템 호출 없이 확보 — 재진입 방지
GFP_KERNEL | __GFP_ZERO 허용 0 초기화 필요 시 kzalloc() 내부 동작과 동일
⚠️

GFP_ATOMIC 남용 주의: GFP_ATOMIC은 커널 예비 메모리를 소모합니다. 할당 실패 시 NULL이 반환되므로 반드시 반환값을 검사해야 합니다. 프로세스 컨텍스트에서는 항상 GFP_KERNEL을 우선 사용하세요.

kmem_cache_create_usercopy

사용자 공간으로 데이터를 복사할 수 있는 슬랩 캐시를 생성할 때 사용합니다. copy_to_user() 등의 함수가 슬랩 객체를 안전하게 접근할 수 있는 범위를 명시합니다:

/* include/linux/slab.h */
struct kmem_cache *kmem_cache_create_usercopy(
    const char *name,
    unsigned int size,
    unsigned int align,
    slab_flags_t flags,
    unsigned int useroffset,  /* 사용자 접근 허용 시작 offset */
    unsigned int usersize,    /* 사용자 접근 허용 크기 */
    void (*ctor)(void *)
);

/* 예: task_struct에서 comm 필드(task 이름)만 사용자 복사 허용 */
task_struct_cachep = kmem_cache_create_usercopy(
    "task_struct",
    sizeof(struct task_struct),
    ARCH_MIN_TASKALIGN,
    SLAB_PANIC | SLAB_ACCOUNT,
    offsetof(struct task_struct, comm),  /* useroffset */
    sizeof(task->comm),                    /* usersize */
    NULL
);
🚫

메모리 누수 주의: 모듈 언로드 시 반드시 kmem_cache_destroy()를 호출해야 합니다. 누락 시 커널 메모리가 영구 누수됩니다. 또한 kmem_cache_destroy() 호출 전에 해당 캐시의 모든 객체가 반환(kmem_cache_free())되어야 하며, 활성 객체가 남아있으면 BUG()가 발생합니다.

kmalloc과의 관계

kmalloc()은 내부적으로 미리 생성된 Slab 캐시를 사용합니다:

/* mm/slab_common.c - 부팅 시 생성되는 kmalloc 캐시 */
struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH + 1];

/* kmalloc-8, kmalloc-16, kmalloc-32, ..., kmalloc-8192 */
kmalloc(24, GFP_KERNEL);  → kmalloc-32 캐시 사용
kmalloc(200, GFP_KERNEL); → kmalloc-256 캐시 사용

할당기 선택 가이드

상황에 따라 적절한 커널 메모리 할당기를 선택하는 것이 중요합니다. 잘못된 선택은 성능 저하, 메모리 낭비, 심하면 데드락으로 이어질 수 있습니다:

상황 권장 API 이유
자주 생성/삭제되는 고정 크기 객체 kmem_cache_alloc() 캐싱으로 할당 비용 최소화, 내부 단편화 없음
임시 소형 버퍼 (크기 ≤ 8KB) kmalloc() 편리한 API, 내부적으로 슬랩 캐시 사용
0 초기화 필요 버퍼 kzalloc() kmalloc() + memset(0) 단축형, 정보 유출 방지
배열·연속 물리 메모리 (크기 > 8KB) alloc_pages() Buddy Allocator 직접 사용, 물리 연속성 보장
크기가 크고 물리 연속성 불필요 vmalloc() 가상 주소 연속, 물리 주소 비연속 — 큰 버퍼에 적합
Per-CPU 전용 데이터 alloc_percpu() CPU별 격리, false sharing 방지, lock 불필요
DMA 버퍼 (하드웨어 접근) dma_alloc_coherent() 물리 연속 + 캐시 코히런시 보장
💡

크기별 선택 규칙: 객체가 고정 크기이고 빈번하게 할당/해제된다면 kmem_cache를 사용하세요. 일회성이거나 크기가 동적이면 kmalloc()이 적합합니다. 8KB를 넘고 물리 연속성이 불필요하면 vmalloc()을 고려하세요. 각 할당기의 상세 비교는 메모리 관리 심화를 참고하세요.

NUMA 인식 할당 (NUMA-Aware Allocation)

SLUB은 NUMA 토폴로지를 인식하여 각 NUMA 노드마다 독립적인 partial 슬랩 목록을 유지합니다. 이를 통해 원격 노드 메모리 접근(remote NUMA access)을 최소화하여 지연 시간을 줄입니다:

kmem_cache_node 구조

/* mm/slab.h */
struct kmem_cache_node {
    spinlock_t    list_lock;        /* partial 목록 보호 */
    unsigned long nr_partial;       /* partial 슬랩 수 */
    struct list_head partial;       /* partial 슬랩 목록 */
    atomic_long_t nr_slabs;         /* 총 슬랩 수 */
    atomic_long_t total_objects;    /* 총 객체 수 (활성 + 프리) */
};

/* kmem_cache는 NUMA 노드당 하나의 kmem_cache_node를 가짐 */
struct kmem_cache {
    /* ... */
    struct kmem_cache_node *node[MAX_NUMNODES];  /* 노드당 캐시 */
};

NUMA 할당 우선순위

/*
 * 할당 우선순위 (현재 CPU → 로컬 NUMA 노드 → 새 페이지):
 *
 * 1. kmem_cache_cpu.freelist          (per-CPU, lock-free, 매우 빠름)
 *    ↓ miss
 * 2. kmem_cache_cpu.page.freelist     (현재 CPU 슬랩의 잔여 객체)
 *    ↓ empty
 * 3. kmem_cache_node[local].partial   (로컬 NUMA 노드 partial 목록)
 *    ↓ empty
 * 4. alloc_pages(GFP_KERNEL | __GFP_THISNODE, ...)  (로컬 노드 새 페이지)
 *    ↓ 실패 시 원격 노드 fallback
 */

/* 특정 NUMA 노드에서 객체 할당 */
void *obj = kmem_cache_alloc_node(my_cache, GFP_KERNEL, numa_node_id());

/* 노드 1에 강제 할당 (NUMA 정책 테스트 목적) */
void *obj2 = kmem_cache_alloc_node(my_cache, GFP_KERNEL, 1);
💡

NUMA 성능 팁: kmem_cache_alloc_node()로 객체를 로컬 노드에 생성하고, 해당 노드의 CPU에만 작업을 핀닝(CPU affinity)하면 원격 NUMA 접근 지연을 효과적으로 제거할 수 있습니다. NUMA 토폴로지 파악은 NUMA 페이지를 참고하세요.

보안 강화 (Security Hardening)

SLUB은 힙 기반 취약점 악용을 어렵게 만드는 보안 메커니즘을 제공합니다. 현대 배포판 커널(Ubuntu, RHEL, Debian 등)은 대부분 이 옵션들을 활성화합니다:

CONFIG 옵션 보호 기법 방어 대상 성능 영향
SLAB_FREELIST_RANDOM freelist 순서 무작위화 힙 스프레이, 힙 레이아웃 예측 공격 초기화 시 1회 (미미)
SLAB_FREELIST_HARDENED freelist 포인터 XOR 난독화 freelist 포인터 위조 (힙 오버플로) 할당/해제 시 XOR 2회
INIT_ON_ALLOC_DEFAULT_ON 할당 시 자동 0 초기화 초기화 전 메모리 정보 유출 할당마다 memset
INIT_ON_FREE_DEFAULT_ON 해제 시 자동 0 초기화 use-after-free 정보 유출 해제마다 memset

SLAB_FREELIST_HARDENED 구현

freelist 포인터를 저장할 때 ptr XOR s→random XOR ptr_addr로 난독화합니다. 힙 오버플로로 freelist 포인터를 덮어써도, 커널 부팅 시 생성된 s→random 비밀값을 모르면 예측 가능한 주소로 조작하기 어려워집니다:

/* mm/slub.c — CONFIG_SLAB_FREELIST_HARDENED */
static inline void *freelist_ptr_decode(const struct kmem_cache *s,
                                         void *ptr,
                                         unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
    /*
     * 디코딩: encoded XOR s->random XOR ptr_addr = 원본 포인터
     * 인코딩 시 secret(s->random)과 저장 주소를 XOR했으므로
     * 공격자가 s->random을 모르면 위조된 포인터 예측 불가
     */
    return (void *)((unsigned long)ptr ^ s->random ^ ptr_addr);
#else
    return ptr;
#endif
}

static inline void freelist_ptr_encode(const struct kmem_cache *s,
                                        void *ptr,
                                        unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
    unsigned long encoded = (unsigned long)ptr ^ s->random ^ ptr_addr;
    *(void **)ptr_addr = (void *)encoded;
#else
    *(void **)ptr_addr = ptr;
#endif
}

/* s->random: 커널 부팅 시 get_random_long()으로 생성 */
static int __init kmem_cache_init(void)
{
    s->random = get_random_long();  /* 재부팅 시마다 변경 */
    /* ... */
}
ℹ️

CONFIG_SLAB_FREELIST_RANDOM 작동: 슬랩 초기화 시 shuffle_freelist()가 Fisher-Yates 알고리즘으로 객체 순서를 섞습니다. 이로 인해 kmem_cache_alloc()이 반환하는 주소를 예측하기 어려워져 힙 스프레이 공격의 효과를 크게 떨어뜨립니다.

KFENCE (Kernel Electric-Fence)

KFENCE는 프로덕션 환경에서도 사용 가능한 샘플링 기반 메모리 안전성 검사기입니다. 기존 KASAN(전수 검사)과 달리 통계적으로 일부 할당만 특수 풀에서 수행하여 경계 침범과 use-after-free를 탐지합니다:

# KFENCE 활성화 (부팅 파라미터)
# 100ms마다 1개 할당을 KFENCE 특수 풀에서 수행하여 검사
kfence.sample_interval=100

# KFENCE 통계 확인 (런타임)
cat /sys/kernel/debug/kfence/stats
# total allocs: 18420, faults: 2
# out-of-bounds: 1, use-after-free: 1, invalid-free: 0

# 오류 발생 시 커널 로그 예시:
# BUG: KFENCE: use-after-free read in my_func+0x3c/0xa0
# Use-after-free read at 0xffff888... (in kfence-#42):
#   my_func+0x3c/0xa0
#   do_work+0x18/0x40
# Freed by task 1234:
#   kfree+0x5a/0x90
#   cleanup_obj+0x12/0x30
💡

KASAN vs KFENCE 비교: KASAN은 개발·테스트 환경에서 모든 할당을 검사하므로 메모리 사용량이 2~3배 증가하고 성능이 크게 저하됩니다. KFENCE는 프로덕션 커널에서 CONFIG_KFENCE=y로 활성화하여 성능 영향 없이 장기 운영 중 확률적으로 버그를 탐지합니다. 자세한 내용은 디버깅 도구를 참고하세요.

디버깅 (Debugging)

slabinfo 확인

# 모든 slab 캐시 정보 보기
cat /proc/slabinfo

# 출력 예:
# name          active  num_objs  objsize  objperslab  pagesperslab
task_struct      120      120     7104        4             8
dentry          8000    8400      192       21             1
inode_cache     5000    5120      608       13             2

slabtop 모니터링

# 실시간 slab 사용량 모니터링
sudo slabtop

# 정렬 옵션:
# -s c : 캐시 크기 정렬
# -s o : 객체 수 정렬
sudo slabtop -s c

KASAN 연동

CONFIG_KASAN 활성화 시 SLUB과 통합되어 use-after-free, heap-buffer-overflow를 런타임에 탐지합니다:

/* CONFIG_KASAN 환경에서의 kmem_cache_free 흐름 */
void kmem_cache_free(struct kmem_cache *s, void *x)
{
    /* KASAN: 객체 범위 외 접근 검사 (red zone) */
    kasan_slab_free(s, x, _RET_IP_);

    /* SLUB: freelist에 반환 */
    slab_free(s, virt_to_slab(x), x, NULL, 1, _RET_IP_);
}
# KASAN 오류 발생 시 커널 로그 예시
# BUG: KASAN: slab-use-after-free in my_function+0x42/0x80
# Read of size 4 at addr ffff888... by task process/1234

# SLUB 디버그 통계 확인 (CONFIG_SLUB_STATS=y 필요)
cat /sys/kernel/slab/<cache-name>/alloc_fastpath
cat /sys/kernel/slab/<cache-name>/alloc_slowpath
💡

슬랩 디버그 빌드 옵션: 커널 빌드 시 CONFIG_SLUB_DEBUG=yCONFIG_KASAN=y를 함께 활성화하면 슬랩 경계 위반, use-after-free, double-free를 정확히 잡아낼 수 있습니다. slub_debug=FZP 커널 파라미터로 Freeing poison, Zero-on-free, Padding check을 런타임에 활성화할 수도 있습니다.

메모리 압박과 Shrinker 연동

SLAB_RECLAIM_ACCOUNT 플래그를 설정하면 해당 슬랩은 메모리 압박 시 커널이 회수할 수 있는 reclaimable 메모리로 집계됩니다. VFS inode/dentry 캐시처럼 "비워도 되는" 캐시에 적합합니다:

/* SLAB_RECLAIM_ACCOUNT: 메모리 압박 시 회수 대상으로 등록 */
inode_cachep = kmem_cache_create(
    "inode_cache",
    sizeof(struct inode),
    0,
    SLAB_RECLAIM_ACCOUNT | SLAB_PANIC | SLAB_MEM_SPREAD,
    init_once
);

/*
 * /proc/meminfo에서 슬랩 분류:
 * SReclaimable: SLAB_RECLAIM_ACCOUNT 플래그 슬랩 (dentry, inode 등)
 * SUnreclaim:   회수 불가 슬랩 (task_struct, mm_struct 등)
 */
# /proc/meminfo에서 슬랩 통계 확인
grep -E "Slab|SReclaim|SUnreclaim" /proc/meminfo
# Slab:            512000 kB
# SReclaimable:    420000 kB  ← 회수 가능
# SUnreclaim:       92000 kB  ← 회수 불가

# 수동 회수 (디버깅 전용 — 프로덕션 환경 주의)
echo 2 > /proc/sys/vm/drop_caches   # dentries, inodes만
echo 3 > /proc/sys/vm/drop_caches   # pagecache + dentries + inodes
⚠️

drop_caches 주의: echo 3 > /proc/sys/vm/drop_caches는 프로덕션 서버에서 심각한 성능 저하를 유발합니다. 슬랩 캐시는 커널 kswapd가 필요에 따라 자동 회수하므로, 수동 해제는 메모리 분석 목적으로만 사용하세요. 관련 내용은 Shrinker 페이지를 참고하세요.

성능 최적화 (Performance)

캐시 플래그

플래그 설명 용도
SLAB_HWCACHE_ALIGN 캐시 라인 정렬 False sharing 방지
SLAB_POISON 메모리 독 패턴 채우기 디버깅 (use-after-free 탐지)
SLAB_RED_ZONE Red zone 추가 버퍼 오버플로 탐지
SLAB_PANIC 생성 실패 시 panic 중요 캐시

모범 사례

/* 좋은 예: 전용 캐시 + 생성자 */
static struct kmem_cache *task_struct_cache;

static void task_struct_ctor(void *obj)
{
    struct task_struct *task = obj;
    memset(task, 0, sizeof(*task));
    /* 초기화 로직 */
}

task_struct_cache = kmem_cache_create(
    "task_struct",
    sizeof(struct task_struct),
    ARCH_MIN_TASKALIGN,
    SLAB_HWCACHE_ALIGN | SLAB_PANIC,
    task_struct_ctor
);

실사용 사례

inode 캐시

/* fs/inode.c */
static struct kmem_cache *inode_cachep;

static void init_once(void *foo)
{
    struct inode *inode = (struct inode *) foo;
    inode_init_once(inode);
}

void inode_init(void)
{
    inode_cachep = kmem_cache_create(
        "inode_cache",
        sizeof(struct inode),
        0,
        SLAB_RECLAIM_ACCOUNT | SLAB_PANIC | SLAB_MEM_SPREAD,
        init_once
    );
}

dentry 캐시

VFS 디렉터리 엔트리 캐시는 파일 경로 탐색 성능의 핵심입니다. 이름 조회(lookup) 결과를 캐시하여 디스크 접근을 방지합니다:

/* fs/dcache.c */
static struct kmem_cache *dentry_cache __read_mostly;

void __init vfs_caches_init_early(void)
{
    dentry_cache = kmem_cache_create_usercopy(
        "dentry",
        sizeof(struct dentry),
        0,
        SLAB_RECLAIM_ACCOUNT | SLAB_PANIC | SLAB_MEM_SPREAD | SLAB_ACCOUNT,
        offsetof(struct dentry, d_iname),
        DNAME_INLINE_LEN,
        NULL
    );
}

/* dentry 할당 */
static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
{
    struct dentry *dentry;
    dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
    /* ... 초기화 ... */
    return dentry;
}

네트워크 sk_buff 캐시

네트워크 스택에서 패킷을 표현하는 sk_buff는 초당 수백만 개가 할당·해제되는 핵심 객체입니다. 전용 슬랩 캐시로 할당 비용을 최소화합니다:

/* net/core/skbuff.c */
static struct kmem_cache *skbuff_head_cache __read_mostly;

void __init skb_init(void)
{
    skbuff_head_cache = kmem_cache_create_usercopy(
        "skbuff_head_cache",
        sizeof(struct sk_buff),
        0,
        SLAB_HWCACHE_ALIGN | SLAB_PANIC,
        offsetof(struct sk_buff, cb),
        sizeof_field(struct sk_buff, cb),
        NULL
    );
}

/* sk_buff 할당 (네트워크 패킷 수신 시) */
struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
{
    struct sk_buff *skb;
    skb = kmem_cache_alloc(skbuff_head_cache, priority);
    /* ... 데이터 영역(skb->data) 별도 할당 ... */
    return skb;
}
ℹ️

슬랩 캐시를 쓰는 주요 커널 객체: task_struct(프로세스), mm_struct(주소 공간), vm_area_struct(가상 메모리 영역), file(열린 파일), inode(파일시스템 노드), dentry(경로 캐시), sk_buff(네트워크 패킷), socket(소켓), bio(블록 I/O) 등 커널 핵심 객체 대부분이 전용 슬랩 캐시를 사용합니다.

완전한 커널 모듈 예제

슬랩 캐시의 전체 생명주기(생성 → 할당 → 사용 → 해제 → 삭제)를 보여주는 완전한 커널 모듈 예제입니다. 실제 드라이버 작성 시 이 패턴을 참고하세요:

/* my_cache_module.c — slab cache 전체 생명주기 예제 */
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/list.h>

MODULE_LICENSE("GPL");

/* 1. 캐시에 저장할 구조체 */
struct my_object {
    int              id;
    char             name[32];
    unsigned long    timestamp;
    struct list_head list;
};

static struct kmem_cache *my_cache;
static LIST_HEAD(my_objects);
static DEFINE_SPINLOCK(my_lock);

/* 2. 생성자: 슬랩 페이지 초기화 시 한 번만 호출됨
 *    매 kmem_cache_alloc() 호출 시 실행되지 않으므로
 *    재사용 시에도 유효해야 하는 필드만 초기화 */
static void my_object_ctor(void *obj)
{
    struct my_object *o = obj;
    INIT_LIST_HEAD(&o->list);
}

/* 3. 할당 헬퍼 — 할당 후 가변 필드는 직접 초기화 */
static struct my_object *my_object_alloc(int id, const char *name)
{
    struct my_object *o = kmem_cache_alloc(my_cache, GFP_KERNEL);
    if (!o)
        return NULL;

    o->id        = id;
    o->timestamp = jiffies;
    strscpy(o->name, name, sizeof(o->name));

    spin_lock(&my_lock);
    list_add(&o->list, &my_objects);
    spin_unlock(&my_lock);

    return o;
}

/* 4. 해제 헬퍼 */
static void my_object_free(struct my_object *o)
{
    spin_lock(&my_lock);
    list_del(&o->list);
    spin_unlock(&my_lock);
    kmem_cache_free(my_cache, o);  /* 슬랩 freelist로 반환 */
}

static int __init my_module_init(void)
{
    struct my_object *o1, *o2;

    /* 5. 캐시 생성 — SLAB_PANIC: 실패 시 커널 패닉 */
    my_cache = kmem_cache_create(
        "my_object",
        sizeof(struct my_object),
        0,                              /* align: 0 = 자동 */
        SLAB_HWCACHE_ALIGN | SLAB_PANIC, /* 캐시라인 정렬 */
        my_object_ctor
    );

    pr_info("my_cache: created, object_size=%zu\n",
            sizeof(struct my_object));

    /* 6. 객체 할당 및 사용 */
    o1 = my_object_alloc(1, "alpha");
    o2 = my_object_alloc(2, "beta");

    /* 7. 객체 해제 */
    my_object_free(o1);
    my_object_free(o2);

    return 0;
}

static void __exit my_module_exit(void)
{
    struct my_object *o, *tmp;

    /* 8. 남은 객체 모두 반환 — kmem_cache_destroy() 전에 필수 */
    spin_lock(&my_lock);
    list_for_each_entry_safe(o, tmp, &my_objects, list) {
        list_del(&o->list);
        kmem_cache_free(my_cache, o);
    }
    spin_unlock(&my_lock);

    /* 9. 캐시 삭제 (활성 객체가 남아있으면 BUG() 발생) */
    kmem_cache_destroy(my_cache);
    pr_info("my_cache: destroyed\n");
}

module_init(my_module_init);
module_exit(my_module_exit);
⚠️

생성자 오해 주의: ctor는 슬랩 페이지가 처음 초기화될 때만 호출되며, kmem_cache_alloc() 호출 시 실행되지 않습니다. 따라서 매 할당마다 초기화해야 하는 필드(id, timestamp 등)는 할당 후 직접 설정해야 합니다. 생성자는 재사용 시에도 항상 유효한 필드(리스트 헤드, 뮤텍스, spinlock 등)의 초기화에만 사용하세요.

성능 분석 도구

슬랩 할당이 성능 병목인지 확인하고 분석하는 방법입니다:

perf kmem

# 5초간 시스템 전체 슬랩 할당 이벤트 캡처
sudo perf kmem record -a sleep 5

# 슬랩 통계 분석 (캐시별 할당 횟수 · 크기 · 단편화율)
sudo perf kmem stat --slab

# 출력 예:
# Alloc Ptr    |Alloc Bytes|Freed Ptr    |Freed Bytes|Caller
# 12420        | 9.5 MB    | 12180       | 9.3 MB    | kmalloc-256
# 8800         | 1.7 MB    | 8600        | 1.6 MB    | dentry

ftrace 함수 추적

# kmem_cache_alloc 함수만 추적
echo 'kmem_cache_alloc' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -30

# 특정 캐시의 sysfs 통계 (CONFIG_SLUB_STATS=y 필요)
cat /sys/kernel/slab/dentry/alloc_fastpath
cat /sys/kernel/slab/dentry/alloc_slowpath
cat /sys/kernel/slab/dentry/free_fastpath

vmstat 슬랩 모니터링

# 1초 간격 슬랩 관련 카운터 모니터링
vmstat -s | grep -i slab

# /proc/meminfo 슬랩 요약
grep -E "Slab|Buffers|Cached" /proc/meminfo

# 캐시별 상세 — 객체 크기 · 페이지 수 · 활성 객체 비율
awk 'NR>2 {printf "%-30s objs=%d size=%dB\n", $1, $3, $4}' /proc/slabinfo \
    | sort -t= -k3 -rn | head -20
💡

병목 판단 기준: alloc_slowpath / alloc_fastpath 비율이 10% 이상이면 해당 캐시에서 Per-CPU 캐시 미스가 잦다는 의미입니다. min_partial 값을 늘리거나 cpu_partial 값을 조정하여 완화할 수 있습니다.

SLAB/SLUB 캐시 감사 체크리스트

슬랩 문제는 누수와 오염(corruption)으로 나뉩니다. 캐시 단위 통계와 디버그 옵션을 함께 사용해 원인을 빠르게 좁히는 것이 핵심입니다.

  1. 증가 캐시 식별: /proc/slabinfo 상위 항목 비교
  2. 회수 가능성 판단: SReclaimable/SUnreclaim 비율 확인
  3. 오염 검사: slub_debug=FZPU로 redzone/poison 활성화
  4. 할당 경로 추적: perf kmem/ftrace로 hot path 확인
# 슬랩 상태 핵심 수집
grep -E "Slab|SReclaimable|SUnreclaim" /proc/meminfo
cat /proc/slabinfo | head -n 50
sudo perf kmem stat --slab 2>/dev/null || true

SLUB Sheaves: Per-CPU 캐싱 최적화 (v6.18+)

커널 6.18에서 도입된 sheaves는 SLUB 할당자에 새로운 per-CPU 캐싱 레이어를 추가하는 최적화입니다. 기존 SLUB의 per-CPU 프리리스트 방식을 개선하여 객체 할당/해제 시 노드 레벨 락 경합을 크게 줄입니다.

도입 배경

기존 SLUB 할당자는 per-CPU 프리리스트가 소진되면 partial slab 리스트에서 새 slab을 가져오기 위해 노드 레벨 락(list_lock)을 잡아야 했습니다. 이는 CPU 코어 수가 많은 시스템(4096코어 이상, v6.14)에서 심각한 경합을 일으킬 수 있습니다.

설계

/*
 * Sheaves 구조 개요 (v6.18+, mm/slub.c)
 *
 * 기존 SLUB:
 *   CPU → per-CPU freelist → partial list (node lock)
 *
 * Sheaves 적용 후:
 *   CPU → sheaf (로컬 배치) → per-CPU freelist → partial list
 *
 * sheaf는 일정 수의 객체를 배치(batch)로 관리하여
 * node lock 접근 빈도를 줄임
 */

/* sheaf: 객체를 배치 단위로 보관하는 per-CPU 구조 */
/* 할당: sheaf에서 객체를 꺼냄 (lock-free) */
/* 해제: sheaf에 객체를 반환 (lock-free) */
/* sheaf 소진/가득 참: 새 sheaf를 교체 (infrequent lock) */
구분기존 SLUBSLUB + Sheaves (v6.18+)
per-CPU 캐싱단일 프리리스트sheaf 배치 + 프리리스트
락 경합프리리스트 소진 시 node locksheaf 교체 시에만 lock (빈도 감소)
배치 처리1개씩 refill배치 단위 refill/drain
메모리 오버헤드낮음약간 증가 (sheaf 구조체)
효과기준고코어 시스템에서 할당/해제 처리량 향상
Sheaves의 의의: 4096 CPU 코어 지원(v6.14)과 함께 대규모 시스템에서의 slab 할당 성능 병목을 해결합니다. SLAB 할당자(2024년 제거)가 가지고 있던 배치 처리의 장점을 SLUB에 재도입한 형태입니다.