Memory · Linux v6.6 · source + diagram note

SLUB

kmem_cache, object allocator, per-cpu freelist.

전체 흐름: 이미 있는 cache에서 객체 하나를 얻는다

SLUB는 동일 크기 객체를 담는 struct kmem_cache *s를 입력받는다. kmem_cache_alloc()는 cache의 레이아웃을 만드는 함수가 아니라 준비된 cache에 객체를 요청한다. 내부 후보는 조건에 따라 갈린다.

그림 1. 객체 할당 API의 경계
호출자이미 존재하는 cache와 GFP flags
kmem_cache_alloc()객체 할당 API, cache 생성 아님
slab_alloc_node()pre-hook · KFENCE · SLUB
object 또는 NULLpost-hook 뒤 반환, API tracepoint 기록

이 그림은 호출 경계다. 객체 공급원은 뒤의 조건부 분기 그림을 본다. mm/slub.c:3489–3504.

표 1. API와 책임 경계
범위입력결과고정 원본
kmem_cache_alloc()기존 cache와 GFP flags객체 또는 NULL 반환. cache를 새로 만들지 않는다.mm/slub.c:3489–3504
slab_alloc_node()cache·node·요청 크기pre-hook 실패는 NULL; 후보 성공 뒤에도 post-hook을 거친다.mm/slub.c:3453–3481
kmem_cache_free()사용을 마친 유효한 객체별개의 해제 API. 해제 뒤 포인터 재사용은 오류다.mm/slub.c:3825–3833

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: mm/slub.c
Linux v6.6: mm/slab_common.c
Linux v6.6: include/linux/slab.h

설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

대표 코드

kmem_cache_alloc()과 tracepoint

원본 코드: 고정 v6.6 mm/slub.c, 3489–3504행
static __fastpath_inline
void *__kmem_cache_alloc_lru(struct kmem_cache *s, struct list_lru *lru,
			     gfp_t gfpflags)
{
	void *ret = slab_alloc(s, lru, gfpflags, _RET_IP_, s->object_size);

	trace_kmem_cache_alloc(_RET_IP_, ret, s, gfpflags, NUMA_NO_NODE);

	return ret;
}

void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
	return __kmem_cache_alloc_lru(s, NULL, gfpflags);
}
EXPORT_SYMBOL(kmem_cache_alloc);

공개 API가 __kmem_cache_alloc_lru()에 위임하고, 내부 helper가 최종 반환값을 tracepoint에 전달한다.

slab_alloc_node()의 후보 순서

원본 코드: 고정 v6.6 mm/slub.c, 3453–3481행
static __fastpath_inline void *slab_alloc_node(struct kmem_cache *s, struct list_lru *lru,
		gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
	void *object;
	struct obj_cgroup *objcg = NULL;
	bool init = false;

	s = slab_pre_alloc_hook(s, lru, &objcg, 1, gfpflags);
	if (!s)
		return NULL;

	object = kfence_alloc(s, orig_size, gfpflags);
	if (unlikely(object))
		goto out;

	object = __slab_alloc_node(s, gfpflags, node, addr, orig_size);

	maybe_wipe_obj_freeptr(s, object);
	init = slab_want_init_on_alloc(gfpflags, s);

out:
	/*
	 * When init equals 'true', like for kzalloc() family, only
	 * @orig_size bytes might be zeroed instead of s->object_size
	 */
	slab_post_alloc_hook(s, objcg, gfpflags, 1, &object, init, orig_size);

	return object;
}

pre-hook 실패면 NULL, KFENCE 성공이면 out, 아니면 __slab_alloc_node()로 간다. 반환 전 post-hook을 거친다.

함수별 분석: CPU slab의 빠른 길과 refill

KFENCE가 객체를 주지 않아 __slab_alloc_node()에 들어온 뒤를 확대한다. 빠른 갱신이 실패하면 재시도하고, lockless 사용 조건이나 freelist·slab·node 조건이 맞지 않으면 느린 경로로 간다. 느린 경로는 잠금 아래 현재 freelist를 다시 확인하므로, 진입할 때마다 새 slab을 할당하는 것은 아니다.

그림 2. CPU freelist pop과 slow path의 분기
CPU freelist 조건과 slow path fallback __slab_alloc_node(): CPU-local fast path와 fallback 현재 CPU의 cc→freelist: [A] → [B]c→slab: S0 · c→tid: n 조건 충족?lockless · freelist · slab · node 예tid + freelist 갱신성공 → 객체 A 반환실패 → 읽기 재시도 Afast path 반환비교 교환 실패 → redo 아니오__slab_alloc()재검사 / refill없으면 NULL 가능 공급원CPU / node partial또는 new_slab() CONFIG_SLUB_TINY에서는 별도 get_partial() / new_slab() 구현.

실제 저장소 크기나 실측 trace가 아닌 분기 도식이다. 코드: mm/slub.c:3329–3402.

표 2. CPU slab 안의 조건과 결과
경계조건결과 / 다음 경로원본
CPU-local 후보KFENCE가 객체를 주지 않고 CONFIG_SLUB_TINY=nc→freelist, c→slab, node, lockless 조건을 함께 검사mm/slub.c:3329–3375
빠른 pop후보 유효 + tid·freelist 비교 교환 성공head 객체 반환; 비교 교환 실패면 redomm/slub.c:3376–3402
일반 slow path빠른 조건 불충족__slab_alloc()에서 freelist 재검사; 필요하면 partial/new slab 시도mm/slub.c:3095–3327
CONFIG_SLUB_TINY별도 __slab_alloc_node() 구현get_partial() 뒤 new_slab(); CPU lockless fast path 아님mm/slub.c:3403–3429

구조체와 필드: CPU마다 다른 freelist

cache 설정, CPU-local 할당 상태, slab 안의 객체 목록은 다른 소유 단위다. 한 CPU의 c→freelist pop을 다른 CPU의 목록 변화와 혼동하지 않는다. c→tid는 갱신 재시도를 판별한다.

그림 3. CPU별 freelist와 partial/new slab 공급원
cache, CPU별 freelist, partial slab과 새 slab의 관계 struct kmem_cache *s객체 크기·flags·CPU/node 자료 CPU 0: c0 → slab S0c0→freelistABA 적중 → A 반환, head는 B CPU 1: c1 → slab S1독립된 c1→freelistCDCPU 0의 head와 같지 않음 partial slab 공급원CPU partial · node partialP0P1부족하면 new_slab() 시도 freelist 부족 시 refill 후보page allocator → 새 slab CONFIG_SLUB_TINY 구성은 별도 코드 경로.

S0/S1과 A–D/P0/P1은 설명용 객체 표식이다. 실제 주소나 객체 수를 뜻하지 않는다. mm/slub.c:3329–3402.

표 3. 필드의 역할
대상읽는 법주의
struct kmem_cache *s크기·flags와 cache별 자료kmem_cache_alloc은 cache 생성이 아니다.
c→freelist / c→slab해당 CPU의 즉시 할당 후보와 현재 slab비었거나 node가 맞지 않으면 fast path가 아니다.
c→tidCPU-local 갱신 변화 감지비교 교환 실패는 객체 획득이 아니라 재시도다.
slab partial / free pointer재사용 가능한 객체 공급원partial과 새 slab은 조건부 fallback이다.

할당과 해제는 별개의 수명 경로

CPU freelist 적중과 slow path refill은 한 요청의 연속 단계가 아니다. 해제는 같은 호출의 마지막 단계가 아니라 호출자가 사용을 마친 뒤 진입하는 다른 API다.

그림 4. 할당 결과와 나중의 해제
할당
기존 cache호출자가 s를 전달
조건부 후보KFENCE / CPU slab / refill
post-hook객체 또는 NULL
해제
사용 종료객체 접근을 먼저 멈춤
kmem_cache_free()cache 확인과 해제 hook
재사용 가능이전 포인터 사용 금지

해제 입구: mm/slub.c:3825–3833. KFENCE·KASAN·debug 설정에 따라 내부 free 동작은 달라질 수 있다.

표 4. 성공·실패·해제 경계
상황결과검토 질문
pre-hook 거부후보 진입 전에 NULL호출자가 NULL을 처리하는가?
후보 성공post-hook 뒤 객체 반환 가능hook 뒤에도 반환 객체가 유효한가?
refill 실패NULL 반환 가능GFP flags와 오류 처리를 확인했는가?
kmem_cache_free()객체 사용 종료 후 해제이전 포인터 접근이 남았는가?

불변 조건과 실패 사례

cache와 객체 수명

객체 사용 중 대응 cache가 파괴되면 안 된다. 해제 뒤 이전 포인터를 다시 읽거나 쓰지 않는다.

빠른 경로 조건

lockless 갱신은 허용 구성, freelist·slab·node 판정과 tid 비교 교환에 의존한다.

실패 반환

pre-hook이나 slab 확보가 실패할 수 있다. post-hook 뒤 최종 반환값을 확인한다.

debug 구성

redzone, poison, KASAN, KFENCE는 검사 지점과 배치·성능을 바꿀 수 있다.

주의

CPU-local freelist 보호는 호출자 객체의 lifetime 보장과 다르다.

주의

같은 주소가 재등장해도 이전 객체 포인터의 use-after-free가 정당화되지 않는다.

계측과 검증: 도구마다 보이는 범위가 다르다

API tracepoint는 요청과 결과를 보여 주지만 KFENCE·CPU freelist·refill 가운데 성공 후보를 확정하지 않는다. sanitizer와 debug 옵션은 오류 검출 도구이지 내부 분기 시간을 모두 재는 시계가 아니다.

표 5. SLUB 관찰 도구의 범위
도구볼 수 있는 것그것만으로 알 수 없는 것
/proc/slabinfocache별 객체·slab 집계 상태한 요청의 분기나 실패 지점
slub_debug=FZPUF 검사, Z redzone, P poison, U 사용자 추적 등 활성 구성의 손상 증거모든 할당의 fast/slow 분기; debug는 배치도 바꿀 수 있음
kmem:kmem_cache_alloc/freetracepoint가 제공될 때 cache API 사건내부 freelist·refill 원인
kmem:kmallockmalloc 계열 사건명시적 kmem_cache_alloc의 모든 호출
KASAN / KFENCE활성 구성의 접근 오류·가드 객체 증거SLUB 전체 요청의 완전한 분기 trace
읽기 전용 진단 예시 (대상 커널에서만 실행):
head -n 8 /proc/slabinfo
# tracefs가 이미 제공하면 event 정의만 읽는다. 설정을 바꾸지 않는다.
test -r /sys/kernel/tracing/events/kmem/kmem_cache_alloc/format && cat /sys/kernel/tracing/events/kmem/kmem_cache_alloc/format
test -r /sys/kernel/tracing/events/kmem/kmem_cache_free/format && cat /sys/kernel/tracing/events/kmem/kmem_cache_free/format

공유 current_tracer, set_ftrace_filter, trace_pipe를 건드리지 않는다. tracefs가 없으면 event 파일도 없을 수 있다. 이 예시는 실제 보드에서 실행하지 않았다.

추가 확인 사항

  • 반환 객체는 KFENCE, CPU freelist, partial, new slab 중 어디에서 왔는가? API tracepoint만으로 구분할 수 있는가?
  • USE_LOCKLESS_FAST_PATH()가 거짓이거나 tid 갱신 실패 시 어디로 이동하는가?
  • 실패 반환과 해제 뒤 호출자 객체 수명 처리는 올바른가?
  • debug/sanitizer 설정이 관측한 배치와 성능을 바꾸는가?