Zephyr v3.7.0 · 기다림의 단위

Polling API와 Event Bit: 깨어났다면 무엇이 남아 있나

여러 커널 객체 중 하나가 준비되기를 기다리는 일과, 한 event 객체에 쌓인 비트의 조합을 기다리는 일은 다르다. 깨어난 순간의 알림, 실제 획득, 다음 대기까지 남는 상태를 나눠서 읽는다.

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

이 글은 Zephyr v3.7.0, 소스 commit 36940db938a8f4a1e919496793ed439850a221c2를 기준으로 한다. IPC / Synchronization의 개별 객체 입문과 Kernel Queues의 payload·버퍼 계약 다음 단계로, 여러 조건의 대기와 상태 보존을 설명한다. 예시는 동작 경계 설명용이며 보드 실행 기록은 아니다.

1. 객체를 가로질러 기다릴까, 비트를 함께 기다릴까

k_poll()은 semaphore, FIFO, message queue, pipe, poll signal처럼 서로 다른 객체의 준비 조건을 한 스레드가 함께 관찰한다. 하나 이상이 준비되면 반환한다. k_event는 한 객체의 32비트 상태에 원인별 비트를 놓고, 지정한 비트 중 하나 또는 전부가 켜졌는지를 판정한다. 이름이 비슷한 k_poll_event.state는 전자의 배열 원소에 남는 알림 상태이고, k_event의 비트 값과 같은 저장소가 아니다.

그림 1. 두 대기 모델의 입력과 판정 단위

↔ 좁은 화면에서는 두 모델을 오른쪽으로 이동하며 비교하세요.

Polling API · 여러 객체 중 하나

sem
count > 0
msgq
used > 0
signal
raised
k_poll(events[], timeout)
준비된 배열 원소의 state 확인

알림만 준다. semaphore 토큰이나 메시지는 그 뒤 별도 호출로 획득한다.

Event API · 한 객체의 비트 조합

bit 0
입력
bit 1
설정
bit 2
전송
k_event_wait(_all)(mask)
같은 32비트 값에서 any / all 판정

반환값은 요청한 비트와 현재 값의 교집합이다. 기본 대기는 비트를 소비하지 않는다.

근거: Polling 정의, Event 상태와 대기 정의, poll event 구조.

2. “준비”가 뜻하는 것은 객체마다 다르다

k_poll_event에는 조건의 종류, 대상 객체, 현재 state가 있다. v3.7.0에서 사용 가능한 mode는 K_POLL_MODE_NOTIFY_ONLY다. 준비를 보고도 객체가 호출자에게 넘어오지 않으므로, 데이터 전달 형식과 별개로 어떤 동작을 다시 시도해야 하는지를 먼저 정해야 한다.

표 1. 객체별 준비 조건과 후속 획득
poll type준비로 검사하는 것반환 뒤 호출자가 할 일경계
SEM_AVAILABLEsemaphore count > 0k_sem_take(..., K_NO_WAIT)알림과 take 사이에 다른 소비자가 가져갈 수 있음
FIFO_DATA_AVAILABLEqueue가 비어 있지 않음k_fifo_get(..., K_NO_WAIT)받은 포인터의 수명은 FIFO 계약을 따름
MSGQ_DATA_AVAILABLEused_msgs > 0k_msgq_get(..., K_NO_WAIT)데이터가 경합 중 사라지면 빈 큐 반환을 처리
PIPE_DATA_AVAILABLE읽을 수 있는 바이트가 있음k_pipe_get(..., K_NO_WAIT)CONFIG_PIPES를 쓸 때
SIGNALsignaled != 0k_poll_signal_check()로 결과 확인별도 reset 전에는 다음 poll도 즉시 준비

근거: 객체별 실제 준비 검사, type·state 상수, notify-only와 후속 확인.

3. 알림은 예약이 아니다

k_poll()이 0을 돌려줘도 semaphore나 메시지의 소유권을 예약한 것은 아니다. 직접 take/get을 기다리던 스레드가 poller보다 먼저 객체를 얻을 수 있고, 알림을 받은 뒤 다른 소비자가 끼어들 수도 있다. 따라서 후속 획득은 K_NO_WAIT로 시도하고 실패를 정상 경쟁 결과로 다룬다.

그림 2. 준비 알림과 실제 획득 사이의 틈

↔ 좁은 화면에서는 시간의 흐름을 오른쪽으로 이동해 보세요.

① 생산자k_sem_give()

count가 1이 된다.

② pollerstate = SEM_AVAILABLE

준비 알림으로 깨어난다.

③ 다른 소비자k_sem_take()

먼저 토큰을 가져가 count가 0이 된다.

④ pollerk_sem_take(K_NO_WAIT)

실패할 수 있다. 알림은 획득 보증이 아니다.

한 번의 poll이 끝났다고 배열의 첫 원소만 보지 말자. 반환할 때 두 조건 이상이 준비될 수 있으므로 모든 state를 검사한다. 재사용 전에 각 state를 K_POLL_STATE_NOT_READY로 되돌린다. Signal에는 별도의 signaled 플래그도 있다. 다시 기다리려면 k_poll_signal_reset()으로 플래그를 지우고 poll event의 state도 초기화해야 한다.

/* sem, msgq, sig는 각각 이미 초기화되어 있다고 가정 */
struct k_poll_event watch[3];
k_poll_event_init(&watch[0], K_POLL_TYPE_SEM_AVAILABLE,
                  K_POLL_MODE_NOTIFY_ONLY, &sem);
k_poll_event_init(&watch[1], K_POLL_TYPE_MSGQ_DATA_AVAILABLE,
                  K_POLL_MODE_NOTIFY_ONLY, &msgq);
k_poll_event_init(&watch[2], K_POLL_TYPE_SIGNAL,
                  K_POLL_MODE_NOTIFY_ONLY, &sig);

for (;;) {
    int rc = k_poll(watch, 3, K_FOREVER);
    if (rc == 0) {
        if (watch[0].state & K_POLL_STATE_SEM_AVAILABLE) {
            if (k_sem_take(&sem, K_NO_WAIT) == 0) { handle_token(); }
        }
        if (watch[1].state & K_POLL_STATE_MSGQ_DATA_AVAILABLE) {
            if (k_msgq_get(&msgq, &item, K_NO_WAIT) == 0) { handle_item(&item); }
        }
        if (watch[2].state & K_POLL_STATE_SIGNALED) {
            unsigned int raised;
            int result;
            k_poll_signal_check(&sig, &raised, &result);
            if (raised) { handle_signal(result); }
            k_poll_signal_reset(&sig);
        }
    }
    /* -EINTR 같은 다른 반환도 별도 정책으로 처리 */
    for (size_t i = 0; i < 3; ++i) {
        watch[i].state = K_POLL_STATE_NOT_READY;
    }
}

위 코드는 핵심 순서를 보여 주는 발췌다. handle_*, item, 객체의 실제 선언과 오류 정책은 애플리케이션이 채운다. 여러 생산자가 같은 poll signal을 반복해서 raise하면 reset 경계의 알림이 합쳐지거나 놓일 수 있다. 발생 횟수가 필요하면 signal보다 count가 있는 semaphore나 항목을 저장하는 FIFO/msgq를 선택한다. Signal을 초기화·reset하는 소유자는 polling 스레드로 두는 것이 문서의 권장 방식이다.

근거: poll 반환·경합·state 재설정, 여러 준비 원소 검사, signal 플래그·결과·raise, 반복 reset과 신호 경쟁.

4. post, set, clear는 같은 32비트를 다르게 고친다

k_event는 이벤트 발생 횟수나 payload 큐가 아니라 켜져 있는 조건의 비트 집합이다. 예를 들어 아래는 32비트 중 낮은 네 비트만 보인 것이다. post는 OR로 더하고, set은 전체를 교체하며, clear는 지정한 비트만 내린다.

그림 3. 같은 시작값에서 이어지는 세 비트 연산

↔ 좁은 화면에서는 비트 3부터 결과값까지 오른쪽으로 이동해 확인하세요. 진한 칸은 1이다.

bit 3bit 2bit 1bit 0저장된 값
처음00110011
post(0100)01110111 = OR
set(0100)01000100 = 교체
clear(0100)00000000 = 해당 비트 제거
표 2. Event 비트 수정 API의 저장 규칙
API새 저장값0011에서 0100을 인수로 주면주의
k_event_post(e, bits)기존값 OR bits0111같은 비트를 여러 번 post해도 횟수는 남지 않음
k_event_set(e, bits)bits로 전체 교체0100기존의 다른 비트가 내려갈 수 있음
k_event_clear(e, bits)기존값 AND NOT bits0011켜져 있지 않은 bit 2만 지우므로 이 예시에서는 변화 없음
k_event_set_masked(e, bits, mask)mask 안에서만 bits로 교체mask에 따라 다름다른 비트는 보존

근거: mask 적용식과 다중 waiter 검사, post·set·set_masked·clear 인수.

5. any/all은 판정 조건, reset은 대기 전 초기화

k_event_wait(e, mask, false, timeout)는 mask 중 하나라도 켜지면, k_event_wait_all은 모두 켜져야 반환한다. 성공 반환값은 저장된 전체 값이 아니라 현재값 & mask다. false로 기다리면 반환 뒤에도 비트가 남는다. true는 “읽은 비트 소비”가 아니라 대기 시작 전에 객체의 모든 비트를 0으로 지우기다.

그림 4. 같은 초기값 1010에서 reset 인수의 차이

↔ 좁은 화면에서는 시작 상태, reset=false, reset=true 경로를 오른쪽으로 이동해 비교하세요.

시작 상태저장값 = 1010

요청 mask는 0010. bit 1은 이미 켜져 있다.

wait(0010, false, K_NO_WAIT)반환 = 0010저장값 = 1010

요청 비트가 보인다. 읽어도 지워지지 않는다.

wait(0010, true, K_NO_WAIT)반환 = 0000저장값 = 0000

검사 전에 bit 3까지 모두 지운다. 다음 대기가 성공하려면 새 post가 필요하다.

표 3. any/all과 reset의 반환·저장값
호출 / 시작값요청 mask즉시 결과호출 후 저장값
wait(..., false, K_NO_WAIT) / 101001100010: 하나 일치1010
wait_all(..., false, K_NO_WAIT) / 101001100000: bit 2 부족1010
wait(..., true, K_NO_WAIT) / 101000100000: 먼저 전체 reset0000

요청 mask가 0이면 v3.7.0 구현은 reset 처리보다 먼저 0을 반환한다. 따라서 이 경우 reset=true여도 저장 비트를 지우지 않는다. 위 그림과 표는 요청 mask가 0이 아닌 경우다.

대기 중 새 비트가 조건을 만족하면 해당 waiter들이 깨어나지만 비트는 자동 소모되지 않는다. 여러 스레드가 같은 객체를 기다릴 때 한 스레드의 reset=true는 다른 스레드가 보던 비트도 지울 수 있다. 이미 처리한 특정 상태만 내려야 한다면 해당 bit mask로 k_event_clear()를 명시적으로 호출하되, 다른 생산자가 같은 비트를 동시에 갱신하는 설계에서는 세대 번호나 별도 큐가 필요한지 따져야 한다.

근거: any/all 판정, 대기 전 reset·반환 mask, 만족한 waiter 깨우기, 공개 wait 계약.

6. timeout과 ISR의 선을 넘지 말자

표 4. Poll·Event 대기와 ISR 제약
경로즉시 / 유한 / 무한 대기반환과 다음 동작ISR
k_poll()K_NO_WAIT, K_MSEC(n), K_FOREVER0 준비, -EAGAIN 시간 초과 또는 준비 조건 없는 K_NO_WAIT, -EINTR 취소. 0일 때 모든 state를 검사thread API. 구현이 ISR 호출을 assert로 막음
k_poll_signal_raise()대기 호출이 아님signal의 signaled와 사용자 result를 기록. reset 전까지 유지ISR에서 알림 생산 가능
k_event_wait() / wait_all()K_NO_WAIT, K_MSEC(n), K_FOREVER성공하면 요청 mask와 일치한 비트, 시간이 초과되면 0. 이 API에는 -EAGAIN 반환이 없음ISR에서는 블로킹 금지. 현재 비트 조회는 k_event_test() 사용
k_event_post/set/clear()대기 호출이 아님비트 변경이 여러 waiter의 조건을 함께 만족시킬 수 있음ISR에서 전달 가능

서로 다른 결과형에 주의한다. Poll은 성공/오류를 int로 반환하고 준비 원인은 배열의 state에 둔다. Event wait는 일치한 uint32_t 비트 자체를 반환하며 0은 조건 불충족이다. ISR에서 현재 상태를 확인할 때 공개 헬퍼 k_event_test()는 내부적으로 k_event_wait(..., false, K_NO_WAIT)를 호출한다.

근거: poll의 ISR assert와 즉시 반환, poll 반환 계약, event ISR의 블로킹 제한과 0 반환, k_event_test 헬퍼, ISR의 event 전달·조회.

7. 객체의 준비와 사건의 기록을 구별하기

하나의 dispatcher가 여러 입력원을 기다리고 각 객체를 별도로 소비한다면 k_poll이 알맞다. 반대로 여러 조건의 현재 성립 여부를 한 비트 집합으로 공유하고 any/all을 판정한다면 k_event가 맞다. 같은 비트의 발생 횟수나 매 발생의 payload가 중요하다면 둘 중 어느 비트 상태만으로도 충분하지 않다. count나 queue에 실제 정보를 보관해야 한다.

표 5. 요구사항별 Poll·Event 선택 기준
요구사항기본 선택반드시 확인할 것
서로 다른 sem/msgq/signal 중 먼저 준비되는 것 처리k_pollstate 전체 순회, K_NO_WAIT 획득 실패, 다음 반복 state 초기화
입력 A/B가 모두 발생했는지, 현재 플래그를 여러 waiter가 조회k_event_wait_all / k_event_testreset=true의 전체 지우기, 오래 남은 비트
같은 알림이 여러 번 왔는지 또는 각 알림의 값 보존semaphore count / queuesignal과 event 비트의 합쳐짐, 큐 용량과 수명

디버깅 로그에는 k_poll의 반환값과 각 state, 실제 take/get 결과를 같은 시각에 남긴다. Event 쪽은 wait mask, reset 인수, 직전 저장 비트, 반환 비트, 이후 clear/post를 기록한다. “깨어났는데 일이 없다”는 현상도 이 두 시간축을 분리하면 경쟁인지 상태 초기화 실수인지 구별할 수 있다.

8. 고정 버전 근거와 이어 읽기

설명의 기준은 Zephyr 3.7.0 Polling API 문서와 3.7.0 Events 문서, 고정 commit의 poll.c, events.c, 공개 API 계약이다. 위 각 절의 좁은 링크는 실제 준비 검사와 상태 변경 위치를 가리킨다.