kmalloc()으로 받은 작은 객체 하나마다 struct page를 새로 allocation하지는 않습니다. slab allocator는 backing page를 여러 object로 나누고, 그 backing storage의 관리 정보를 page descriptor와 겹쳐 사용합니다. 그래서 slab code를 읽다 보면 struct page *와 struct slab *가 같은 주소를 가리키는 장면이 나타납니다.
이것은 우연히 크기가 맞는 두 구조체를 바꿔 쓰는 기법이 아닙니다. offset, alignment, type 판별과 수명에 대한 제약을 맞춘 overlay입니다. Linux v6.1에서 볼 수 있는 SLAB·SLUB·SLOB 분기를 비교하면 각 필드가 왜 필요한지도 드러납니다.
Member가 아니라 다른 해석입니다
struct slab는 struct page 안에 들어 있는 별도 객체가 아닙니다. backing page의 descriptor 저장 공간을 slab 관리용 형태로 해석합니다. page_slab()와 slab_page() 같은 helper는 이 표현을 오갑니다. helper 호출은 새 slab을 만들거나 참조를 추가하지 않으므로, page가 정말 해당 용도로 사용 중인지와 lifetime을 호출자가 보장해야 합니다.
이 배치를 유지하기 위해 SLAB_MATCH 계열의 compile-time assertion이 공유 필드의 offset을 확인합니다. flags와 reference 관련 위치가 맞아야 하고 전체 struct slab도 descriptor 공간을 넘을 수 없습니다. allocator별 필드를 추가할 때 struct slab만 보면 충분하지 않은 이유입니다. underlying struct page의 계약도 함께 지켜야 합니다.
v6.1의 allocator별 선언은 CONFIG_SLAB·CONFIG_SLUB·CONFIG_SLOB의 조건 분기로 선택합니다. 어느 분기도 선택되지 않으면 다른 layout을 추측해 진행하는 것이 아니라 compile-time #error가 됩니다. 실행 중인 slab마다 이 세 layout을 자유롭게 전환하는 구조가 아닙니다.
낮은 bit 충돌도 피해야 합니다. page descriptor의 metadata 첫 word bit 0은 tail 판별에 쓰입니다. v6.1에서 이 공간을 공유하는 list·RCU 연결은 정렬 조건으로 잘못된 tail 판별을 피합니다. 한편 slab_cache는 일반 page의 mapping과 겹치는 위치에 있어 anonymous·movable mapping tag와 충돌하지 않아야 합니다. tail tag와 mapping tag는 서로 다른 위치의 제약입니다. v6.18의 offset assertion을 이 옛 배치에 그대로 대입해서도 안 됩니다.
flags와 refcount, configuration에 따른 memcg 정보는 backing page의 공통 관리와 연결됩니다. 그렇다고 slab 안의 individual object reference count가 이 page refcount와 같지는 않습니다. object를 kfree()하는 사건, slab이 비는 사건, backing page를 page allocator에 반환하는 사건은 서로 다른 단계입니다.
List와 RCU는 서로 다른 수명 단계에서 유효합니다
slab은 가용 object가 남아 있는지 등에 따라 allocator의 list에 들어갑니다. slab_list가 그 연결을 표현합니다. 일부 경로에서는 같은 저장 공간을 rcu_head로 사용하여 지연 반환 callback을 연결할 수 있습니다. 두 member가 union에 있다는 것은 두 가지 역할을 동시에 독립적으로 수행할 수 없다는 뜻입니다.
따라서 list에서 제거되었다는 사실만으로 모든 reader가 더 이상 slab을 볼 수 없다고 단정하면 안 됩니다. RCU를 사용하는 cache라면 정해진 reader protocol과 grace period를 고려해야 합니다. 반대로 rcu_head라는 필드가 존재한다고 모든 slab cache가 항상 RCU 지연 해제를 한다고 말할 수도 없습니다. 해당 cache의 flags와 실제 반환 경로를 따라가야 합니다.
Legacy SLAB는 index와 object 시작점을 관리합니다
v6.1의 legacy SLAB 배치에는 freelist를 나타내는 index 정보, object storage의 시작을 나타내는 s_mem, 사용 상태를 나타내는 active 등이 들어갑니다. backing page 안에서 어떤 object가 비었는지 추적하고 다음 allocation에 연결하려면 object 크기·개수와 freelist 표현이 함께 필요합니다.
여기서 freelist의 표현을 SLUB의 object pointer chain과 같은 것으로 읽으면 안 됩니다. 이름은 같아도 allocator가 object 번호를 다루는지 실제 pointer를 다루는지에 따라 해석이 달라집니다. page descriptor의 공통 영역은 재사용하지만 그 위에서 동작하는 allocation algorithm까지 공통이라는 뜻은 아닙니다.
이 index 배열을 FIFO queue라고 단정하는 것도 맞지 않습니다. v6.1의 slab_get_obj()는 active 위치의 free index를 읽고 active를 증가시킵니다. slab_put_obj()는 active를 감소시킨 위치에 반환 object의 index를 기록합니다. 같은 slab에서 다른 조작 없이 두 object를 차례로 반환하고 다시 얻는 경우에는 마지막으로 반환한 쪽을 먼저 읽게 됩니다. CPU cache와 batch 이동까지 포함한 전체 재사용 순서를 이 작은 경로 하나로 고정할 수는 없지만, 적어도 이 배열에 FIFO 보장을 부여해서는 안 됩니다.
SLUB는 freelist와 counter를 함께 갱신합니다
SLUB 배치에서 핵심은 freelist와 counters의 인접성입니다. counter에는 inuse, objects, frozen 같은 상태가 묶여 있습니다. v6.1의 해당 bitfield는 각각 16·15·1 bit로 표현됩니다. 이 배치도 다른 tag의 고정 ABI는 아닙니다. inuse는 사용 중인 object 수, objects는 slab이 제공하는 object 수를 나타내며, frozen은 CPU 쪽 관리와 slab의 상태 전환에 관여합니다. 이 bit 하나를 “모든 CPU 접근을 막는 lock”으로 읽는 것은 부정확합니다.
frozen slab은 일반 node list 관리에서 제외하고 해당 slab을 freeze한 CPU가 list 조작과 freelist에서의 획득을 담당합니다. 다른 CPU가 object를 freelist로 반환하는 것까지 금지하는 상태는 아닙니다. 따라서 frozen을 모든 외부 접근을 차단하는 배타 lock과 같다고 보면 remote free와 counter 갱신을 설명할 수 없습니다.
지원 architecture에서는 freelist와 counter를 한 번의 double-word compare-and-exchange로 다루는 경로가 있습니다. freelist만 갱신하고 counter가 이전 상태로 남는 상황을 피해야 하므로 배치와 정렬이 algorithm의 일부가 됩니다. 지원 여부나 debug 설정에 따라 다른 동기화 경로도 필요하므로, 이 구조를 보았다고 slab의 모든 연산을 lock-free라고 일반화할 수는 없습니다.
CONFIG_SLUB_CPU_PARTIAL이 켜진 경우에는 CPU가 partial slab을 연결하기 위한 next와 slabs 같은 자료가 나타납니다. 이 역시 slab_list 등과 공간을 공유합니다. partial list의 한 slab인지, node list에 속한 slab인지, 지연 반환 중인 slab인지에 따라 유효한 member가 달라지는 것입니다.
이때 slabs는 시스템 전체나 kmem_cache 전체의 slab 수가 아니라 CPU partial chain에서 남은 slab 수를 추적하는 값입니다. 공통 tail 쪽의 __page_refcount는 backing descriptor의 reference 위치와 겹치며, memcg_data는 CONFIG_MEMCG가 켜졌을 때만 나타납니다. allocator별 __unused member도 새로운 독립 상태라기보다 공유 offset을 맞추는 배치의 일부입니다.
SLOB의 free unit과 block header를 혼동하지 않습니다
v6.1의 SLOB 관련 배치에는 남아 있는 free unit 수를 나타내는 units와 첫 free slob_block을 가리키는 freelist가 있습니다. 이 slab.units를 SLOB storage 안의 개별 slob_block header와 같은 것으로 읽으면 안 됩니다. 전자는 slab 전체의 여유량이고, 후자는 작은 free block의 크기나 다음 free block 위치를 표현하는 allocator 내부 자료입니다.
특히 slob_block의 signed 값과 offset 표현을 설명하면서 descriptor 전체의 units까지 음수면 pointer라는 식으로 확장하면 구조의 층위가 뒤섞입니다. backing page 장부, page 안의 free block header, 실제 user object라는 세 층을 나누어 보면 각 숫자가 무엇을 세는지 명확해집니다.
v6.18의 배치는 별도로 읽습니다
SLAB·SLUB·SLOB의 비교는 v6.1의 분기를 이해하는 데 유용하지만, 이 세 구현이 이후 kernel에서도 그대로 configuration 선택지로 남아 있다고 가정하면 안 됩니다. v6.18의 mm/slab.h는 과거의 세 allocator별 layout을 같은 형태로 나열하지 않습니다. slab overlay와 offset assertion은 이어지지만 내부 필드와 최적화는 달라집니다.
실제 문제를 조사할 때는 먼저 해당 kernel tag와 configuration을 확인하고, 그 버전의 struct slab와 변환 helper를 읽는 순서가 좋습니다. dump에서 freelist처럼 보이는 word를 찾는 일보다, 그 word가 현재 어떤 수명 단계에서 어떤 allocator의 상태인지를 확인하는 일이 먼저입니다.
전체 선언과 변환을 함께 읽습니다
아래는 Linux v6.1의 mm/slab.h에 있는 struct slab과 인접한 배치 assertion 전문입니다. SPDX는 GPL-2.0이며 들여쓰기를 정리했습니다. 세 allocator의 configuration 분기와 미선택 오류, 선택적인 memcg 위치를 함께 포함합니다.
/* Reuses the bits in struct page */
struct slab {
unsigned long __page_flags;
#if defined(CONFIG_SLAB)
union {
struct list_head slab_list;
struct rcu_head rcu_head;
};
struct kmem_cache *slab_cache;
void *freelist; /* array of free object indexes */
void *s_mem; /* first object */
unsigned int active;
#elif defined(CONFIG_SLUB)
union {
struct list_head slab_list;
struct rcu_head rcu_head;
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct {
struct slab *next;
int slabs; /* Nr of slabs left */
};
#endif
};
struct kmem_cache *slab_cache;
/* Double-word boundary */
void *freelist; /* first free object */
union {
unsigned long counters;
struct {
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
unsigned int __unused;
#elif defined(CONFIG_SLOB)
struct list_head slab_list;
void *__unused_1;
void *freelist; /* first free block */
long units;
unsigned int __unused_2;
#else
#error "Unexpected slab allocator configured"
#endif
atomic_t __page_refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
};
#define SLAB_MATCH(pg, sl) \
static_assert(offsetof(struct page, pg) == offsetof(struct slab, sl))
SLAB_MATCH(flags, __page_flags);
SLAB_MATCH(compound_head, slab_list); /* Ensure bit 0 is clear */
#ifndef CONFIG_SLOB
SLAB_MATCH(rcu_head, rcu_head);
#endif
SLAB_MATCH(_refcount, __page_refcount);
#ifdef CONFIG_MEMCG
SLAB_MATCH(memcg_data, memcg_data);
#endif
#undef SLAB_MATCH
static_assert(sizeof(struct slab) <= sizeof(struct page));
SLAB_MATCH(compound_head, slab_list)는 tail tag와 충돌하지 않도록 같은 slot을 확인합니다. RCU 연결의 assertion은 SLOB에서 제외됩니다. 이 tag의 실제 assertion 목록에 없는 최신 field를 덧붙여 같은 source라고 제시하지 않습니다.
변환도 같은 header의 실제 macro 전문으로 연결합니다.
#define folio_slab(folio) (_Generic((folio), \
const struct folio *: (const struct slab *)(folio), \
struct folio *: (struct slab *)(folio)))
#define slab_folio(s) (_Generic((s), \
const struct slab *: (const struct folio *)s, \
struct slab *: (struct folio *)s))
#define page_slab(p) (_Generic((p), \
const struct page *: (const struct slab *)(p), \
struct page *: (struct slab *)(p)))
#define slab_page(s) folio_page(slab_folio(s), 0)
page_slab()은 compound의 임의 tail을 알아서 head로 정규화하는 함수가 아닙니다. 이미 첫 descriptor임을 아는 문맥의 wrapper입니다. folio_slab()도 해당 folio가 slab이라는 전제 아래 다른 type으로 해석합니다. 변환에 성공했다는 것과 타입의 전제·수명·lock이 확보되었다는 것은 다른 질문입니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access, 2025, Physical Memory 장.
- Linux
v6.1: struct slab와 SLAB_MATCH, SLUB 구현, legacy SLAB 구현, SLOB 구현. - Linux
v6.18: 현재 tag의 slab overlay.