본문으로 건너뛰기
Practical RTOS Internals · 47/53

Zephyr 커널 분석 — k_thread·k_sem·Driver Model

· Hawk · 8분 읽기

#한 줄 요약

“Zephyr는 RTOS 모양의 미니 Linux입니다.” — devicetree·KConfig·driver model이 모두 Linux 스타일이며, 커널만 봐도 FreeRTOS의 두세 배 규모입니다.

#어떤 문제를 푸는가

작은 MCU에서 출발한 FreeRTOS와 달리 Zephyr는 처음부터 다양한 SoC를 같은 빌드 체계에서 다루도록 설계되었습니다. Linux Foundation이 관리하며 Wind River, Intel, NXP, Nordic, Espressif가 모두 코드를 보탭니다. 그 결과 커널 자체보다도 드라이버 모델, devicetree, KConfig, west가 함께 따라옵니다.

이번 편은 두 가지를 노립니다. 첫째, kernel/ 서브트리에서 스레드와 동기화 객체가 어떻게 구현되는지 골라 봅니다. 둘째, 드라이버 인스턴스가 devicetree에서 어떻게 컴파일 시간에 만들어지는지를 추적합니다. 이 두 축을 잡으면 Zephyr 코드를 처음 보더라도 길을 잃지 않습니다.

저장소는 github.com/zephyrproject-rtos/zephyr이고, 커널만 보려면 kernel/include/zephyr/kernel.h 두 디렉터리면 충분합니다.

#저장소 구조

zephyr/
├── kernel/ # 커널 핵심
│ ├── thread.c
│ ├── sched.c
│ ├── sem.c
│ ├── mutex.c
│ ├── timer.c
│ ├── msg_q.c
│ ├── poll.c
│ └── ...
├── arch/ # 아키텍처별 port
│ ├── arm/core/cortex_m/
│ ├── arm64/
│ ├── riscv/
│ └── xtensa/
├── drivers/ # 드라이버 (vtable 기반)
│ ├── uart/
│ ├── i2c/
│ ├── gpio/
│ └── ...
├── include/zephyr/
│ ├── kernel.h
│ └── device.h
├── dts/ # devicetree 바인딩
├── boards/ # 보드 정의 + 기본 KConfig
└── samples/

커널만 ~50K LoC, 드라이버와 서브시스템까지 합치면 수백 MB가 되는 거대한 트리입니다. 처음 들어갈 때는 kernel/ 안에서 sched.csem.c만 골라 보는 편이 좋습니다.

#k_thread — TCB의 Zephyr 버전

struct k_thread는 FreeRTOS의 TCB와 비슷한 역할입니다. 다만 userspace 격리와 mem_domain까지 들고 있어서 구조가 더 큽니다.

struct k_thread {
struct _thread_base base; /* priority, state, queue node */
struct _callee_saved callee_saved; /* 아키텍처별 콜백 저장 */
void *init_data;
#if defined(CONFIG_USERSPACE)
_wait_q_t join_queue;
struct k_mem_domain *mem_domain;
#endif
k_thread_stack_t *stack_obj;
size_t stack_size;
char name[CONFIG_THREAD_MAX_NAME_LEN];
struct k_heap *resource_pool;
/* ... */
};

_thread_base가 스케줄러가 보는 공통 헤더입니다. priority, 상태 비트, 큐에 끼울 dnode가 여기 모입니다.

struct _thread_base {
sys_dnode_t qnode_dlist; /* per-priority 또는 wait queue */
uint32_t order_key;
uint8_t prio;
uint8_t user_options;
uint8_t thread_state;
/* ... */
};

thread_state_THREAD_PENDING, _THREAD_PRESTART, _THREAD_DEAD 같은 비트 모음입니다. 한 스레드가 여러 상태를 동시에 가질 수 있도록 비트 OR로 누적합니다.

#sched.c — 두 가지 ready queue

Zephyr는 빌드 옵션에 따라 다른 ready queue 자료구조를 고를 수 있습니다. 기본은 단순한 dumb queue, 큰 시스템에서는 multiqueue 또는 rbtree 기반 scalable queue가 활성화됩니다.

static inline struct k_thread *_priq_dumb_best(sys_dlist_t *pq)
{
struct k_thread *t = NULL;
sys_dnode_t *n = sys_dlist_peek_head(pq);
if (n != NULL) {
t = CONTAINER_OF(n, struct k_thread, base.qnode_dlist);
}
return t;
}

dumb는 한 linked list 안에 priority 순으로 정렬해 두는 방식입니다. priority 수가 적고 ready 스레드가 많지 않은 임베디드 시스템에서는 이 단순함이 그대로 이득입니다.

조금 큰 시스템에서는 multiqueue가 켜집니다.

#if defined(CONFIG_SCHED_MULTIQ)
uint32_t multiq_bitmap[CONFIG_NUM_BITMAP_WORDS];
sys_dlist_t multiq_queues[K_NUM_PRIO_BITMAP];
#endif

priority별 큐와 비트맵을 함께 두는 구조입니다. find_lsb_set(bitmap) 한 번이면 최상위 priority가 나오므로 FreeRTOS의 uxTopReadyPriority + CLZ 조합과 같은 효과를 봅니다. SMP 빌드에서는 per-CPU runqueue에 push/pull balancing이 얹힌 형태로 확장됩니다. 이 구조의 비교는 4-07: SMP RTOS에서 다룹니다.

#k_sem — spinlock 기반 동기화 객체

kernel/sem.c의 take 함수는 SMP를 고려한 일반적인 구조를 보여 줍니다.

struct k_sem {
_wait_q_t wait_q;
uint32_t count;
uint32_t limit;
};
int k_sem_take(struct k_sem *sem, k_timeout_t timeout)
{
int ret = 0;
k_spinlock_key_t key = k_spin_lock(&sem->lock);
if (sem->count > 0) {
sem->count--;
ret = 0;
} else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
ret = -EBUSY;
} else {
ret = z_pend_curr(&sem->lock, key, &sem->wait_q, timeout);
return ret;
}
k_spin_unlock(&sem->lock, key);
return ret;
}

k_spin_lock은 단일 코어 빌드에서 IRQ disable로 축약되고, SMP 빌드에서는 spinlock + IRQ disable로 확장됩니다. 같은 소스가 두 빌드에서 모두 정확합니다.

z_pend_curr는 현재 스레드를 wait queue에 끼우고 락을 풀면서 스케줄러로 돌아갑니다. timeout이 지나면 다시 ready로 돌리고 적절한 errno로 빠져나옵니다. 이 패턴이 mutex, msg queue, k_poll에서 그대로 재사용됩니다.

#Driver Model — vtable + devicetree

Zephyr 드라이버는 vtable + per-instance data + per-instance config의 삼각형으로 구성됩니다. UART 드라이버 예를 보면 분리가 분명합니다.

drivers/uart/uart_stm32.c
static int uart_stm32_init(const struct device *dev) { /* ... */ }
static int uart_stm32_poll_in(const struct device *dev,
unsigned char *c) { /* ... */ }
static const struct uart_driver_api uart_stm32_driver_api = {
.poll_in = uart_stm32_poll_in,
.poll_out = uart_stm32_poll_out,
.err_check = uart_stm32_err_check,
/* ... */
};
DEVICE_DT_INST_DEFINE(0, uart_stm32_init, NULL,
&uart_stm32_data_0, &uart_stm32_cfg_0,
PRE_KERNEL_1, CONFIG_SERIAL_INIT_PRIORITY,
&uart_stm32_driver_api);

DEVICE_DT_INST_DEFINE이 핵심 매크로입니다. devicetree에서 같은 compatible을 가진 모든 인스턴스에 대해 device 구조체를 컴파일 시간에 만들어 둡니다. 런타임 dynamic registration이 필요 없으므로 ROM에 그대로 자리잡습니다.

devicetree 입력은 Linux DTS와 같은 문법입니다.

boards/arm/nucleo_f407g/nucleo_f407g.dts
/dts-v1/;
#include <st/f4/stm32f407Xg.dtsi>
/ {
chosen {
zephyr,console = &usart2;
zephyr,sram = &sram0;
zephyr,flash = &flash0;
};
};
&usart2 {
pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>;
pinctrl-names = "default";
current-speed = <115200>;
status = "okay";
};

빌드 시 dts/의 바인딩과 매칭되어 generated header로 변환되고, 드라이버 매크로가 그 header를 consume합니다. 보드를 바꿔도 드라이버 코드는 한 줄도 변하지 않습니다.

#KConfig — feature를 컴파일에서 잘라내기

prj.conf
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_LOG=y

Linux의 KConfig 문법 그대로입니다. 켜지 않은 기능은 링크 단계에서 사라지므로 작은 MCU에서도 footprint를 통제할 수 있습니다. menuconfig로 인터랙티브 탐색이 가능합니다.

Terminal window
west build -b nucleo_f407g samples/hello_world -t menuconfig

#west — 멀티 저장소 메타툴

Zephyr는 단일 저장소가 아니라 Zephyr core + 수십 개 module로 분산되어 있습니다. west가 manifest를 읽어 모두 동기화합니다.

Terminal window
west init -m https://github.com/zephyrproject-rtos/zephyr --mr main
cd zephyrproject
west update
west build -b nucleo_f407g samples/hello_world
west flash

west가 git 작업, 빌드, 플래시, signing을 한 인터페이스로 묶습니다. Linux 세계의 apt + cmake + ninja를 단일 도구로 뭉친 셈입니다.

#POSIX 서브시스템

subsys/posix를 활성화하면 pthread, semaphore, signal API의 일부가 그대로 사용 가능해집니다.

#include <pthread.h>
pthread_t tid;
pthread_create(&tid, NULL, thread_entry, NULL);
pthread_join(tid, NULL);
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mtx);
pthread_mutex_unlock(&mtx);

Linux에서 쓰던 코드가 재컴파일만으로 MCU에서 도는 경우가 많습니다. 다만 fork, mmap, signal handler에는 제약이 있으므로 호환되는 부분만 의존하는 편이 안전합니다.

#Network·BLE — 통합 서브시스템

Zephyr가 FreeRTOS와 갈리는 지점은 내장 서브시스템의 폭입니다.

subsys/net/ IPv4·IPv6·TCP·UDP·CoAP·MQTT·LwM2M·HTTP
subsys/bluetooth/ BLE host + controller (Nordic·Espressif)
subsys/usb/ USB device + host
subsys/fs/ LittleFS·FATFS·NVS
subsys/logging/ LOG_INF·LOG_ERR + 백엔드 (UART·RTT·net)

각 서브시스템이 KConfig로 켜고 끌 수 있도록 모듈화되어 있습니다. BLE는 특히 현재 가장 많이 채택되는 RTOS가 Zephyr입니다. Nordic nRF Connect SDK가 Zephyr 기반이고, Espressif도 ESP32에 Zephyr를 정식 지원합니다.

#User Mode — MPU/MMU 분리

CONFIG_USERSPACE=y로 빌드하면 스레드가 user mode로 동작합니다.

K_THREAD_DEFINE(user_thr, 2048, user_entry, NULL, NULL, NULL,
5, K_USER, 0);

User 스레드는 커널 데이터에 직접 접근할 수 없고, k_sem_take 같은 API도 syscall로 wrap되어 호출됩니다. MPU나 MMU가 강제하는 격리 위에서 Linux 같은 권한 분리가 가능해집니다. safety-critical 시스템에서 application 코드와 커널의 경계를 강하게 두고 싶을 때 유용합니다.

#FreeRTOS와 Zephyr — 한 줄 비교

항목FreeRTOSZephyr
커널 LoC~20K~50K+
devicetree×
KConfig× (FreeRTOSConfig.h)
드라이버 모델× (HAL 별도)
Network stackexternalbuilt-in
BLEexternalbuilt-in
POSIX×partial
User mode (MPU)partialfull
적합한 규모작은 센서·MCU중급·고급 MCU·SoC
인증SafeRTOS 상업 변종Auto Cert·DO-178C 진행

작은 펌웨어는 FreeRTOS의 단순함이 이깁니다. 통신 스택과 드라이버가 많이 필요한 IoT 게이트웨이나 BLE 디바이스는 Zephyr가 합리적입니다.

#SMP Support

Zephyr SMP는 4-07: SMP RTOS에서 깊게 다루지만, 코어 API 자체는 단순합니다.

K_THREAD_DEFINE(thr0, 2048, entry, NULL, NULL, NULL, 5, 0, 0);
k_thread_cpu_mask_disable_all(&thr0_thread);
k_thread_cpu_mask_enable(&thr0_thread, 0); /* CPU 0 affinity */

ARM Cortex-A, ARMv8-M dual-core, RISC-V SMP를 모두 지원하며, FreeRTOS 11 SMP보다 runqueue 자료구조와 balancing 알고리즘이 더 일찍 안정화되어 있습니다.

#자주 보는 함정

경고 — devicetree 변경 후 캐시된 빌드 사용

west build 직후 DTS만 바꾸고 다시 빌드하면 이전 generated header가 그대로 남아 있는 경우가 있습니다. west build --pristine이나 빌드 디렉터리 삭제로 강제 재생성을 시킵니다.

경고 — KConfig 의존성 미설정

CONFIG_BT=y
# CONFIG_BT_CTLR 또는 BT_HCI_RAW 미설정 → 런타임에 동작하지 않음

심볼 하나를 켰는데 짝이 되는 옵션을 빠뜨리면 빌드는 통과해도 BLE adv가 시작되지 않습니다. menuconfig로 의존성을 확인하는 습관이 필요합니다.

경고 — 스택 크기를 FreeRTOS 감각으로 잡기

K_THREAD_DEFINE(thr, 512, entry, ...);

Zephyr는 logging, k_poll, 드라이버 syscall 등이 스택을 더 씁니다. 최소 1024바이트, 일반적인 작업은 2048바이트가 안전한 기준입니다.

경고 — User mode에서 직접 레지스터 접근

SCB->ICSR = ...; /* user 스레드 → fault */

User mode 스레드는 커널 영역에 직접 접근할 수 없습니다. syscall로 wrap된 API만 사용해야 하며, 그렇지 않으면 MPU fault로 죽습니다.

#정리

  • Zephyr 커널은 kernel/sched.ckernel/sem.c를 중심으로 읽으면 동기화와 스케줄러의 골격이 잡힙니다.
  • 스레드 상태는 비트 OR로 누적되며, ready queue는 dumb/multiqueue/scalable 중 빌드 옵션으로 선택됩니다.
  • 동기화 객체는 k_spinlock을 통해 단일 코어와 SMP에서 동일한 코드가 동작하도록 일반화되어 있습니다.
  • 드라이버 모델은 vtable + devicetree-driven 인스턴스 정의가 핵심이며, 보드를 바꿔도 드라이버 코드가 변하지 않습니다.
  • KConfig가 모든 기능을 컴파일 단계에서 잘라내므로 작은 MCU에서도 footprint 통제가 가능합니다.
  • west는 멀티 저장소 manifest와 빌드·플래시·signing을 묶는 메타툴입니다.
  • POSIX 서브시스템과 BLE/네트워크 스택이 내장되어 있어 IoT 게이트웨이급 시스템에 잘 맞습니다.
  • User mode와 mem_domain으로 MPU/MMU 기반 권한 분리를 제공하므로 safety-critical 격리를 일찍 도입할 수 있습니다.

다음 편은 5-03 RT-Thread에서 경량 커널과 풍부한 component를 함께 가진 중국 출신 RTOS를 봅니다.

#관련 항목

Practical RTOS Internals · 48 of 53

  1. 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
  2. 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
  3. 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
  4. 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
  5. 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
  6. 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
  7. 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
  8. 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
  9. 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
  10. 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
  11. 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
  12. 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
  13. 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
  14. 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
  15. 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
  16. 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
  17. 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
  18. 18RISC-V Context Switch 분석 — ECALL·mret·CSR
  19. 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
  20. 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
  21. 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
  22. 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
  23. 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
  24. 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
  25. 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
  26. 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
  27. 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
  28. 28Priority Ceiling Protocol — Immediate vs Original 비교
  29. 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
  30. 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
  31. 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
  32. 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
  33. 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
  34. 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
  35. 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
  36. 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
  37. 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
  38. 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
  39. 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
  40. 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
  41. 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
  42. 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
  43. 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
  44. 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
  45. 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
  46. 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
  47. 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
  48. 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
  49. 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
  50. 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
  51. 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
  52. 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
  53. 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL