전체 흐름: 이미 있는 cache에서 객체 하나를 얻는다
SLUB는 동일 크기 객체를 담는 struct kmem_cache *s를 입력받는다. kmem_cache_alloc()는 cache의 레이아웃을 만드는 함수가 아니라 준비된 cache에 객체를 요청한다. 내부 후보는 조건에 따라 갈린다.
이 그림은 호출 경계다. 객체 공급원은 뒤의 조건부 분기 그림을 본다. mm/slub.c:3489–3504.
| 범위 | 입력 | 결과 | 고정 원본 |
|---|---|---|---|
| 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
설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
GitHub 원본:
대표 코드
kmem_cache_alloc()과 tracepoint
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()의 후보 순서
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을 할당하는 것은 아니다.
실제 저장소 크기나 실측 trace가 아닌 분기 도식이다. 코드: mm/slub.c:3329–3402.
| 경계 | 조건 | 결과 / 다음 경로 | 원본 |
|---|---|---|---|
| CPU-local 후보 | KFENCE가 객체를 주지 않고 CONFIG_SLUB_TINY=n | c→freelist, c→slab, node, lockless 조건을 함께 검사 | mm/slub.c:3329–3375 |
| 빠른 pop | 후보 유효 + tid·freelist 비교 교환 성공 | head 객체 반환; 비교 교환 실패면 redo | mm/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는 갱신 재시도를 판별한다.
S0/S1과 A–D/P0/P1은 설명용 객체 표식이다. 실제 주소나 객체 수를 뜻하지 않는다. mm/slub.c:3329–3402.
| 대상 | 읽는 법 | 주의 |
|---|---|---|
| struct kmem_cache *s | 크기·flags와 cache별 자료 | kmem_cache_alloc은 cache 생성이 아니다. |
| c→freelist / c→slab | 해당 CPU의 즉시 할당 후보와 현재 slab | 비었거나 node가 맞지 않으면 fast path가 아니다. |
| c→tid | CPU-local 갱신 변화 감지 | 비교 교환 실패는 객체 획득이 아니라 재시도다. |
| slab partial / free pointer | 재사용 가능한 객체 공급원 | partial과 새 slab은 조건부 fallback이다. |
할당과 해제는 별개의 수명 경로
CPU freelist 적중과 slow path refill은 한 요청의 연속 단계가 아니다. 해제는 같은 호출의 마지막 단계가 아니라 호출자가 사용을 마친 뒤 진입하는 다른 API다.
해제 입구: mm/slub.c:3825–3833. KFENCE·KASAN·debug 설정에 따라 내부 free 동작은 달라질 수 있다.
| 상황 | 결과 | 검토 질문 |
|---|---|---|
| pre-hook 거부 | 후보 진입 전에 NULL | 호출자가 NULL을 처리하는가? |
| 후보 성공 | post-hook 뒤 객체 반환 가능 | hook 뒤에도 반환 객체가 유효한가? |
| refill 실패 | NULL 반환 가능 | GFP flags와 오류 처리를 확인했는가? |
| kmem_cache_free() | 객체 사용 종료 후 해제 | 이전 포인터 접근이 남았는가? |
불변 조건과 실패 사례
객체 사용 중 대응 cache가 파괴되면 안 된다. 해제 뒤 이전 포인터를 다시 읽거나 쓰지 않는다.
lockless 갱신은 허용 구성, freelist·slab·node 판정과 tid 비교 교환에 의존한다.
pre-hook이나 slab 확보가 실패할 수 있다. post-hook 뒤 최종 반환값을 확인한다.
redzone, poison, KASAN, KFENCE는 검사 지점과 배치·성능을 바꿀 수 있다.
CPU-local freelist 보호는 호출자 객체의 lifetime 보장과 다르다.
같은 주소가 재등장해도 이전 객체 포인터의 use-after-free가 정당화되지 않는다.
계측과 검증: 도구마다 보이는 범위가 다르다
API tracepoint는 요청과 결과를 보여 주지만 KFENCE·CPU freelist·refill 가운데 성공 후보를 확정하지 않는다. sanitizer와 debug 옵션은 오류 검출 도구이지 내부 분기 시간을 모두 재는 시계가 아니다.
| 도구 | 볼 수 있는 것 | 그것만으로 알 수 없는 것 |
|---|---|---|
| /proc/slabinfo | cache별 객체·slab 집계 상태 | 한 요청의 분기나 실패 지점 |
| slub_debug=FZPU | F 검사, Z redzone, P poison, U 사용자 추적 등 활성 구성의 손상 증거 | 모든 할당의 fast/slow 분기; debug는 배치도 바꿀 수 있음 |
| kmem:kmem_cache_alloc/free | tracepoint가 제공될 때 cache API 사건 | 내부 freelist·refill 원인 |
| kmem:kmalloc | kmalloc 계열 사건 | 명시적 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 설정이 관측한 배치와 성능을 바꾸는가?