Zephyr v3.7.0 · 비동기 I/O

RTIO: 제출한 요청과 회수한 완료 사이

RTIO는 I/O의 작업 설명서인 SQE와 결과 통지인 CQE를 분리한다. submit의 반환, 요청의 실제 완료, CQE의 소비, 버퍼의 반환은 서로 다른 사건이다. 이 순서를 섞지 않아야 비동기화가 안전해진다.

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

기준은 Zephyr v3.7.0, commit 36940db938a8f4a1e919496793ed439850a221c2다. CONFIG_RTIO=y와 해당 요청을 처리하는 실제 iodev 구현이 있다는 전제에서 RTIO 계약을 설명한다. 모든 주변장치·보드가 RTIO를 네이티브로 지원한다는 뜻은 아니다. 그림과 표는 이 글을 위해 새로 그렸다.

1. 두 큐 사이에서 누가 무엇을 하는가

RTIO_DEFINE은 SQE와 CQE의 크기가 정해진 풀을 둔 컨텍스트를 정의한다. 애플리케이션이 rtio_sqe_acquire()로 SQE를 얻어 작업과 iodev를 기록하면 제출 큐에 들어간다. rtio_submit() 이후 executor가 SQE를 iodev의 submit 함수에 넘긴다. iodev는 장치 작업을 시작하거나 자체 큐에 넣고, 결과를 rtio_iodev_sqe_ok() 또는 rtio_iodev_sqe_err()로 executor에 알린다. 그 결과가 CQE로 생산되면 소비자가 읽고 풀 슬롯을 돌려준다.

그림 1. SQE에서 CQE까지: 요청을 넘기는 경계와 돌아오는 결과

↔ 작은 화면에서는 왼쪽에서 오른쪽으로 작업 경로를 이동해 보세요.

애플리케이션sqe_acquire → 작업·버퍼·userdata 준비 → submit요청 설명을 만든다
RTIO 컨텍스트SQE 풀 + 제출 큐 / CQE 풀 + 완료 큐두 풀의 크기는 유한하다
Executor순서·체인 플래그를 해석하고 iodev submit 호출장치 자체가 아니다
IODev·드라이버지원하는 요청을 수행하고 ok/err로 보고구현·장치별 능력을 확인한다
← 완료 경로: 드라이버 보고 → executor의 CQE 생산 → 애플리케이션 consume → CQE release

근거: 컨텍스트·submit 시작 경계, iodev의 submit 계약, executor의 실제 iodev 전달, 3.7.0 RTIO 문서.

2. SQE는 명령이고 CQE는 결과다

SQE에는 작업 종류, 우선순위, 플래그, iodev, 버퍼 참조와 userdata가 들어간다. CQE에는 result, 되돌아온 userdata, 버퍼 관련 플래그가 있다. userdata를 요청마다 구별되게 주면 완료 순서가 제출 순서와 달라도 어느 요청의 결과인지 매칭할 수 있다. 일반 독립 SQE들의 CQE 순서를 제출 순서라고 가정하지 않는다. 순서가 필요한 작업은 RTIO_SQE_CHAINED의 별도 계약을 살펴야 한다.

표 1. SQE 요청과 CQE 결과
항목SQE · 요청CQE · 결과
대표 필드op, flags, iodev, 버퍼, userdataresult, userdata, flags
누가 채우는가호출자가 준비; executor·iodev가 사용완료 경로가 생산; 호출자가 읽음
용량 한계획득에 실패하면 NULL; 복사 제출도 -ENOMEM 가능회수 뒤 release해야 CQE 풀 슬롯 재사용
식별호출자가 고유한 userdata를 정할 수 있음그 값으로 비순차 완료를 대응

순서를 보장한다고 해서 실패할 때 중단되는 것은 아니다. RTIO_SQE_CHAINED는 다음 작업이 앞 작업의 완료를 기다리게 한다. 그러나 이 글의 v3.7.0 고정 구현에서는 앞 작업이 실패해도 rtio_executor_done()이 다음 chained SQE를 rtio_iodev_submit()에 넘긴다. 이 경로는 다음 SQE에 취소 플래그를 자동으로 세우지 않으므로, 앞 작업의 실패만으로 뒤 작업의 실행까지 막는다고 가정하면 안 된다. 헤더의 failure cascading 설명과 이 구현을 구분해야 한다. RTIO_SQE_TRANSACTION은 한 iodev에 맡기는 별도 단위이며, 오류 결과를 같은 transaction의 후속 CQE에 전달하는 경로와도 다르다. 이 글의 이후 예시는 독립적인 단발 요청이다. 고정 executor의 완료·후속 제출 분기를 함께 확인한다.

근거: 체인·트랜잭션·플래그, SQE·CQE 구조체, 완료 순서 설명.

3. submit(0), submit(n), consume은 서로 다르다

rtio_submit(&rtio, 0)은 executor에 쌓인 SQE를 전달하고 특정 완료 수를 기다리지 않는다. 그렇다고 함수가 항상 즉시 돌아온다거나 장치 작업을 절대로 호출자 문맥에서 수행하지 않는다는 보장은 없다. rtio_submit(&rtio, n)의 양수 wait_count는 n건의 완료를 기다리는 경로를 연다. 이것은 CQE를 소비·해제하는 호출이 아니다. 결과 필드가 필요하면 rtio_cqe_consume() 또는 rtio_cqe_consume_block()로 얻어 확인하고 rtio_cqe_release()로 돌려줘야 한다.

그림 2. “완료를 기다림”과 “완료 항목을 회수함”의 다른 시계

↔ 좁은 화면에서는 왼쪽에서 오른쪽으로 단계별 진행을 확인하세요.

순서① SQE 준비② executor 전달③ 완료 생산④ 결과 회수
submit(0)
SQE와 버퍼 준비
기다릴 수 0전달 호출은 수행
작업 완료는 나중일 수 있음
consume[_block] → release
submit(n)
SQE와 버퍼 준비
양수 n완료 수에 이를 때까지 대기
대기가 끝나도 결과 내용은 미회수
consume → 결과 읽기 → release
표 2. 제출·대기·소비 API의 차이
호출보장하는 진행아직 해야 할 일
rtio_submit(r, 0)현재 SQE를 executor에 제출; 완료 수 대기 없음별도 완료 확인과 CQE 회수
rtio_submit(r, n>0)n건 완료를 기다리는 경로CQE의 result·userdata 검사와 release
rtio_cqe_consume(r)현재 CQE 하나를 시도; 없으면 NULL읽은 뒤 rtio_cqe_release
rtio_cqe_consume_block(r)CQE 하나가 나올 때까지 기다려 회수읽은 뒤 rtio_cqe_release

동일 컨텍스트에 여러 스레드가 동시에 submit(n>0)으로 대기하는 설계는 피한다. v3.7.0의 CONFIG_RTIO_SUBMIT_SEM 경로에는 다른 스레드의 동시 submit이 미정의라는 TODO가 있다. wait_count는 “특정 userdata의 성공”도 보증하지 않으므로 결과를 CQE에서 확인한다. CQE 풀이 가득 차면 구현은 누락 수를 별도 계수하므로, 완료 수 대기만으로 모든 결과 항목이 보존됐다고 가정하지 않는다.

근거: submit의 wait_count·동시 호출 TODO, consume와 release, CQE 풀 포화 분기.

4. cancel()의 0은 장치 중단 완료가 아니다

rtio_sqe_cancel(sqe)은 SQE와 이어진 항목에 취소 플래그를 세우고 0을 반환한다. executor가 아직 iodev에 보내기 전이면 취소를 적용할 기회가 있다. 그러나 이미 iodev의 submit에 들어간 요청은 플래그 설정이 실제 DMA·버스 작업을 되돌리는 명령이 아니다. 공식 문서도 실행 중인 요청의 취소가 보장되지 않는다고 밝힌다. 더구나 이 고정 구현은 취소 플래그가 보이는 완료 경로에서 CQE를 만들지 않는 분기가 있으므로, “취소했다면 반드시 -ECANCELED CQE를 받는다”라고 설계하지 않는다. 취소한 요청의 CQE만 무조건 consume_block으로 기다리는 방식도 피한다. 사전 취소된 SQE 하나만 제출하면서 rtio_submit(r, 1)로 한 건의 완료를 기다리면, 다른 완료가 없는 한 대기가 끝나지 않을 수 있다. 취소 호출 결과를 기다릴 CQE 수로 세지 않는다.

그림 3. 취소 플래그를 세운 순간에 따라 달라지는 실행 경계

↔ 좁은 화면에서는 아직 대기 중인 SQE부터 드라이버 실행 중인 SQE까지 보세요.

① 제출 큐에 남음

취소 플래그를 executor가 볼 수 있다. iodev에 보내지 않고 처리할 수 있다.

사전 취소 경로는 NO_RESPONSE도 표시한다. 이 요청을 submit(r, 1)의 대기 완료로 세면 멈출 수 있다.
② executor가 꺼냄

iodev 전달 직전에 플래그를 검사한다. 어느 쪽이 먼저 일어났는지가 관건이다.

취소 호출의 0은 플래그 설정 결과다.
③ iodev가 시작함

드라이버·하드웨어의 실제 작업 중단은 보장되지 않는다.

사용 중인 버퍼를 즉시 재사용하면 위험하다.
표 3. 취소 전후에 확인할 수 있는 상태
관찰말할 수 있는 것단정하면 안 되는 것
cancel 반환 0대상 SQE에 취소 플래그를 표시함하드웨어 I/O가 멈췄거나 버퍼가 안전해짐
executor 전달 전플래그 검사로 iodev 호출을 피할 수 있음언제나 -ECANCELED CQE를 받거나 이 요청만으로 submit(r, 1) 대기가 끝남
iodev 실행 중드라이버의 실제 작업과 경합함일괄 취소·콜백 억제·부작용 없음

근거: 3.7.0 취소 경고, cancel은 플래그 설정, 전달 전 취소·NO_RESPONSE, 완료할 때 CQE를 억제하는 분기.

5. 같은 “버퍼”라도 소유권의 끝이 다르다

일반 read/write SQE는 호출자 버퍼의 주소를 기록한다. rtio_sqe_prep_tiny_write()는 아주 짧은 송신 데이터를 SQE 내부로 복사한다. CONFIG_RTIO_SYS_MEM_BLOCKS=y와 메모리 풀 컨텍스트를 사용하고 이를 지원하는 iodev가 rtio_sqe_rx_buf()를 호출하면 풀 버퍼를 확보할 수 있다. CQE를 release했다고 해서 풀 버퍼가 반환되지는 않는다. 풀 버퍼는 rtio_release_buffer()로 따로 반환한다.

표 4. 버퍼와 큐 슬롯의 수명
자원요청에 들어가는 것안전한 사용 경계
일반 RX/TX 버퍼호출자 메모리의 포인터작업이 끝나기 전 수정·반환하지 않음; 완료 결과와 드라이버 계약 확인
tiny write최대 7바이트를 SQE 내부에 복사원본 짧은 값은 prep 이후 독립; SQE는 executor가 관리
mempool readCONFIG_RTIO_SYS_MEM_BLOCKS=y + RTIO_DEFINE_WITH_MEMPOOL + 지원 iodevCQE에서 버퍼를 조회하고 처리 후 rtio_release_buffer
CQE 슬롯결과 구조체 포인터필요한 필드를 읽거나 복사한 뒤 rtio_cqe_release; 이후 포인터 재사용 금지
SQE 핸들유한 풀의 엔트리완료 후 풀에 돌아가므로 오래 보관한 핸들로 취소하지 않음

취소 요청만으로 버퍼를 해제하지 않는다. 이 버전은 이미 시작한 I/O의 중단과 취소 CQE를 보장하지 않으므로, 해당 iodev의 종료·소유권 계약 없이 버퍼를 재사용하면 경합할 수 있다. 풀 기반 read에서도 CQE release만으로 데이터 버퍼까지 반환되지 않는다.

근거: read/write 포인터와 tiny write 복사, iodev의 풀 버퍼 획득, CQE에서 풀 버퍼 조회, 별도 메모리 풀 반환, 실제 CQE·버퍼를 각각 반환하는 센서 예시.

6. RTIO API가 있다고 내 드라이버가 처리하는 것은 아니다

executor는 iodev->api->submit으로 요청을 넘긴다. 그러므로 애플리케이션이 어떤 작업을 준비할 수 있다고 해서 모든 드라이버가 그 작업을 구현했다고 볼 수는 없다. 이 고정 소스의 I²C RTIO 계층은 요청을 자체 큐에 넣고 완료를 executor에 보고한다. 센서 계층은 드라이버의 submit 구현이 있으면 이를 호출하고, 없으면 단발 read에서 기존 동기 API를 RTIO work queue로 감싸는 경로를 제공한다. 스트리밍인데 드라이버 구현이 없으면 -ENOTSUP으로 끝난다.

표 5. 드라이버별 RTIO 지원 경계
경로고정 소스에서 확인한 일판단할 것
I²C RTIO 계층i2c_iodev_api.submit → I²C 요청 큐 → ok/err선택한 컨트롤러가 이 경로를 연결하고 요청 플래그를 처리하는가
센서 네이티브센서 드라이버의 api->submit 호출해당 센서가 비동기 submit을 구현했는가
센서 단발 fallbackwork queue에서 기존 sensor_sample_fetch·channel_get 사용API는 비동기여도 센서 하드웨어가 네이티브 비동기라는 뜻은 아님
센서 스트리밍 미지원submit 구현 없으면 -ENOTSUP요구하는 stream 기능의 드라이버별 지원 확인

근거: executor → iodev, I²C iodev API, I²C 완료·큐, 센서 네이티브·fallback·미지원 분기, fallback 동기 fetch·channel read.

7. 적용 전에는 요청 수와 회수 속도를 같이 설계한다

표 6. 요청·회수 설계 점검 질문
점검 질문확인할 고정 계약어긋나면 나타나는 일
선택한 iodev가 이 op를 처리하는가?드라이버 submit 구현과 구성제출 자체는 되지만 CQE에 오류 결과
SQE를 몇 개 동시에 잡는가?RTIO_DEFINE의 sq_szsqe_acquire가 NULL
CQE를 얼마나 빨리 소비·해제하는가?cq_sz와 실제 완료 발생량결과 항목 부족·누락 계수 증가 가능
결과를 어떻게 대응하는가?CQE의 userdata·result완료 순서를 요청 순서로 오인
취소 후 자원은 누가 확인하는가?드라이버 진행 상태와 버퍼 수명진행 중 I/O와 메모리 재사용 경합

실행 검증은 한 번에 여러 요청을 밀어 넣는 부하, 일부 요청의 실패, CQE 소비를 지연시키는 경우, 취소와 실제 완료가 교차하는 경우를 별도로 본다. 이 글의 도식은 구현의 제어 흐름을 설명하며 어떤 보드에서 특정 지연이나 처리량을 측정한 결과는 아니다.

근거: 고정 SQE·CQE 크기, SQE 부족 반환, CQE 부족 기록.

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

기준 문서는 Zephyr 3.7.0 RTIO다. 공개 타입·API는 고정 커밋의 rtio.h, 실행 순서는 rtio_executor.c, 드라이버 참여 예시는 I²C RTIO와 센서 RTIO fallback에 고정했다. 위 링크의 줄 범위는 각 주장과 직접 연결된다.