what is slab
당연히 커널도 객체들이 필요하기에 여러 메모리가 필요할텐데 이를 stack으로는 감당 불가능 할것이다. 즉, 유저들과 똑같이 동적 메모리가 필요한데 커널에서 이러한 동적메모리 관리를
슬랩 할당자(SLAB Allocator) 를 통해 구현한다.
커널 개발자는 kmalloc()을 호출해 슬랩 할당자로부터 동적으로 메모리를 할당받을 수 있고, kfree()를 호출해 할당받은 메모리를 해제할 수 있다.
이러한 슬랩 할당자는 과거 SLAB, SLUB, SLOB 할당자로 나뉘었는데, SLUB 할당자의 발전으로 다른 할당자를 충분히 대체할 수 있게 됨으로써 걍 나머지는 사장 되었다. 이제 최신 커널 기준으로 슬랩 할당자는 SLUB을 의미한다.
슬랩 할당자의 주요 구성 요소는 다음과 같다.
- 슬랩 객체(Slab Object): 커널에서
task_struct나mm_struct등을kmalloc()으로 할당받을 때 반환되는 객체로, 슬랩 할당자는 할당을 효율적으로 관리하기 위해 여러 조건에 따라 정의된 크기의 슬랩 객체를 미리 할당해 놓음 - 슬랩 페이지(Slab Page): 버디 할당자(Buddy Allocator)로부터 페이지를 할당받아 구성되며, 이 슬랩 페이지 안에는 동일한 크기의 슬랩 객체들이 위치하게 된다. → 이걸 그냥 slab 이라고 주로 한다.
버디 할당자(Buddy Allocator)란?
페이지 프레임 단위(4KiB)로 메모리를 할당해주는 할당자로, 보통 큰 메모리 블록을 관리하기 위한 할당자입니다. 슬랩 할당자가 슬랩 페이지를 구성할 때 이 버디 할당자로부터 페이지를 받아온 뒤, 슬랩 페이지를 슬랩 객체로 분할하여 사용합니다.
- 슬랩 캐시(Slab Cache): 특정 크기의 객체로 구성된 슬랩 페이지의 집합
- Per-CPU & Per-Node: 슬랩 캐시는 빠른 할당을 위해 슬랩 페이지를 노드별로, CPU별로 관리합니다. ( 노드는 numa 시스템에서 cpu 묶음을 의미 )
- Freelist: 해제된 슬랩 객체들끼리 연결되는 리스트
- Partial list: 슬랩 페이지에 속한 슬랩 객체가 할당될 경우, 그 슬랩 페이지는 이 리스트에서 관리된다, 즉, 해당 캐시 ( kmalloc-512 ) 에 존재하는 여러 slab들을 총괄리하는 얘임
슬랩 캐시는 슬랩 페이지의 집합이고, 슬랩 페이지는 동일한 크기의 슬랩 객체로 구성되어 있다. 아래는 대표적인 슬랩 캐시이며, 다음과 같이 슬랩 객체의 크기별로 캐시가 분리되어 있다.
좀 더 쉽게 말하자면, • kmalloc-512 같은 slab cache는 내부적으로 “슬랩(slab)”을 여러 개 들고 있음 → SLUB에서 “슬랩(slab)”은 보통 버디에서 받아온 pagesperslab 페이지 묶음 하나를 말한다. 이 pagesperslab 묶음은 설정에 따라 다른데
- pagesperslab = 1이면 slab = 1 page(4KB)
- pagesperslab = 4이면 slab = 4 pages(16KB)
처럼 정의가 된다.
직접 슬럽캐시들을 보자면,
# cat /proc/slabinfo | grep "kmalloc"
...
kmalloc-8k 450 502 16384 2 8 : tunables 0 0 0 : slabdata 251 251 0
kmalloc-4k 2597 3068 8192 4 8 : tunables 0 0 0 : slabdata 767 767 0
kmalloc-2k 2224 3968 4096 8 8 : tunables 0 0 0 : slabdata 496 496 0
kmalloc-1k 4921 8608 2048 16 8 : tunables 0 0 0 : slabdata 538 538 0
kmalloc-512 28209 44128 1024 32 8 : tunables 0 0 0 : slabdata 1379 1379 0
kmalloc-256 37684 87040 512 32 4 : tunables 0 0 0 : slabdata 2720 2720 0
kmalloc-192 150168 209824 256 32 2 : tunables 0 0 0 : slabdata 6557 6557 0
kmalloc-128 21693 23968 256 32 2 : tunables 0 0 0 : slabdata 749 749 0
kmalloc-96 60864 60864 128 32 1 : tunables 0 0 0 : slabdata 1902 1902 0
kmalloc-64 199168 199168 128 32 1 : tunables 0 0 0 : slabdata 6224 6224 0
kmalloc-32 17173 28544 64 64 1 : tunables 0 0 0 : slabdata 446 446 0
kmalloc-16 27226 46464 32 128 1 : tunables 0 0 0 : slabdata 363 363 0
kmalloc-8 31277 47022 40 102 1 : tunables 0 0 0 : slabdata 461 461 0
리눅스의 /proc/slabinfo를 확인해볼 수 있고 현재 시스템에서 사용 중인 슬랩 캐시의 목록을 확인할 수 있다. 이 중 kmalloc-N 캐시는 다음과 같이 GFP_KERNEL 플래그로 kmalloc()을 호출했을 때 사용되는 캐시들이다.
각각의 값을 해석하자면
kmalloc-256 37684 87040 512 32 4 : tunables 0 0 0 : slabdata 2720 2720 0으로 보자면
| 항목 | 값 | 의미 |
|---|---|---|
| name | kmalloc-256 | 이 슬랩 캐시 이름(대개 “이 근처 크기 kmalloc 요청”을 처리하는 캐시) |
| active_objs | 37684 | 현재 사용 중(in-use) 객체 개수 |
| num_objs | 87040 | 캐시에 존재하는 전체 객체 개수(in-use + free) |
| objsize | 512 | 이 캐시의 실제 객체 크기(바이트) |
| objperslab | 32 | 슬랩 1개(= pagesperslab 페이지 묶음) 안에 들어가는 객체 수 |
| pagesperslab | 4 | 슬랩 1개를 구성하는 페이지 수 |
| tunables: limit | 0 | SLAB(구형)에서 주로 쓰던 튜닝 값들. SLUB에선 0으로 보이는 경우가 흔함(필드 자체는 slabinfo 포맷의 일부). |
| tunables: batchcount | 0 | 위와 동일 |
| tunables: sharedfactor | 0 | 위와 동일 |
| slabdata: active_slabs | 2720 | 활성(사용 중 객체가 있는) 슬랩 개수 |
| slabdata: num_slabs | 2720 | 캐시에 존재하는 전체 슬랩 개수 |
| slabdata: sharedavail | 0 | slabinfo 포맷에 있는 값(문서화가 빈약/버전별 의미가 애매할 수 있음). |
다음과 같다.
그리고 각각의 개념에 대해 좀더 자세히 알아 보자면, 각 캐시별로 이런 구조체가 존재하는데 , 당연히 각 cpu마다 있음
struct kmem_cache_cpu {
void **freelist; // 다음에 줄 free object (CPU가 바로 pop)
struct slab *slab; // 지금 이 CPU가 할당에 쓰는 slab (active slab)
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct slab *partial; // partially allocated slabs (per-CPU partial)
#endif
}구조체는 다음과 같은데
freelist는 알다시피 바로 사용할 수 있는 현재 active slab에 대한 free slab object 들에 대한 연결리스트이다. 이 freelist는 사실 깊게 들어가보면 2개가 존재한다.
- CPU freelist (per-CPU freelist)
- slab 쪽 freelist(=active slab에 남아있는 free object들)
이게 무슨 말이냐면, 사실 둘을 전혀 다른게 아니라 같은 freelist를 가리킨다. 근데 중요한건 멀티스레드, 멀티코어를 지원하는 최신 cpu 기술에 따라 위 두개가 동기화가 되지 않을때가 상당히 많다.
따라서, 기본적으로 1번에서 빠르게 사용하다가 1번 리스트가 비면 2번 리스트를 확인함으로써 정말로 비어져 있는지 확인을 한다. 만약 남아있다면 해당 리스트를 1번 리스트로 가져와서 사용하는 느낌이다.
근데, 근본적으로 왜 동기화가 되지 않는 걸까?
그 이유는 remote free 때문인데 A cpu에서 kmalloc을 했을때 해당 cpu가 사용하는 slab에서 가져올 것이다. ( 나중에 다룸 ) 근데, free를 할때는 다른 B cpu에서 한다면? 그럼 빠르게 사용되는 A cpu에 있는 CPU freelist 에 굳이 동기화 시킬 필요는 없다. 그냥 slab에 있는 freelist에 free됬다고만 체크할뿐이다.
freelist에 대해서 뭔 느낌인지 알겠어 그럼 구조체에 slab ( active-slab) 은 뭔데?
얘는 cpu가 현재 가장 애용하는 slab 들에 가리킨다.
당연히 캐시별로 각각의 active-slab 이존재한다.
아까도 말했듯 slab에 존재하는 freelist도 여기에 있다. 이 active-slab이 꽉차면 partial-list에서 가져오는 건데
이 partial 이라는 개념을 헷갈리면 안되는데
cpu에서 관리하는 slab들에 대해서
- Full slab: free object가 0개(= 더 이상 alloc 못 함)
- Partial slab: free object가 남아있음(= alloc 가능)
- Empty slab: 모든 object가 free (정책에 따라 유지하거나 버디로 반납)
이런 상태가 존재한다고 보면 된다. 따라서 이러한 slab들중 Partial 상태인 slab들을 연결해놓은게 ( freelist는 객체들이었지만, 얘는 slab( 페이지 ) 들의 리스트 ) partial-list 이다.
freelist: “free object들의 체인”
per-CPU partial slab list: “free object가 아직 남아있는 slab(페이지 묶음) 들의 리스트”
흐름을 정리해보자면 다음과 같다.
kmalloc(size) → 버디까지 가는 전체 흐름
0) Size-class(캐시) 선택
- kmalloc(size)가 들어오면 커널이 size를 올림(round up) 해서, 그 크기를 담당하는 kmalloc- slab cache(예: kmalloc-512)를 선택한다.
→ 즉, 위와 같이 300byte의 할당을 요청하면, 요청한 크기에 해당하는 kmlloc-512 캐시로부터 객체가 할당됩니다. 만약 150byte를 요청하면 kmalloc-192 캐시를, 20byte를 요청하면 kmalloc-32 캐시를 사용하게 됩니다.
1) 가장 빠른 경로: per-CPU freelist
- 현재 CPU의 kmem_cache_cpu에 있는 per-CPU freelist(free object 체인)를 확인한다.
- freelist가 비어있지 않으면 → head에서 object 하나 pop 해서 바로 반환한다. (끝)
2) active slab에서 리필(이 CPU가 “현재 쓰는 slab”)
- per-CPU freelist가 비어있으면, 이 CPU가 들고 있는 active slab(현재 할당에 쓰는 slab)을 확인한다.
- active slab 안에 free object가 남아있으면 → 그 slab의 freelist에서 object들을 일부/전체를 per-CPU freelist로 옮겨 리필(refill) 한다.
- 리필된 per-CPU freelist에서 object 하나 pop 해서 반환한다. (끝)
3) (옵션) per-CPU partial slab list에서 slab 가져오기
- active slab에도 남은 free object가 없으면(=active slab이 사실상 full) → per-CPU partial slab list를 본다.
- 거기에 partial slab(=free object가 남아있는 slab) 이 있으면 → 그 slab을 꺼내 새 active slab로 교체한다.
- 새 active slab의 freelist에서 per-CPU freelist를 리필한 뒤, object 하나 pop 해서 반환한다. (끝)
4) per-node partial list(중앙 리스트)에서 slab 가져오기
- per-CPU partial에도 없으면 → 같은 NUMA node의 per-node partial list(공유 리스트)를 락 잡고 확인한다.
- partial slab을 찾으면 → 가져와서 active slab로 설정하고, freelist를 리필한 뒤 object 하나 pop 해서 반환한다. (끝)
5) 최종: 버디로 새 slab(페이지 묶음) 할당
- per-node partial에도 usable slab이 없으면 → 버디 할당자로 가서, 해당 cache의 설정(pagesperslab)에 맞는 페이지 묶음을 새로 할당받는다.
- 그 페이지 묶음을 SLUB이 object 크기로 쪼개서 freelist를 구성하고, 이를 새 slab로 초기화한다.
- 이 새 slab을 active slab로 설정하고, per-CPU freelist를 리필한 뒤 object 하나 pop 해서 반환한다. (끝)
핵심 한 줄 요약
- “내 손(per-CPU freelist) → 내가 쓰는 판(active slab) → 내가 로컬로 쌓아둔 판들(per-CPU partial) → 중앙 창고(per-node partial) → 새로 주문(버디)”
커널 코드와 함께 분석