전체 흐름: 이미 있는 cache에서 객체 하나를 얻는다
SLUB는 동일 크기 객체를 담는 struct kmem_cache *s를 입력받는다. kmem_cache_alloc_noprof()는 cache의 레이아웃을 만드는 함수가 아니라 준비된 cache에 객체를 요청한다. 내부 후보는 조건에 따라 갈린다.
이 그림은 호출 경계다. 객체 공급원은 뒤의 조건부 분기 그림을 본다. mm/slub.c:5352–5361.
| 범위 | 입력 | 결과 | 고정 원본 |
|---|---|---|---|
| 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 경로로 내려간다.
휴대전화에서는 그림을 좌우로 밀어 전체 흐름을 볼 수 있다.
그림 크게 열기 · 분기 설명용 도식이며 실제 주소 배치나 실측 trace가 아니다.
아래 표도 좁은 화면에서는 좌우로 이동한다.
| 단계 | 판정 / 입력 | 상태 변화와 다음 경로 | 원본 위치 |
|---|---|---|---|
| 진입 | slab_pre_alloc_hook(), KFENCE 결과 | KFENCE가 객체를 반환하면 후처리로 간다. 아니면 s->cpu_sheaves의 유무를 본다. | mm/slub.c:5317–5335 |
| 선택적 sheaves | local lock, main sheaf의 크기, 요청 NUMA node | 적중하면 마지막 object를 꺼내 size를 줄인다. 실패 또는 비활성화 시 __slab_alloc_node()를 호출한다. | mm/slub.c:5162–5230, 5331–5335 |
| CPU freelist | c->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
설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
GitHub 원본:
대표 코드
kmem_cache_alloc_noprof
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
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을 할당하는 것은 아니다.
실제 저장소 크기나 실측 trace가 아닌 분기 도식이다. 코드: mm/slub.c:4871–4965.
| 경계 | 조건 | 결과 / 다음 경로 | 원본 |
|---|---|---|---|
| CPU-local 후보 | KFENCE·선택적 sheaves가 객체를 주지 않음 | c→freelist, c→slab, node, lockless 조건을 함께 검사 | mm/slub.c:4871–4939 |
| 빠른 pop | 후보 유효 + tid·freelist 비교 교환 성공 | head 객체 반환; 비교 교환 실패면 redo | mm/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는 갱신 재시도를 판별한다.
S0/S1과 A–D/P0/P1은 설명용 객체 표식이다. 실제 주소나 객체 수를 뜻하지 않는다. mm/slub.c:4871–4965.
| 대상 | 읽는 법 | 주의 |
|---|---|---|
| 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:6825–6833. 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·sheaves·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, sheaves, CPU freelist, partial, new slab 중 어디에서 왔는가? API tracepoint만으로 구분할 수 있는가?
USE_LOCKLESS_FAST_PATH()가 거짓이거나 tid 갱신 실패 시 어디로 이동하는가?- 실패 반환과 해제 뒤 호출자 객체 수명 처리는 올바른가?
- debug/sanitizer 설정이 관측한 배치와 성능을 바꾸는가?