Memory · Linux 6.18.37 LTS · source + diagram note

SLUB

최신 LTS 기준: kernel.org longterm 6.18.37 기준으로 파일 위치와 링크를 맞춘 원본 코드 분석 노트입니다. kmem_cache, object allocator, per-cpu freelist.

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

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

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

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

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

객체가 나오는 네 갈래

앞의 그림 1은 기존 cache의 API 경계를, 뒤의 그림 2는 조건부 후보를 보여 준다. 실제 할당 코드를 읽을 때는 slab_alloc_node()의 후보 순서를 더해야 한다. v6.18.37에서는 조건부 per-CPU sheaves가 먼저 객체를 줄 수 있고, 그렇지 않으면 CPU slab의 freelist와 refill 경로로 내려간다.

보충 그림. 단일 객체 할당의 분기와 반환 지점

휴대전화에서는 그림을 좌우로 밀어 전체 흐름을 볼 수 있다.

SLUB 단일 객체 할당: KFENCE·조건부 sheaves, 현재 CPU freelist, partial 또는 새 slab으로 이어지는 분기. 성공한 단계는 뒤 단계를 건너뛴다.

그림 크게 열기 · 분기 설명용 도식이며 실제 주소 배치나 실측 trace가 아니다.

아래 표도 좁은 화면에서는 좌우로 이동한다.

보충 표. 분기에서 확인할 객체와 상태 변화
단계판정 / 입력상태 변화와 다음 경로원본 위치
진입slab_pre_alloc_hook(), KFENCE 결과KFENCE가 객체를 반환하면 후처리로 간다. 아니면 s->cpu_sheaves의 유무를 본다.mm/slub.c:5317–5335
선택적 sheaveslocal lock, main sheaf의 크기, 요청 NUMA node적중하면 마지막 object를 꺼내 size를 줄인다. 실패 또는 비활성화 시 __slab_alloc_node()를 호출한다.mm/slub.c:5162–5230, 5331–5335
CPU freelistc->tid, c->freelist, c->slab, node 일치fast path에서는 다음 포인터와 tid를 함께 갱신한다. 경합으로 갱신이 실패하면 다시 읽고 재시도한다.mm/slub.c:4879–4965
refill / 새 slab현재 slab의 남은 목록, CPU partial, node partial가능한 곳에서 freelist를 채운다. 모두 없으면 new_slab()을 시도하며, 새 slab 할당까지 실패하면 NULL을 반환한다.mm/slub.c:4545–4818

근거: 고정된 Linux 6.18.37 mm/slub.c (SHA-256 92e3bc1c4d26b3d563941692a646c5fa5dcc696f917a8254964c42ad94cfd680). 위 행 번호는 이 고정본 기준이다. 컴파일 설정과 요청 조건에 따라 그림의 일부 갈래는 실행되지 않는다.

소스 코드 위치

먼저 확인할 Linux 6.18.37 LTS 소스 파일

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

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

대표 코드

kmem_cache_alloc_noprof

원본 코드: 고정 v6.18.37 mm/slub.c, 5352–5361행
void *kmem_cache_alloc_noprof(struct kmem_cache *s, gfp_t gfpflags)
{
	void *ret = slab_alloc_node(s, NULL, gfpflags, NUMA_NO_NODE, _RET_IP_,
				    s->object_size);

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

	return ret;
}
EXPORT_SYMBOL(kmem_cache_alloc_noprof);

확인 사항: 이 함수는 이미 존재하는 kmem_cache를 입력받아 slab_alloc_node()의 반환 객체를 tracepoint에 기록하고 그대로 돌려준다. cache 생성 함수가 아니다.

slab_alloc_node

원본 코드: 고정 v6.18.37 mm/slub.c, 5317–5350행
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;
	bool init = false;

	s = slab_pre_alloc_hook(s, gfpflags);
	if (unlikely(!s))
		return NULL;

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

	if (s->cpu_sheaves)
		object = alloc_from_pcs(s, gfpflags, node);

	if (!object)
		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
	 * In case this fails due to memcg_slab_post_alloc_hook(),
	 * object is set to NULL
	 */
	slab_post_alloc_hook(s, lru, gfpflags, 1, &object, init, orig_size);

	return object;
}

확인 사항: 선행 hook, KFENCE, 조건부 sheaves, __slab_alloc_node() 순서로 읽는다. 앞 단계에서 객체를 얻으면 뒤 할당 경로를 건너뛰고 post-hook 후 반환한다.

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

앞의 보충 SVG는 KFENCE·선택적 sheaves까지 포함한 전체 후보를 보여 준다. 여기서는 두 후보가 객체를 주지 못해 __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() KFENCE·선택적 sheaves가 먼저 성공하면 이 CPU 경로에 들어오지 않는다.

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

표 2. CPU slab 안의 조건과 결과
경계조건결과 / 다음 경로원본
CPU-local 후보KFENCE·선택적 sheaves가 객체를 주지 않음c→freelist, c→slab, node, lockless 조건을 함께 검사mm/slub.c:4871–4939
빠른 pop후보 유효 + tid·freelist 비교 교환 성공head 객체 반환; 비교 교환 실패면 redomm/slub.c:4940–4965
slow path빠른 조건 불충족__slab_alloc() → ___slab_alloc()에서 freelist 재검사; 필요하면 다른 공급원 탐색mm/slub.c:4839–4869
refill / 새 slab현재 CPU 목록이 부족partial slab 또는 new_slab() 확보 시도, 최종 실패 시 NULL 가능mm/slub.c:4545–4818

구조체와 필드: 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 선택적 sheaves는 이 CPU slab 관계도보다 앞선 별도 객체 후보.

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

표 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 / sheaves / CPU slab / refill
post-hook객체 또는 NULL
해제
사용 종료객체 접근을 먼저 멈춤
kmem_cache_free()cache 확인과 해제 hook
재사용 가능이전 포인터 사용 금지

해제 입구: mm/slub.c:6825–6833. 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·sheaves·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, sheaves, CPU freelist, partial, new slab 중 어디에서 왔는가? API tracepoint만으로 구분할 수 있는가?
  • USE_LOCKLESS_FAST_PATH()가 거짓이거나 tid 갱신 실패 시 어디로 이동하는가?
  • 실패 반환과 해제 뒤 호출자 객체 수명 처리는 올바른가?
  • debug/sanitizer 설정이 관측한 배치와 성능을 바꾸는가?