네 가지 도구의 선택 지도
네 객체를 producer → wait queue → timeout이라는 한 줄로 합치면 중요한 차이를 놓친다. semaphore는 개수를 세는 신호, mutex는 소유자가 있는 배타적 잠금, message queue는 고정 크기 값 복사, condition variable은 mutex로 보호한 조건이 바뀌기를 기다리는 수단이다.
k_sem_give/take0…limit의 count. 데이터 자체와 소유자를 보관하지 않는다.
k_mutex_lock/unlock소유 thread와 재진입 횟수. 경쟁 시 priority inheritance.
k_msgq_put/get고정 크기 메시지를 복사한다. 가득 참/비어 있음이 대기 조건.
k_condvar_wait/signal보호한 predicate를 다시 검사하며 기다린다. 신호를 누적하지 않는다.
네 칸은 서로 변환되는 단계가 아니다. 실제 요구가 count, owner, 값 복사, 공유 상태 변화 중 무엇인지 먼저 고른다.
| 객체 | 보관하는 것 | 대기하는 조건 | ISR 경계 |
|---|---|---|---|
| semaphore | count와 limit | count가 0일 때 take | give 가능; take는 K_NO_WAIT |
| mutex | owner·lock count | 다른 thread가 소유 | lock/unlock 불가 |
| message queue | 고정 크기 값의 ring buffer | put은 full, get은 empty | put/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–277 | owner·재진입 횟수·대기자에게 소유권 전달 |
| msgq가 포인터를 저장하는가? | msg_q.c:136–174, 228–272 | 성공 시 고정 크기 값을 복사; full 대기 중에는 호출자의 data를 참조 |
| condvar의 wait는 잠금을 유지하는가? | condvar.c:109–125 | mutex 해제→대기→반환 전 다시 획득 |
객체별 내부 계약
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를 반복 검사한다.
불변 조건
| 객체 | 지켜야 할 조건 | 검증 방법 |
|---|---|---|
| semaphore | 0 ≤ count ≤ limit; limit=1이면 burst 횟수는 합쳐질 수 있음 | give/take 횟수와 k_sem_count_get() 기록 |
| mutex | owner만 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 재획득 순서를 보여준다.
k_sem_take/give/reset의 count·대기자 분기세마포어 전용 그림이다. mutex의 owner와 msgq의 데이터 복사에 그대로 적용하지 않는다.
k_condvar_wait() 호출signal은 상태 값을 저장하지 않는다. timeout으로 돌아와도 mutex 재획득 후 반환한다.
반환값과 진단표
| 호출/관찰 | v3.7.0 반환 | 뜻 | 먼저 확인할 것 |
|---|---|---|---|
k_sem_take, 빈 count | -EBUSY 즉시 / -EAGAIN 대기 만료·reset | 신호가 아직 없거나 reset됨 | give 주체, limit, 대기 정책 |
k_mutex_lock, 경쟁 | -EBUSY 즉시 / -EAGAIN timeout | owner가 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=1 | item size, 16칸 | mutex, predicate, condvar |
| 생산 | ISR give | put이 값을 복사 | 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 / limit | semaphore가 보유한 사용 가능 횟수와 상한 |
| owner / inheritance | mutex 소유 thread와 경쟁 시 임시 우선순위 조정 |
| msgq copy | 고정 크기 값을 송신자에서 큐 또는 대기 수신자로 복사 |
| predicate | mutex 아래에서 읽고 쓰는 실제 공유 조건; condvar 신호 자체와 다름 |