Zephyr v3.7.0 · 커널과 서비스 소스 분석

Zephyr 데이터 전달: 포인터와 바이트

같은 ‘큐’라는 이름 아래에도 넘기는 것이 항목 주소인지, 고정 크기 복사본인지, 나중에 읽을 송신 버퍼인지가 다릅니다.

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

이 글은 Zephyr v3.7.0, commit 36940db938a8f4a1e919496793ed439850a221c2의 데이터 전달 계약을 비교한다. 기존 IPC / Synchronization 글의 semaphore·mutex·message queue 입문과 달리 payload 저장 방식, 버퍼 수명, 대기·ISR 조건을 중심으로 읽는다. 그림은 새로 작성한 설명용 도식이다.

1. 먼저 무엇이 이동하는지 고른다

생산자와 소비자를 잇는 객체를 고를 때 크기보다 먼저 소비자가 어떤 메모리를 언제 읽는가를 따져야 한다. FIFO/LIFO는 원래 항목을 연결하고, message queue는 정해진 크기만큼 복사한다. mailbox에는 상대 스레드와 메시지 처리 완료까지의 계약이 있다. pipe는 항목이 아닌 바이트 스트림이다.

표 1. Zephyr 3.7.0 데이터 전달 객체의 기본 계약
객체전달 단위payload와 저장자연스러운 사용
FIFO가변 크기 항목의 주소항목 자체 연결, payload 복사 없음들어온 순서대로 소비
LIFO가변 크기 항목의 주소항목 자체 연결, payload 복사 없음가장 최근 항목 먼저 소비
Message queue고정 크기 레코드대기 수신자 또는 고정 용량 ring buffer로 복사작은 독립 값, 용량 상한
Mailbox스레드 간 메시지 설명자·가변 payload매칭 뒤 수신/후속 호출 때 송신 버퍼에서 복사 가능상대 선택·처리 완료·지연 수신
Pipe바이트 스트림의 일부 또는 전부대기 독자에게 직접 또는 선택적 ring buffer로 복사길이가 달라지는 연속 바이트

FIFO/LIFO에서 ‘주소 전달’은 메모리를 자동 할당하거나 복제한다는 뜻이 아니다. 누가 항목의 수명을 책임질지 애플리케이션이 정해야 한다. 반대로 message queue에서 큰 구조체를 통째로 복사하면 잠금 중 복사 시간이 길어질 수 있다. 필요하면 메시지 본체 대신 포인터 값을 고정 크기 레코드로 보낼 수 있지만, 그 포인터가 가리키는 데이터의 수명은 여전히 별도 계약이다.

근거: 3.7.0 Kernel Services 비교표, Message Queues, 고정 문서 mailboxes.rst, Pipes.

2. FIFO와 LIFO는 같은 항목을 다른 끝에서 꺼낸다

세 항목 A·B·C가 모두 큐에 쌓인 뒤 소비한다면 FIFO는 A→B→C, LIFO는 C→B→A를 돌려준다. 다만 이미 기다리는 소비자가 있으면 새 항목은 큐에 쌓이기 전에 그 스레드로 바로 전달될 수 있다. 그러므로 아래 순서는 항목이 저장된 경우의 그림이다.

그림 1. A·B·C가 대기 소비자 없이 쌓였을 때의 꺼내는 순서

↔ 좁은 화면에서는 항목과 소비 결과를 좌우로 이동해 읽으세요.

생산 순서
A 등록같은 항목 주소
B 등록같은 항목 주소
C 등록같은 항목 주소
수명 책임소비 전 재사용 금지
FIFO · append/get
① A가장 오래됨
② B
③ C
출구A → B → C
LIFO · prepend/get
① C가장 최근
② B
③ A
출구C → B → A

Zephyr의 k_fifo_put/k_lifo_put은 각각 내부 queue append/prepend로 연결한다. 항목 payload를 별도 버퍼에 복사하지 않는다.

표 2. 연결된 항목의 메모리 계약
경로항목 배치소유권·실패 경계
k_fifo_put / k_lifo_put항목 첫 word를 커널 연결용으로 예약하고 word 정렬같은 항목을 꺼내기 전에 다시 넣으면 정의되지 않은 동작. 생산자는 소비 전 항목 내용을 재사용하지 않음
k_fifo_alloc_put / k_lifo_alloc_put항목 첫 word 예약은 불필요. 기다리는 소비자가 있으면 주소를 직접 넘기고, 없어서 큐에 저장할 때만 호출 스레드 resource pool의 임시 연결 노드를 할당payload 자체는 복사되지 않음. 노드 할당 경로에서는 실패 가능(-ENOMEM)
get(timeout)같은 항목의 주소를 반환빈 큐라면 thread는 대기 가능; ISR은 K_NO_WAIT만 사용

무한 큐가 아니다. 고정된 내부 슬롯 수는 없지만 항목 메모리와 임시 연결 노드의 자원은 유한하다. 소비 속도보다 생산 속도가 빠르면 애플리케이션이 항목 풀·폐기·역압 정책을 별도로 둬야 한다.

근거: 3.7.0 FIFOs, 고정 문서 LIFOs, kernel.h FIFO 매크로와 LIFO 매크로, kernel/queue.c 124–238행.

3. Message queue는 한 칸의 크기와 칸 수를 고정한다

k_msgq는 msg_size 바이트짜리 칸을 max_msgs개 둔 ring buffer다. put()은 데이터 바이트를 복사한다. 기다리는 수신자가 있으면 그 수신 버퍼로 바로 복사하고, 없으면 ring에 복사한다. get()도 수신자 버퍼에 복사한다. 성공한 put 뒤에는 생산자의 원본 값 변수가 큐의 저장소가 아니다.

그림 2. 고정 크기 레코드의 복사 경로

↔ 좁은 화면에서 생산자·ring·소비자 단계를 좌우로 이동해 읽으세요.

생산자 변수msg_size 바이트
→
ring의 한 칸용량 max_msgs
→
소비자 변수별도의 복사본
수신자가 대기 중중간 ring을 거치지 않고 수신 버퍼로 직접 복사
→
직접 전달그래도 바이트 복사
→
보장항목 포인터를 보관하지 않음

칸이 가득 찬 경우에는 성공한 복사가 아직 일어나지 않았다. thread의 대기형 put은 송신 원본 주소를 대기 상태에서 보유하므로 호출 완료 전까지 그 버퍼가 유효해야 한다.

표 3. 버퍼 용량과 대기 결과
상황동작확인할 결과
빈 큐에 put고정 크기 복사. 기다리는 수신자면 직접, 아니면 ring으로성공 뒤 원본 변수는 재사용 가능
가득 찬 큐에 putK_NO_WAIT면 즉시 실패, thread의 대기형 timeout이면 공간을 기다림즉시 -ENOMSG, 대기 시간 초과 -EAGAIN
빈 큐에서 getK_NO_WAIT면 즉시 실패, thread는 메시지를 기다릴 수 있음반환값 확인 후 수신 버퍼 사용
큰 레코드payload 전체를 잠금 경로에서 복사복사 시간·인터럽트 지연 검토. 큰 항목은 별도 수명 계약의 포인터 전달 고려

근거: 3.7.0 Message Queues, kernel/msg_q.c 124–179행과 216–277행.

4. Mailbox는 설명자 매칭과 데이터 소비 완료를 나눈다

메일박스는 thread 간 객체다. 송신자는 tx_target_thread, 수신자는 rx_source_thread를 지정하거나 K_ANY를 쓸 수 있다. 호환되는 두 설명자가 만나야 메시지가 전달된다. 수신자는 즉시 버퍼로 복사하거나 k_mbox_get(..., NULL, ...) 뒤 데이터 크기가 0보다 클 때 k_mbox_data_get()으로 복사·폐기할 수 있다. 크기가 0이면 수신할 때 즉시 정리되므로 후속 호출이 필요 없다. 동기 송신은 이 소비·정리가 끝나야 반환하고, 비동기 송신의 완료 알림은 semaphore를 전달했을 때만 온다.

그림 3. 설명자 매칭과 뒤늦은 payload 복사

↔ 좁은 화면에서 각 스레드의 단계와 버퍼 수명을 좌우로 이동해 읽으세요.

시간
t0 · 송신
t1 · 매칭
t2 · 지연 수신
t3 · 완료
송신 버퍼
payload 원본 유효
주소가 수신 설명자로 전달
아직 보존 필요
소비 완료 후 재사용
동기 송신
상대 대기 · timeout 적용
상대와 만나면 계속 대기
수신자가 복사/폐기할 때까지
반환
비동기 송신
설명자 복제 후 반환*
payload는 복사 안 됨
sem != NULL이면 완료 대기 가능
sem != NULL일 때만 신호

* 비동기 설명자 풀이 비어 있으면 k_mbox_async_put()도 내부 k_stack_pop(K_FOREVER)에서 설명자를 기다릴 수 있다. 비동기 설명자 복제만으로 payload의 독립 저장본이 생기지는 않는다. sem != NULL이면 완료 신호까지, sem == NULL이면 앱이 별도로 소비 완료를 확인할 때까지 tx_data를 재사용하지 않는다.

표 4. ‘전송했다’와 ‘처리됐다’의 차이
경로기다리는 것버퍼·제약
k_mbox_put()timeout은 호환 수신자와 만나기까지. 매칭 후 payload 소비는 시간 제한 없이 기다릴 수 있음송신 버퍼를 호출 완료까지 보존
k_mbox_async_put()CONFIG_NUM_MBOX_ASYNC_MSGS > 0의 설명자 풀이 비면 내부적으로 기다릴 수 있음. 소비가 끝나면 sem != NULL인 경우에만 신호설명자는 복제해도 tx_data는 복제 안 됨. semaphore가 없으면 앱의 별도 완료 확인 전 재사용 금지
수신 버퍼 NULL실제 수신 크기 > 0이면 설명자만 받고 payload 복사를 미룸. 크기 0이면 즉시 정리크기 > 0일 때만 k_mbox_data_get(buffer) 또는 NULL로 복사·폐기
ISR이 API 계열은 thread 간 메시지 교환ISR에서 송신/수신하지 않음

근거: 고정 문서 Mailboxes, kernel/mailbox.c 25–85행, 194–333행, 335–425행.

5. Pipe는 레코드 경계 없이 바이트 수를 약속한다

CONFIG_PIPES=y의 k_pipe는 선택적 ring buffer를 둔 바이트 흐름이다. 버퍼 크기 0도 가능하며, 대기하는 상대에게 직접 복사할 수 있다. 한 put이 한 get에 그대로 대응한다는 보장은 없다. min_xfer는 완료로 인정할 최소 바이트 수이고, bytes_written/bytes_read는 실제 이동량이다. 반환값이 0이어도 요청 전체가 이동했는지 이 숫자로 검사해야 한다. 특히 min_xfer=0의 성공 0은 한 바이트도 이동하지 않았을 수 있다.

그림 4. 요청량과 최소량, 실제 이동량은 서로 다른 숫자

↔ 좁은 화면에서 직접 전달·ring 저장·부분 완료 단계를 이동해 읽으세요.

경로
요청
즉시 가능한 곳
판정
호출자가 할 일
버퍼 있음
10바이트 put
대기 독자 또는 ring에 6바이트 복사
min_xfer=4면 6바이트로 성공 가능
나머지 4바이트를 다시 보낼지 결정
버퍼 없음
10바이트 put
대기 독자에게만 직접 복사 가능
독자가 없고 K_NO_WAIT, min_xfer>0이면 미충족
bytes_written=0과 오류 확인

숫자는 API 동작을 읽기 위한 예시다. 실제 이동량은 동시 독자·writer, ring 공간, timeout에 따라 달라진다. min_xfer=0이라면 성공 0과 이동량 0도 함께 나올 수 있다.

표 5. Pipe에서 꼭 함께 읽을 반환값
조건3.7.0 구현해석
min_xfer > 요청량입력 오류 -EINVAL최소 요구량부터 바로잡기
K_NO_WAIT이고 즉시 가능한 양 < min_xfer복사 전 -EIO, 이동량 0ISR도 이 무대기 조건을 따라야 함
min_xfer=0K_NO_WAIT 또는 timeout 경계에서 성공 0·이동량 0 가능성공 코드만으로 전송을 주장하지 말고 이동량 확인
이동량 ≥ min_xfer이나 요청량보다 작음성공 0 가능남은 바이트 처리 책임은 호출자
thread 대기 후 최소량 미달-EAGAIN 가능, 이동량은 별도 기록오류값만 보고 부분 이동이 없었다고 단정 금지

근거: 3.7.0 Pipes, kernel/pipes.c 308–319행, 380–512행과 555–717행.

6. ISR에서 ‘쓸 수 있음’은 기다릴 수 있다는 뜻이 아니다

ISR는 잠들 수 없다. Zephyr 3.7.0의 FIFO/LIFO, message queue, pipe에는 ISR 허용 경로가 있지만 대기형 get/put과 구별해야 한다. 특히 k_msgq_put()과 k_pipe_put()은 ISR에서 K_NO_WAIT가 필요하다. mailbox는 이 계열과 달리 thread 간 교환용이다.

표 6. 호출 문맥과 대기 가능 범위
객체ISR send/putISR receive/getthread가 기다릴 때
FIFO / LIFOput 가능get(K_NO_WAIT)만빈 큐 get은 timeout 대기 가능
Message queueput(K_NO_WAIT)만get(K_NO_WAIT)만가득 찬 put·빈 get 모두 timeout 대기 가능
Mailbox불가불가동기 put의 수신 매칭 대기, 매칭 후 데이터 소비까지 대기
Pipeput(..., K_NO_WAIT)만get(..., K_NO_WAIT)만min_xfer 미달이면 timeout 대기 가능

실패 경로부터 설계한다. ISR가 보낸 데이터가 꽉 찬 ring이나 부족한 pipe 공간 때문에 거절될 수 있다. 즉시 실패를 단순히 무시하지 말고 데이터 유실 허용 여부와 재시도 주체를 정한다. FIFO/LIFO도 항목 수 상한이 없다는 말이 메모리 상한이 없다는 뜻은 아니다.

근거: 3.7.0 ISR 비교표, msg_q.c ISR assert, pipes.c ISR assert, 고정 Mailboxes 문서.

7. 보드에서 남길 검증 기록

  1. Zephyr commit, 최종 .config의 CONFIG_PIPES와 CONFIG_NUM_MBOX_ASYNC_MSGS, 객체 생성 위치·초기화 순서를 남긴다.
  2. FIFO/LIFO 항목마다 예약 word, 정렬, 할당 주체, put 뒤 재사용 시점, 중복 삽입 방지를 기록한다.
  3. Message queue의 msg_size × max_msgs, put/get 반환값, 가득 참과 purge 정책, 큰 레코드 복사 시간을 측정한다.
  4. Mailbox의 송신 버퍼 수명, 상대 매칭 조건, deferred data_get, 완료 semaphore와 async 설명자 풀 고갈을 별도 실험한다.
  5. Pipe의 요청 바이트·min_xfer·실제 이동량·반환값을 함께 기록하고, 부분 이동 뒤 남은 데이터 처리를 확인한다.
  6. ISR 경로는 모두 무대기 호출만 사용하고, 데이터 유실·재시도 규칙을 별도로 검증한다.

8. 고정 버전 근거와 설명의 경계

공식 v3.7.0 문서와 같은 commit의 공개 API·구현을 대조했다. A·B·C 및 바이트 수는 설명용이며, 보드에서의 처리량·ISR 지연을 측정한 값이 아니다. Mailbox의 비동기 payload 수명은 설명자만 복제하고 실제 복사는 수신 시 수행하는 고정 소스에서 도출한 사용 조건이다.

연결해서 읽기