Runtime Services · Zephyr 3.7.0 LTS3 · source + diagram note

IPC / Synchronization

Zephyr 3.7.0의 semaphore·mutex·message queue·condition variable을 데이터, 소유권, 대기 조건에 따라 구분한다. 각 API의 ISR 경계와 실패 반환값을 고정 원본으로 확인한다.

기준 버전: Zephyr 3.7.0 LTS3 형식: 사용 예제 + 설명 + 그림 분석 방법: Linux 커널과 비교

네 가지 도구의 선택 지도

네 객체를 producer → wait queue → timeout이라는 한 줄로 합치면 중요한 차이를 놓친다. semaphore는 개수를 세는 신호, mutex는 소유자가 있는 배타적 잠금, message queue는 고정 크기 값 복사, condition variable은 mutex로 보호한 조건이 바뀌기를 기다리는 수단이다.

그림 1. Zephyr IPC·동기화 도구가 보장하는 것
semaphorek_sem_give/take
0…limit의 count. 데이터 자체와 소유자를 보관하지 않는다.
mutexk_mutex_lock/unlock
소유 thread와 재진입 횟수. 경쟁 시 priority inheritance.
message queuek_msgq_put/get
고정 크기 메시지를 복사한다. 가득 참/비어 있음이 대기 조건.
condition variablek_condvar_wait/signal
보호한 predicate를 다시 검사하며 기다린다. 신호를 누적하지 않는다.
ISR 경계: semaphore give·take(K_NO_WAIT), msgq put·get(K_NO_WAIT)는 문서상 허용된다. mutex lock/unlock과 condvar wait는 thread 문맥에서 다룬다.

네 칸은 서로 변환되는 단계가 아니다. 실제 요구가 count, owner, 값 복사, 공유 상태 변화 중 무엇인지 먼저 고른다.

객체보관하는 것대기하는 조건ISR 경계
semaphorecount와 limitcount가 0일 때 takegive 가능; take는 K_NO_WAIT
mutexowner·lock count다른 thread가 소유lock/unlock 불가
message queue고정 크기 값의 ring bufferput은 full, get은 emptyput/get 모두 K_NO_WAIT
condition variable대기 thread 목록공유 predicate가 거짓wait는 mutex 재획득 때문에 thread 전용

고정 버전과 소스 위치

모든 구현 설명은 Zephyr v3.7.0 커밋 36940db938a8f4a1e919496793ed439850a221c2에 맞췄다. 넷은 서로 다른 객체 구현이다. kernel/events.c나 poll은 이 네 API의 공통 상태 머신이 아니므로 본문 그림의 범위에 넣지 않았다.

kernel/sem.ckernel/mutex.ckernel/msg_q.ckernel/condvar.c

세 API의 사용 예제와 호출 조건

다음 코드는 애플리케이션에서 API를 사용하는 예제이며 Zephyr 구현 코드의 발췌는 아니다. 세 번째 예제의 struct sample과 sample은 주변 코드에서 정의해야 하며, 실제 코드에서는 put/get 반환값도 검사해야 한다.

1. ISR이 알리고 thread가 기다림

K_SEM_DEFINE(rx_sem, 0, 1);

void isr_rx_ready(void)
{
        k_sem_give(&rx_sem);
}

void rx_thread(void)
{
        k_sem_take(&rx_sem, K_FOREVER);
}

limit=1은 중복 give를 하나의 count로 합친다. 발생 횟수를 모두 보존해야 하는 이벤트에는 이 값이 맞지 않을 수 있다. k_sem_take()의 K_NO_WAIT 실패는 -EBUSY, 기다린 후 timeout 또는 reset은 -EAGAIN이다.

2. mutex로 버스 접근 보호

#include <zephyr/kernel.h>

K_MUTEX_DEFINE(bus_lock);
void transfer_on_shared_bus(void); /* Application-specific operation. */

int transfer_with_lock(void)
{
        int ret = k_mutex_lock(&bus_lock, K_MSEC(20));

        if (ret != 0) {
                return ret;
        }
        transfer_on_shared_bus();
        return k_mutex_unlock(&bus_lock);
}

성공한 thread만 unlock한다. Zephyr mutex는 같은 owner의 재진입을 허용하고, 획득한 횟수만큼 해제해야 한다. 다른 thread와 경쟁할 때 owner에 priority inheritance가 적용될 수 있으나 CONFIG_PRIORITY_CEILING과 중첩 잠금 순서의 영향을 받는다. ISR에서는 lock/unlock 모두 사용하지 않는다.

3. 작은 값 복사 메시지

K_MSGQ_DEFINE(sensor_q, sizeof(struct sample), 16, 4);

k_msgq_put(&sensor_q, &sample, K_NO_WAIT);
k_msgq_get(&sensor_q, &sample, K_MSEC(100));

K_MSGQ_DEFINE(..., 16, 4)는 16개의 고정 크기 메시지 칸을 선언한다. k_msgq_put()는 값을 복사하며 포인터를 큐에 보관하지 않는다. 다만 기다리는 put은 호출이 끝날 때까지 입력 버퍼가 유효해야 한다. 이 예제의 두 반환값도 실제 코드에서는 처리해야 한다.

구현 분기를 읽는 순서

질문v3.7.0 원본 분기결론
give가 count를 늘리는가?sem.c:104–112대기자가 있으면 그 thread를 깨우고, 없을 때만 limit까지 증가
mutex의 소유자는 누구인가?mutex.c:117–156, 211–277owner·재진입 횟수·대기자에게 소유권 전달
msgq가 포인터를 저장하는가?msg_q.c:136–174, 228–272성공 시 고정 크기 값을 복사; full 대기 중에는 호출자의 data를 참조
condvar의 wait는 잠금을 유지하는가?condvar.c:109–125mutex 해제→대기→반환 전 다시 획득

객체별 내부 계약

semaphore는 수를 세는 신호

k_sem_give()는 대기자가 있으면 우선순위·대기 순서에 따라 한 thread를 깨운다. 대기자가 없을 때만 count를 limit까지 올린다. 따라서 count는 아직 소비되지 않은 신호량이며 데이터 버퍼 자체가 아니다. k_sem_reset()은 count를 0으로 하고 기다리던 take를 -EAGAIN으로 끝낸다.

mutex는 소유권과 priority inheritance

k_mutex_lock()은 같은 owner가 재진입하면 lock count를 늘린다. 다른 thread가 기다리면 owner 우선순위가 조정될 수 있다. k_mutex_unlock()은 owner만 호출하고 마지막 unlock에 대기자에게 소유권이 넘어간다. 여러 mutex를 중첩할 때는 역순 해제가 중요하다.

message queue는 고정 길이 복사

비어 있을 때 get, 가득 찼을 때 put은 timeout에 따라 기다릴 수 있다. 기다리는 수신자가 있으면 put이 ring buffer를 거치지 않고 그 수신자의 버퍼에 직접 복사할 수도 있다. 큐가 가득 찬 상태로 put이 대기하면 원본은 호출자 버퍼 포인터를 잠시 참조하므로 함수가 반환할 때까지 버퍼 수명을 유지한다.

condition variable은 predicate와 함께 사용

호출자가 mutex를 잡고 공유 조건을 검사한다. 조건이 거짓이면 k_condvar_wait()가 mutex를 원자적으로 풀고 기다린 뒤 반환 전에 다시 잡는다. signal 자체가 조건을 저장하지 않으며, 깨어난 후 다른 thread가 상태를 바꿀 수 있으므로 predicate를 반복 검사한다.

불변 조건

객체지켜야 할 조건검증 방법
semaphore0 ≤ count ≤ limit; limit=1이면 burst 횟수는 합쳐질 수 있음give/take 횟수와 k_sem_count_get() 기록
mutexowner만 unlock; 재진입 횟수만큼 해제획득/해제 경로와 thread ID 추적
message queue모든 항목이 동일한 msg_size; 대기 put의 입력 버퍼 수명 유지정의 매크로, queue depth, put 반환값 확인
condition variable같은 mutex 아래에서 predicate 검사·갱신; wait 반환 후 재검사조건 검사 루프와 signal 순서 코드 리뷰

실제 대기 분기와 condvar 시간 축

각 API의 반환 코드가 다르므로 하나의 ready/failed 상태 머신으로 합치지 않는다. 그림 2는 sem.c의 세마포어 분기, 그림 3은 condition variable의 대기와 mutex 재획득 순서를 보여준다.

그림 2. k_sem_take/give/reset의 count·대기자 분기
Zephyr 3.7.0 세마포어 take, give, reset의 실제 분기 count가 양수면 take가 즉시 감소한다. 0이면 K_NO_WAIT는 EBUSY, 기다리는 thread는 wait queue로 들어간다. give는 먼저 대기자를 깨우고, 대기자가 없을 때만 count를 limit까지 올린다. timeout과 reset은 EAGAIN으로 대기를 끝낸다. 세마포어는 하나의 직선 흐름이 아니라 count와 대기자 유무로 분기한다 Zephyr v3.7.0 · kernel/sem.c:96–185 · 이 그림은 mutex·msgq 내부를 설명하지 않는다 k_sem_take()현재 count 확인 count > 0count-- · 즉시 0 반환 count = 0timeout 값으로 분기 K_NO_WAIT-EBUSY · 대기 없음 기다릴 수 있는 threadwait_q에 pend 시간 만료 / reset-EAGAIN 반환 k_sem_give()wait_q 먼저 확인 대기 thread 있음한 thread 깨움 · take는 0이 분기에서 count 증가 없음 대기 thread 없음count = min(count+1, limit)poll 이벤트 처리 분기 포함 reset은 별도 제어 경로 대기 thread 모두 -EAGAIN으로 깨운 뒤 count = 0 ISR: give 허용 · take는 K_NO_WAIT만 허용 출처: sem.c:104–110, 132–158, 166–185

세마포어 전용 그림이다. mutex의 owner와 msgq의 데이터 복사에 그대로 적용하지 않는다.

그림 3. condition variable은 mutex를 놓고 기다린 뒤 다시 잡는다
1. lock공유 predicate를 mutex 아래에서 검사
2. falsek_condvar_wait() 호출
3. release + pendmutex를 놓고 condvar wait queue에서 대기
4. wake + locksignal/timeout 뒤 mutex를 다시 획득
5. recheckpredicate를 다시 보고 반복 또는 처리

signal은 상태 값을 저장하지 않는다. timeout으로 돌아와도 mutex 재획득 후 반환한다.

반환값과 진단표

호출/관찰v3.7.0 반환뜻먼저 확인할 것
k_sem_take, 빈 count-EBUSY 즉시 / -EAGAIN 대기 만료·reset신호가 아직 없거나 reset됨give 주체, limit, 대기 정책
k_mutex_lock, 경쟁-EBUSY 즉시 / -EAGAIN timeoutowner가 lock 유지owner, 중첩 순서, critical section
k_mutex_unlock 오류-EPERM 비소유자 / -EINVAL 미잠금잘못된 해제 경로획득 성공·thread ID·해제 횟수
k_msgq_put, full-ENOMSG 즉시·purge / -EAGAIN timeout전달하지 못함queue depth, 소비 속도, purge 호출
k_msgq_get, empty-ENOMSG 즉시 / -EAGAIN timeout수신할 값이 없음생산 시각과 put 반환값
k_condvar_wait-EAGAIN timeout조건 충족을 확인하지 못함mutex 아래 predicate, signal 시점

오해와 잘못된 접근

semaphore를 데이터 저장소로 간주

give/take는 count만 조정한다. 실제 데이터는 별도 버퍼나 msgq에 보관하고 그 수명·동시 접근을 따로 설계한다.

mutex를 ISR에서 사용

owner와 priority inheritance가 thread에 묶인다. ISR에서 lock/unlock을 호출하지 않고, ISR→thread 알림에는 허용된 nonblocking 경로를 사용한다.

msgq put 실패를 버림

앞의 예제는 put 반환값을 검사하지 않는다. queue가 가득 차 K_NO_WAIT put이 -ENOMSG이면 값은 전달되지 않았으므로 손실·재시도 정책이 필요하다.

condvar signal을 count로 간주

기다리는 thread가 없을 때 signal은 다음 wait를 위해 저장되지 않는다. 반드시 mutex가 보호하는 predicate를 기준으로 루프에서 기다린다.

Linux와 비교할 때의 경계

이름이 비슷해도 이 글은 Zephyr 3.7.0의 반환값과 문맥 제약을 기준으로 한다. 예를 들어 msgq의 -ENOMSG, semaphore의 -EBUSY, mutex의 owner/priority inheritance를 Linux IPC 일반론으로 치환하지 않는다. 네 객체를 고를 때는 전달할 것이 count인지 값인지, 소유권이 필요한지, ISR에서 호출되는지를 먼저 적는다.

producer·consumer 시간 축

시점semaphore 알림message queue 전달condvar 조건
정의count=0, limit=1item size, 16칸mutex, predicate, condvar
생산ISR giveput이 값을 복사thread가 mutex 아래 상태 갱신 후 signal
대기count=0이면 take 대기empty get/full put 대기wait가 mutex 해제 후 잠듦
소비take 성공 후 별도 데이터 읽기get이 값을 수신 버퍼로 복사mutex 재획득 후 predicate 재검사

문제 사례

ISR 이벤트가 몰렸는데 한 번만 처리된다

0/1 semaphore의 count는 1에서 포화된다. 발생 횟수 모두가 필요한지, 실제 payload를 msgq에 복사해야 하는지 결정한다.

msgq가 비어 보이는데 데이터가 사라졌다

put의 -ENOMSG를 버렸거나 purge가 기존 항목을 지웠는지 확인한다. 생산·소비 속도와 k_msgq_num_used_get()을 같은 시간축에 기록한다.

condvar signal 뒤에도 thread가 기다린다

signal이 predicate를 바꾸지는 않는다. 공유 상태를 mutex 아래에서 갱신했는지, wait 진입 전후에 predicate를 확인하는지 살핀다.

측정할 값

  • semaphore: give/take 수, 최대 count, reset 시각.
  • mutex: owner, 보유 시간, 대기 시간, 중첩 획득 순서와 priority ceiling 설정.
  • message queue: 항목 크기, depth, -ENOMSG 빈도, ISR 복사 시간.
  • condition variable: predicate 변경 시점, signal과 wait 반환 시점, 재획득 대기 시간.

코드 리뷰 확인 사항

  • 이 객체가 전달하는 것은 count, 배타적 소유권, 복사한 값, 공유 predicate 중 무엇인가?
  • ISR에서 호출한다면 공식 API가 허용하며 timeout이 K_NO_WAIT인가?
  • 모든 실패 반환값을 처리하고, msgq put 손실 정책을 정했는가?
  • mutex는 획득한 thread가 같은 횟수로 해제하고, 중첩 순서가 명확한가?
  • condvar의 predicate는 같은 mutex 아래에서 검사·갱신하고 wait 후 다시 검사하는가?

디버깅 및 계측 지점

API명, 호출 문맥(ISR/thread), timeout, 반환값, thread ID, 시각을 함께 기록한다. semaphore는 count, mutex는 owner와 보유 시간, msgq는 used/free 칸 수, condvar는 predicate 값을 추가한다. 모든 대기를 하나의 “timeout”으로 집계하면 -EBUSY, -ENOMSG, -EAGAIN의 원인 차이가 사라진다.

실험 기록 양식

[실험 주제] IPC / Synchronization
[Zephyr 버전] Zephyr 3.7.0 LTS3
[보드/SoC]
[west manifest revision]
[관련 Kconfig]
[관련 devicetree node]
[관찰한 원본 코드]
[예상 상태 전이]
[실제 상태 전이]
[실패 시 return/log/assert]
[latency 또는 memory 측정값]
[다음 실험]

객체 종류, 호출 문맥, timeout 값, 반환값과 실제 전달한 데이터 또는 잠금 소유자를 함께 기록한다.

주요 용어

용어이 글의 뜻
count / limitsemaphore가 보유한 사용 가능 횟수와 상한
owner / inheritancemutex 소유 thread와 경쟁 시 임시 우선순위 조정
msgq copy고정 크기 값을 송신자에서 큐 또는 대기 수신자로 복사
predicatemutex 아래에서 읽고 쓰는 실제 공유 조건; condvar 신호 자체와 다름

다음 Zephyr RTOS 글