Zephyr v3.7.0 · 공유 채널과 관찰자

Zephyr zbus: 같은 게시, 서로 다른 시점의 메시지

zbus 채널은 메시지의 최신값을 한 곳에 둡니다. Listener는 게시 중 그 값을 보고, subscriber는 채널 참조를 받아 나중에 읽으며, 설정을 켠 message subscriber는 게시 시점의 복사본을 받습니다. 이 차이가 데이터 손실처럼 보이는 현상의 출발점입니다.

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

이 글은 Zephyr v3.7.0, commit 36940db938a8f4a1e919496793ed439850a221c2의 zbus를 기준으로 한다. Kernel Queues가 FIFO·msgq의 항목 저장 방식을 다룬다면, 여기서는 서비스들이 한 채널의 최신값과 게시 통지를 공유할 때 각 관찰자가 어떤 시점의 데이터를 보는지 설명한다. 그림과 예시는 설명용으로 새로 작성했다.

1. 채널은 항목 목록이 아니라 하나의 현재 메시지다

ZBUS_CHAN_DEFINE은 메시지 타입·초기값·validator·관찰자 목록을 지정해 채널을 만든다. 채널에는 메시지 크기만큼의 저장 영역이 하나 있고, publish는 그 영역을 갱신한 뒤 관찰자 통지를 시도한다. 여러 송신자와 수신자는 채널 이름을 통해 느슨하게 연결되지만, 이것을 모든 과거 메시지가 쌓이는 로그로 읽으면 안 된다. 세 번째 경로인 message subscriber는 기본 꺼짐인 CONFIG_ZBUS_MSG_SUBSCRIBER=y를 설정해야 쓸 수 있다.

그림 1. 한 채널 값에서 갈라지는 세 관찰 경로

↔ 좁은 화면에서는 채널에서 세 관찰자로 이어지는 경로를 오른쪽으로 이동해 보세요.

Channel: 현재 메시지message = 최신값정해진 크기의 공유 저장 영역 하나와 접근 잠금
Listener동기 callback게시 호출 안에서 잠긴 채널의 const 메시지를 직접 읽는다.
Subscriber비동기 threadmsgq에는 채널 참조만 들어간다. 나중에 read하면 그때의 최신값을 복사한다.
Message subscriber비동기 thread · 설정 필요CONFIG_ZBUS_MSG_SUBSCRIBER=y일 때 게시 시점의 메시지를 net_buf에 담아 FIFO로 보낸다. wait_msg가 호출자 버퍼로 복사한다.

근거: 채널의 메시지 참조·크기와 세 observer type, 채널 정의의 단일 메시지 저장 영역, message subscriber 설정 조건, 3.7.0 관찰자 설명.

2. publish는 복사와 통지를 같은 호출 안에서 진행한다

zbus_chan_pub()은 먼저 validator가 있다면 메시지를 검사한다. 채널 잠금을 얻은 뒤 송신자의 구조체를 채널 저장 영역으로 복사하고, VDED가 listener callback 호출·message subscriber 복사본 전달·subscriber 참조 통지를 관찰자 순서대로 수행한다. 그런 다음 잠금을 푼다. VDED는 별도 상주 스레드가 아니라 게시자의 실행 문맥에서 돌기 때문에, 느린 listener와 가득 찬 observer 큐는 게시 호출의 시간에 직접 영향을 준다.

그림 2. publish 호출 안의 채널 잠금과 VDED 경계

↔ 좁은 화면에서는 validator부터 unlock까지 오른쪽으로 이동해 확인하세요. 베이지 단계에서 채널이 잠긴다.

① 검사validator(msg)

거부하면 아직 채널 값은 바뀌지 않는다.

② 잠금chan_lock(timeout)

다른 read·publish·claim과 접근을 조정한다.

③ 저장memcpy(channel, msg)

송신자의 스택 객체도 채널로 복사된다.

④ VDEDobs[0] → obs[1] → …

관찰자 목록의 순서대로 같은 게시 호출에서 통지한다.

⑤ 해제chan_unlock()

오류가 있었어도 ③에서 갱신된 값은 남을 수 있다.

반환값과 채널 내용은 별개로 확인한다. Validator가 거부하면 복사 전에 -ENOMSG가 나온다. 반면 복사 뒤 observer 통지가 실패하면 publish가 오류를 반환해도 채널의 현재 메시지는 이미 새 값일 수 있다. 재시도를 무조건 “첫 게시가 전혀 없었다”는 전제로 설계하면 중복 통지나 상태 불일치가 생길 수 있다.

근거: validator·복사·통지·해제와 반환 순서, 세 observer 통지 함수, 게시자 문맥의 VDED 설명.

3. 게시 A 다음 B가 오고, 관찰자가 늦게 읽는다면

세 관찰자가 활성화되고 통지가 모두 성공한 상태에서, 한 채널에 A를 게시하고 곧 B를 게시한 뒤 비동기 관찰자 스레드가 실행되는 경우를 보자. Subscriber 큐에는 두 번의 채널 참조가 남을 수 있지만 두 참조 모두 같은 현재 채널을 가리킨다. 두 번의 zbus_chan_read()가 모두 B를 읽을 수 있다. Message subscriber는 게시 시점의 메시지를 net_buf에 담으므로 A와 B를 각각 회수할 수 있다. Listener는 A와 B의 VDED 시점마다 직접 호출된다.

그림 3. 통지 두 건과 메시지 두 건은 같지 않다

↔ 좁은 화면에서는 A 게시, B 게시, 늦은 소비 순서로 오른쪽으로 이동해 보세요.

t₁ · A 게시t₂ · B 게시t₃ · 늦은 소비
채널 저장 영역
A로 교체
B로 교체
B가 현재값
Subscriber msgq
&channel 통지
&channel 통지
두 참조를 꺼내도 read는 둘 다 B 가능
Message subscriber FIFO
A 스냅샷 버퍼
B 스냅샷 버퍼
성공적으로 전달된 두 버퍼에서 A, B 회수
Listener callback
잠긴 채널에서 A 확인
잠긴 채널에서 B 확인
별도 대기 큐 없음
표 1. 관찰자별 전달 단위와 읽는 시점
관찰자전달 단위메시지를 보는 때비용·실패 경계
Listener동기 callback과 잠긴 채널의 const 참조각 publish/notify의 VDED 도중느린 callback은 게시 호출을 늘림. 같은 채널을 다시 잠그는 read/claim은 피함
Subscribermsgq에 채널 포인터zbus_sub_wait() 후 별도 read 시점의 최신값큐 깊이와 경쟁에 따른 통지 실패, 이전 값 덮어쓰기
Message subscriber
CONFIG_ZBUS_MSG_SUBSCRIBER=y
net_buf에 담긴 게시 시점 메시지, FIFOzbus_sub_wait_msg()가 호출자 버퍼로 복사버퍼 풀과 메모리 비용. 풀 부족은 구성에 따라 assert·실패로 드러남

근거: callback·포인터 큐·net_buf clone, 게시 값의 버퍼 복사, 두 wait API의 회수 방식, 풀 한계와 최신값 덮어쓰기 경고.

4. read는 복사하고, claim은 잠금 수명을 호출자에게 넘긴다

zbus_chan_read()는 잠금을 얻고 채널 메시지 전체를 호출자의 버퍼에 복사한 다음 잠금을 푼다. zbus_chan_claim()은 잠금을 얻은 채 돌아와 직접 메시지나 user_data를 볼 수 있게 한다. 호출자는 반드시 zbus_chan_finish()로 잠금을 풀어야 한다. finish 자체는 관찰자에게 변경을 알리지 않는다. 직접 메시지를 수정했다면 필요한 통지는 finish 뒤 zbus_chan_notify()로 별도 수행한다.

표 2. 채널 접근 API의 복사·잠금·통지
호출데이터 접근잠금 종료관찰자 통지
zbus_chan_pub송신자 버퍼 → 채널로 복사호출 내부예, VDED 실행
zbus_chan_read채널 → 호출자 버퍼로 복사호출 내부아니요
zbus_chan_claim직접 접근을 위한 잠금 획득호출자가 finish아니요
zbus_chan_finishclaim한 채널 해제이 호출에서 해제아니요
zbus_chan_notify메시지 복사 없이 현재값 통지호출 내부예, VDED 실행
/* state_chan은 struct state_msg 채널로 미리 정의했다고 가정 */
int rc = zbus_chan_claim(&state_chan, K_MSEC(20));
if (rc == 0) {
    struct state_msg *state = zbus_chan_msg(&state_chan);
    state->level = next_level;      /* 잠금 보유 중 직접 변경 */
    zbus_chan_finish(&state_chan); /* 반드시 짝지어 해제 */
    rc = zbus_chan_notify(&state_chan, K_MSEC(20));
    /* rc를 확인: 통지가 실패해도 앞선 직접 변경은 남을 수 있다 */
}

finish와 뒤이은 notify는 하나의 원자적 작업이 아니다. 두 호출 사이에 다른 게시자가 채널 값을 바꾸면 notify는 바뀐 현재 값을 알릴 수 있다. 자신이 수정한 값의 게시·통지를 한 잠금 구간으로 묶어야 하면 zbus_chan_pub()의 계약을 사용한다.

Listener callback은 게시자의 잠금 구간 안에서 실행되므로 같은 채널을 다시 claim하거나 read하는 대신 zbus_chan_const_msg()로 현재 메시지를 직접 본다. 반대로 비동기 subscriber는 통지 후 채널이 아직 잠겨 있을 수 있으므로 read의 timeout을 여유 있게 잡고 실패를 처리한다.

근거: read·notify·claim·finish 실제 잠금 경로, claim/finish/notify 공개 계약, listener 접근과 claim 예시, subscriber read timeout 경고.

5. 통지의 순서와 처리 완료의 순서를 섞지 않는다

3.7.0의 VDED는 채널 정의에 나열한 관찰자, ZBUS_CHAN_ADD_OBS로 붙인 정적 관찰자, 실행 중 추가한 관찰자의 순서로 통지한다. 첫 묶음은 정의 순서, 두 번째 묶음은 매크로의 sequence priority, 마지막 묶음은 추가 순서다. 비활성 관찰자나 가려진 observation은 건너뛴다. 이것은 VDED가 통지를 시도하는 순서다. 깨어난 비동기 스레드가 언제 처리까지 마칠지는 우선순위와 선점에 달려 있다.

표 3. 관찰자 통지의 세 등록 계층
우선 통지 계층등록 방법계층 안의 순서주의
① 채널 정의 관찰자ZBUS_CHAN_DEFINE(... ZBUS_OBSERVERS(...))나열한 순서callback 실행과 큐 넣기가 게시 호출 안에서 진행
② 정의 후 정적 관찰자ZBUS_CHAN_ADD_OBS매크로의 sequence priority채널 정의 묶음보다 먼저 올 수 없음
③ 런타임 관찰자zbus_chan_add_obs()추가한 순서CONFIG_ZBUS_RUNTIME_OBSERVERS가 필요

통지 수신도 무조건 보장된다고 가정하지 않는다. Subscriber의 msgq가 가득 차면 통지 오류가 날 수 있고, message subscriber의 net_buf 확보도 실패할 수 있다. 후자는 설정에 따라 assert로 중단되거나 오류로 드러나므로 버퍼 풀 크기와 메모리 여유를 먼저 검증한다. Publish가 오류를 돌려주었다면 validator·잠금·관찰자 통지 중 어느 단계였는지 구분한다. CONFIG_ZBUS_MSG_SUBSCRIBER_NET_BUF_POOL_SIZE는 메시지 크기·생산 속도·소비 지연에 맞춰 정한다.

근거: 3.7.0의 세 통지 계층과 비활성 무시, 순서대로 통지하며 실패를 보존하는 구현, assert/mock 분기, 전달 한계와 풀 설정.

6. timeout은 채널 잠금뿐 아니라 게시 통지에도 걸린다

timeout은 validator나 listener callback의 실행을 강제로 중단하지 않는다. 따라서 호출 전체의 실행 시간 상한으로 읽으면 안 된다. 게시 중 계산한 남은 시간은 뒤이은 큐·버퍼 대기에 전달되며, callback 실행 비용은 별도로 제한해야 한다.

표 4. zbus API별 timeout과 ISR 경계
APItimeout이 걸리는 경로ISR에서의 경계실패 뒤 확인
pub / notify채널 잠금과 VDED의 큐·버퍼 전달에 남은 시간 사용K_NO_WAIT만 사용. Listener도 ISR 문맥에서 실행될 수 있음validator 이전 거부인지, 복사 뒤 통지 실패인지 구분
read / claim채널 잠금 획득K_NO_WAIT만 사용바쁜 채널·시간 초과를 처리; claim 성공이면 finish 필수
finish대기 없이 잠금 해제성공한 claim과 같은 제어 흐름에서 호출finish가 알림을 보내지는 않음
sub_wait / sub_wait_msg관찰자 큐/FIFO에서 기다림ISR 호출 금지; 스레드에서 처리통지만 받았는지 메시지 복사본을 받았는지 구분

K_NO_WAIT는 “CPU 작업이 0”이라는 뜻이 아니다. ISR에서 publish하면 잠금을 기다리지는 않지만 VDED의 listener 호출과 메시지 복사·통지 시도는 여전히 ISR 문맥에서 실행된다. 긴 callback이나 큰 메시지 전달을 ISR 경로에 두지 않는다.

근거: ISR K_NO_WAIT 강제와 pub·read·claim 경로, 두 구독 대기의 ISR 금지, 게시자 문맥의 VDED, 공개 timeout·finish 계약.

7. 최신 상태, 모든 샘플, 즉시 반응 중 무엇이 필요한가

표 5. 필요한 정보에 따른 관찰자 선택
필요한 것가까운 선택확인할 경계
게시 중 짧은 검증·반응Listenercallback이 publisher와 같은 문맥·잠금 안에서 끝나는가
채널의 최신 상태만 읽으면 됨Subscriber + chan_read참조 통지 두 건이 과거값 두 건을 보장하지 않음
각 게시의 메시지 내용을 나중에 처리Message subscriber
CONFIG_ZBUS_MSG_SUBSCRIBER=y 필요
net_buf 풀·전달 오류·소비 속도
대량 바이트 스트림Pipe 등 데이터 경로 재검토zbus의 채널 잠금·복사·VDED 비용

진단할 때는 publish 호출의 반환값만 남기지 말고 채널 이름, validator 결과, 잠금 대기, 관찰자별 통지 결과, subscriber 큐 깊이와 message subscriber 버퍼 풀 상태를 나란히 기록한다. 과거 샘플이 필요했는데 최신값만 보였다면 subscriber가 채널 참조를 받은 시점과 실제 read 시점을 먼저 비교한다.

근거: 3.7.0 사용 기준, subscriber의 과거값·풀 경계.

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

기준 문서는 Zephyr 3.7.0 zbus다. 구현은 같은 릴리스 commit의 zbus.c, 공개 타입과 API는 zbus.h에 고정했다. 위 각 절의 좁은 출처 링크는 실제 복사·잠금·통지 위치를 가리킨다.