전체 흐름
스택, slab, heap은 하나의 순서로 변환되는 단계가 아니다. 스택은 스레드 실행 공간, slab은 같은 크기의 블록 저장소, heap은 가변 크기 할당자다. 메모리 도메인은 별도의 접근 권한 경계이며 CONFIG_USERSPACE와 하드웨어 지원 여부에 따라 적용된다.
스택·slab·heap은 별도의 선택지다. 메모리 도메인은 사용자 모드의 접근 경계로서 할당자와 독립적이다.
| 경로 | 크기를 정할 때 | 사용 중 실패 | 반환 책임 |
|---|---|---|---|
| thread stack | 스택 객체 선언 시 | 설정에 따라 overflow 감지·치명 오류 | 스레드 생명주기와 스택 객체 수명 관리 |
| memory slab | 블록 크기와 개수 선언 시 | -ENOMEM 또는 대기 후 -EAGAIN | k_mem_slab_free() |
| k_heap | heap 영역 선언 시 | 할당 불가·timeout이면 NULL | 같은 heap에 k_heap_free() |
| memory domain | partition과 권한 정의 시 | 허용되지 않은 사용자 접근은 fault | 할당·해제와 별도로 권한 범위 관리 |
소스 코드 위치
아래 설명은 Zephyr v3.7.0의 고정 커밋 36940db938a8f4a1e919496793ed439850a221c2와 해당 버전 문서에 맞췄다. kernel/mempool.c는 이 버전에서 k_malloc() 등 system heap 경로를 구현한다. 이를 slab이나 별도 고정 블록 풀과 동일시하지 않는다.
kernel/mem_slab.ckernel/kheap.ckernel/mempool.ckernel/mem_domain.cinclude/zephyr/kernel/thread_stack.h사용 예제와 실제 계약
아래 세 코드는 API 사용을 설명하는 예제다. Zephyr 구현 코드의 발췌는 아니며, 실제 반환값과 호출 조건은 위의 v3.7.0 문서와 소스에서 확인할 수 있다.
1. 고정 크기 slab: 128바이트 × 16개
K_MEM_SLAB_DEFINE(packet_slab, 128, 16, 4);
void *block;
if (k_mem_slab_alloc(&packet_slab, &block, K_NO_WAIT) == 0) {
process_block(block);
k_mem_slab_free(&packet_slab, block);
}
K_MEM_SLAB_DEFINE(packet_slab, 128, 16, 4)는 128바이트 블록 16개를 마련하고 정렬값으로 4를 요청한다. v3.7.0의 매크로는 정렬과 블록 크기를 WB_UP()으로 머신 워드 단위에 맞춘다. 따라서 실제 정렬은 64비트 환경에서 8바이트로 커질 수 있다. 이 예제의 블록 저장 공간은 2,048바이트이며 slab 객체의 관리 필드는 별도다. K_NO_WAIT 할당이 실패하면 처리 함수를 호출하지 않는다. 정의 매크로
공식 API에서 빈 slab에 K_NO_WAIT로 요청하면 -ENOMEM, 기다리다 시간이 끝나면 -EAGAIN이다. ISR에서 호출할 때는 K_NO_WAIT만 사용한다. 성공한 블록은 같은 slab에 반환해야 한다.
2. 스택 객체: 요청 크기와 전달 크기를 구분
K_THREAD_STACK_DEFINE(parser_stack, 2048);
k_thread_create(&parser_thread, parser_stack,
K_THREAD_STACK_SIZEOF(parser_stack),
parser_entry, NULL, NULL, NULL,
7, 0, K_NO_WAIT);
K_THREAD_STACK_DEFINE가 만든 객체는 단순 char[2048]가 아니다. 아키텍처·userspace 설정에 따른 정렬, guard, 예약 공간이 붙을 수 있으므로 k_thread_create()에는 sizeof(parser_stack) 대신 K_THREAD_STACK_SIZEOF(parser_stack)를 전달한다.
3. 별도 heap: 할당 실패와 timeout
K_HEAP_DEFINE(app_heap, 4096);
void *buf = k_heap_alloc(&app_heap, len, K_MSEC(5));
if (buf != NULL) {
k_heap_free(&app_heap, buf);
}
K_HEAP_DEFINE(app_heap, 4096)는 system heap과 다른 이름 붙은 heap이다. 내부 관리 공간이 필요하므로 4,096바이트 전부가 사용자 payload가 되는 것은 아니다. 이 예제의 K_MSEC(5)는 메모리가 풀릴 때까지 스레드가 대기할 수 있다는 뜻이다. ISR 호출이라면 timeout은 K_NO_WAIT여야 한다.
소스 추적 순서
| 질문 | v3.7.0에서 볼 곳 | 확인할 계약 |
|---|---|---|
| slab이 비면? | kernel/mem_slab.c · Memory Slabs API | free list, wait queue, -ENOMEM/-EAGAIN, 반환 시 대기자 처리 |
| heap이 비면? | kernel/kheap.c · Memory Heaps API | sys_heap 할당, timeout 대기, NULL 반환 |
k_malloc는 어디로? | kernel/mempool.c | CONFIG_HEAP_MEM_POOL_SIZE 기반 system heap과 k_free |
| stack 실제 크기는? | thread_stack.h · Threads 문서 | 객체 길이, guard/reserved, K_THREAD_STACK_SIZEOF |
| user thread가 RAM을 볼 수 있나? | Memory Protection Design | domain partition, user 권한, 하드웨어 제약 |
빌드 산출물은 객체가 실제로 포함됐는지 확인하는 보조 증거다. 할당 실패의 원인을 찾을 때는 devicetree status보다 해당 slab의 사용 블록 수와 heap의 가용량·요청 크기부터 본다.
내부 동작 분석
고정 블록 slab
빈 블록을 free list로 관리한다. 128바이트 요청과 1바이트 요청 모두 이 예제에서는 블록 하나를 점유한다. 크기 단편화 대신 고정 개수의 고갈이 설계 변수다. 모든 블록이 사용 중이면 스레드는 timeout 정책에 따라 대기할 수 있다.
가변 크기 heap
k_heap는 동기화와 대기 큐를 제공하고 내부 sys_heap를 사용한다. v3.7.0 문서는 해제된 인접 free chunk의 병합과 크기별 bucket을 설명한다. 따라서 단편화 위험을 말할 수는 있지만, 할당 루프가 무제한이라고 단정하면 안 된다. deadline에는 timeout 대기와 실제 보드 측정치를 함께 넣는다.
스택 객체
스택 객체의 총 메모리와 함수에 전달할 stack size는 같지 않을 수 있다. 사용자 모드 스레드용 스택은 권한 전환용 공간과 보호 정렬 조건이 붙을 수 있다. supervisor 전용이면 K_KERNEL_STACK_DEFINE가 더 적절한지 검토한다.
메모리 도메인과 가상 메모리
메모리 도메인은 사용자 스레드에 추가 partition 접근을 부여하는 장치다. supervisor 접근을 제한하는 수단으로 해석하지 않는다. MMU 기반 virtual memory와 demand paging은 지원·설정이 필요한 별도 기능이며 모든 Zephyr 보드의 기본 전제가 아니다.
불변 조건 확인 목록
| 대상 | 지켜야 할 조건 | 검증 방법 |
|---|---|---|
| slab | 블록 크기 128은 정렬 4의 배수이고, 동시 점유는 16개 이하 | k_mem_slab_num_used_get()와 최대 사용량 측정 |
| stack | 스택 객체는 스레드 실행 기간 동안 살아 있고, 생성 인자는 K_THREAD_STACK_SIZEOF | stack usage 계측 및 생성 코드 확인 |
| heap | 할당·해제는 같은 heap의 API 쌍을 사용하고 NULL을 처리 | 소유권 추적·할당 실패 로깅 |
| user partition | user 스레드가 접근할 RAM은 domain 권한과 하드웨어 정렬 조건에 맞음 | 대상 보드의 userspace 설정·fault 재현 |
slab 블록의 상태 전이
할당 요청의 성공·실패와 블록의 소유 상태를 구분한다. 다음 그림은 위 128×16 slab 예제에서 블록 하나가 할당되고 반환되는 과정을 보여준다.
k_mem_slab_alloc/free의 블록 소유권과 빈 slab 분기K_NO_WAIT → -ENOMEM; 스레드가 기다렸다가 timeout이면 -EAGAIN. ISR은 기다리지 않는다.대기자가 없다면 반환된 블록은 첫 칸으로 돌아간다. 대기자가 있으면 우선순위와 대기 순서에 따라 블록을 넘긴다. 실패 반환은 블록 상태가 아니라 해당 요청의 결과다.
실패 조건표
| 관찰 | 뜻 | 먼저 확인할 것 |
|---|---|---|
slab -ENOMEM | 기다리지 않고 블록을 얻지 못함 | 동시 사용 블록 수, 반환 누락, burst 상한 |
slab -EAGAIN | 대기했지만 timeout | 반환자 실행 시간, 우선순위, timeout 설정 |
heap NULL | 요청 크기를 만족 못 하거나 timeout | 같은 heap의 가용량·요청 크기·해제 누락 |
| 스택 fault/overflow | 스택 경계 침범 가능 | 실제 stack 사용량, guard 설정, call depth |
| user access fault | 허용되지 않은 RAM 접근 가능 | domain membership, partition 범위·속성 |
자주 발생하는 오해
스택 객체에 sizeof를 바로 전달
sizeof(parser_stack)는 guard·예약 영역 때문에 생성 API가 요구하는 크기와 다를 수 있다. K_THREAD_STACK_SIZEOF(parser_stack)를 사용한다.
슬랩 고갈을 시스템 초기화 실패로 해석
slab 객체가 준비되어 있어도 16개가 동시에 점유되면 새 요청은 실패하거나 기다린다. 실패는 사용량·반환 지연과 먼저 연결해 본다.
heap에서 기다리는 timeout을 ISR에 사용
ISR의 slab/heap 할당은 K_NO_WAIT여야 한다. 대기가 필요한 작업은 스레드 문맥으로 넘긴다.
system heap과 이름 붙은 heap을 혼동
k_malloc()/k_free()와 k_heap_alloc()/k_heap_free()는 경로가 다르다. 포인터의 소유 heap과 해제 API를 함께 기록한다.
메모리 도메인을 할당자로 취급
partition은 user 모드 접근 권한을 기술한다. 블록을 새로 확보하거나 supervisor 접근을 제한한다는 뜻이 아니다.
Linux와 비교할 때의 경계
Zephyr를 Linux의 페이지 캐시·프로세스 가상 주소 공간을 축소한 버전으로 보면 slab과 heap의 역할을 혼동하기 쉽다. 이 글의 slab은 고정 크기 블록 API, heap은 가변 크기 allocator, stack은 스레드 실행 공간이다. Zephyr에도 MMU 기반 virtual memory와 demand paging 문서가 있지만 해당 기능은 플랫폼·설정에 의존한다.
따라서 비교의 기준은 운영체제 이름보다 실제 빌드 설정과 하드웨어다. CONFIG_MMU, CONFIG_USERSPACE, heap 크기, stack 보호 설정이 무엇인지 먼저 확인한다.
메모리 요청의 시간 축
| 시점 | slab 예제 | heap 예제 | stack 예제 |
|---|---|---|---|
| 정의 | 128×16 블록 저장소 | 4,096바이트 요청 영역 | parser_stack 객체 |
| 커널 준비 | free list 초기화 | 내부 sys_heap 초기화 | 스레드 생성 전 객체 유효 |
| 사용 | 블록 하나 할당·반환 | len만큼 할당·반환 | 스레드 호출 프레임 저장 |
| 압력 | 16개 점유 시 대기·실패 | 공간 부족/timeout 시 NULL | 크기 초과 시 보호 방식에 따라 fault |
이 표의 각 열은 별도 자원의 수명 주기다. 한 열이 다른 열로 순차 이동한다는 뜻은 아니다.
Bring-up 문제 사례
부하가 몰릴 때 slab만 실패한다
성공·실패 호출 수와 동시에 점유한 블록 수를 기록한다. 16개 상한에 닿았는지, 소비자가 k_mem_slab_free()를 늦게 하거나 잊었는지 확인한다. 무턱대고 timeout을 늘리면 요청 지연만 늘 수 있다.
heap은 남아 보이는데 큰 요청이 NULL이다
총 free bytes만으로는 해당 크기의 연속 chunk가 있다는 뜻이 아니다. 요청 크기 분포, 해제 패턴, 동일 heap 사용 여부와 timeout을 함께 기록한다.
사용자 스레드에서만 접근 fault가 난다
주소가 유효해도 사용자 모드에서 접근이 허용된 영역인지는 별도로 확인해야 한다. 스레드 자신의 스택은 사용할 수 있으며, 같은 도메인의 다른 사용자 스택은 아키텍처와 CONFIG_MEM_DOMAIN_ISOLATED_STACKS 설정에 따라 공유되거나 격리된다. partition으로 추가한 영역은 범위·접근 속성과 스레드의 도메인 소속을 확인한다. 모든 RAM 접근에 별도 partition이 필요하다는 뜻은 아니다. 사용자 스택 접근 규칙
추가 측정 항목
- slab:
k_mem_slab_num_used_get()및 최대 사용량으로 16개 상한의 여유를 확인한다. - heap: 요청 크기 분포와
NULL빈도, timeout 대기 시간을 같은 부하에서 측정한다. - stack: 실제 최대 사용량을 stack 분석 도구와 함께 확인하고 보호 설정별 총 객체 비용을 비교한다.
- userspace: 같은 접근을 supervisor/user 모드에서 나누어 시험해 권한 오류와 메모리 부족을 분리한다.
코드 리뷰 확인 사항
- 이 포인터는 stack·slab·이름 붙은 heap·system heap 중 어디서 왔는가? 누가 언제 돌려주는가?
- 최악 부하에서 동시에 필요한 slab 블록 수와 stack depth는 얼마인가?
- ISR에서 호출하는 slab/heap API의 timeout은
K_NO_WAIT인가? k_mem_slab_alloc()의 오류 코드와k_heap_alloc()의NULL을 각각 처리하는가?- 사용자 스레드가 접근할 영역은 자신의 스택인가, 공유 스택인가, partition으로 추가한 영역인가? 해당 접근 권한과 하드웨어 제약을 확인했는가?
디버깅 및 계측 지점
먼저 보드·설정 값을 기록한다: CONFIG_HEAP_MEM_POOL_SIZE, CONFIG_USERSPACE, CONFIG_MMU, stack 보호/분석 설정. 다음에 할당 API별 요청 크기, 결과 코드 또는 NULL, 호출 문맥, 반환 지연을 기록한다. 보드별 차이는 이 값과 실제 RAM 배치가 다를 때 조사한다.
slab에 대해서는 사용 블록 수를 주기적으로 읽고, heap은 요청 크기와 실패율을 추적한다. 로그 자체가 timing을 바꿀 수 있으므로 계측을 켠 빌드와 제품 빌드를 구분한다.
실험 기록 양식
[실험 주제] Memory Management
[Zephyr 버전] Zephyr 3.7.0 LTS3
[보드/SoC]
[west manifest revision]
[관련 Kconfig]
[관련 devicetree node]
[관찰한 원본 코드]
[예상 상태 전이]
[실제 상태 전이]
[실패 시 return/log/assert]
[latency 또는 memory 측정값]
[다음 실험]대상이 slab, heap, stack, memory domain 중 무엇인지 적고, 호출 문맥·요청 크기·실패 반환값·블록 반환 책임자를 함께 기록한다.
주요 용어
| 용어 | 이 글에서의 정확한 뜻 |
|---|---|
k_mem_slab | 같은 크기의 블록 여러 개를 관리하는 커널 객체 |
k_heap | 한 영역에서 가변 크기 버퍼를 할당하는 동기화된 heap |
sys_heap | k_heap 아래의 저수준 allocator; 동시 호출 직렬화는 호출자 책임 |
system heap | k_malloc()/k_free()가 쓰는 별도의 전역 heap 경로 |
stack object | 정렬·guard·예약 영역이 포함될 수 있는 스레드용 메모리 객체 |
memory domain | user thread의 RAM partition 접근 권한을 묶는 경계 |