한눈에: RIO가 무엇인가

Registered I/O(등록형 I/O, 줄여서 RIO)는 Windows 8 · Server 2012부터 제공되는 고성능 소켓 방식. 목적은 IOCP와 같다 — 많은 연결의 비동기 송수신. 다른 점은 커널과 유저 공간을 오가는 비용을 걷어내는 데 초점.

핵심 아이디어 한 줄: 미리 등록해두고, 유저 공간의 링(ring)으로 완료를 주고받는다.

IOCP에서 초당 수십만 건의 완료를 처리할 때 매 완료마다 GetQueuedCompletionStatus로 커널에 들어갔다 나오는데, RIO는 이 왕복 자체를 없애는 것이 가장 큰 차이.

시작하기 전에 (전제)

<aside> 📌

IOCP와 비교, 핵심 차이 3가지

<aside> 1️⃣

버퍼를 미리 등록한다. IOCP는 WSARecv/WSASend를 부를 때마다 커널이 그 버퍼의 물리 메모리 페이지를 고정(pin). RIO는 시작할 때 RIORegisterBuffer한 번만 고정하고, 이후 송수신은 "등록된 영역 안의 어디"만 가리킨다. 이름의 "Registered"가 바로 이것이다.

</aside>

<aside> 2️⃣

제출과 완료가 유저 공간 링이다. 완료를 걷을 때 RIODequeueCompletion은 유저 공간에 매핑된 완료 큐에서 여러 건을 한 번에 긁어온다 — 커널 진입 없이. IOCP의 "완료 1건당 시스템 호출 1번"이 "여러 건을 시스템 호출 0번으로"가 된다.

</aside>

<aside> 3️⃣

통지는 필요할 때만 요청한다. 바쁠 때는 워커가 잠들지 않고 링을 계속 돌며 완료를 긁는다(커널 진입 0). 완료가 비어 할 일이 없을 때만 RIONotify로 "다음 완료가 오면 깨워달라"를 무장하고 잠든다.

</aside>

용어 대응표 (IOCP → RIO)

IOCP를 안다면 이 표를 참고.

IOCP RIO 무엇이 달라지나
WSARecv / WSASend RIOReceive / RIOSend I/O 제출 함수. 이름만 다르고 역할은 같다
GetQueuedCompletionStatus (완료 1건당 커널 진입) RIODequeueCompletion (유저 공간 링에서 여러 건을 한 번에, 커널 진입 없음) 완료를 걷는 방식 — 성능의 핵심 차이
IOCP 포트 (프로세스에 1개, 워커 풀이 공유) CQ = 완료 큐(Completion Queue), 워커마다 1개 완료가 모이는 곳
CreateIoCompletionPort (소켓을 완료 포트에 바인드) RQ = 요청 큐(Request Queue), 연결마다 1개 소켓↔완료 큐 결합은 같다. RIO는 여기서 미결 I/O 개수까지 미리 예약 — 그 예약이 IOCP엔 없던 것
호출할 때마다 커널이 버퍼를 물리 메모리에 고정 RIORegisterBuffer로 시작 시 한 번만 고정 버퍼 등록 — RIO의 "Registered"
OVERLAPPED (I/O 요청마다 하나) RequestContext (완료에 실려 오는 식별값) 이 완료가 recv인지 send인지 구분
CompletionKey (소켓마다) SocketContext 어느 연결의 완료인지 식별
완료가 나면 워커가 항상 깨어남 RIONotify를 무장해야만 이벤트로 깨어남 바쁠 땐 안 자고 계속 돎 (커널 진입 0)
CancelIoEx로 대기 중 I/O 취소 취소 API가 없다. closesocket으로 대체 종료 설계가 가장 크게 바뀐다 (뒤에서 상세)

RIO API 한 바퀴

RIO가 제공하는 함수들을 확인한다. IOCP의 WSARecv/WSASend/GetQueuedCompletionStatus에 해당하는 것들이다.

핵심 함수 8개

함수 하는 일 반환 (실패값)
WSAIoctl(sock, SIO_GET_MULTIPLE_EXTENSION_FUNCTION_POINTER, &WSAID_MULTIPLE_RIO, ...) RIO 함수 포인터 테이블을 런타임에 획득 (프로세스 1회) 0 = 성공
RIORegisterBuffer(char* buf, DWORD len) 버퍼를 물리 고정+등록. 이후 RIO_BUF가 이 ID+오프셋으로 가리킴 RIO_BUFFERID (RIO_INVALID_BUFFERID)
RIOCreateCompletionQueue(DWORD size, RIO_NOTIFICATION_COMPLETION* n) 완료 큐(CQ) 생성. n으로 통지 방식 지정 RIO_CQ (RIO_INVALID_CQ)
RIOCreateRequestQueue(SOCKET s, maxRecv, maxRecvBuf, maxSend, maxSendBuf, recvCQ, sendCQ, ctx) 소켓에 요청 큐(RQ) 부착. 미결 개수·완료 CQ·컨텍스트 지정 RIO_RQ (RIO_INVALID_RQ)
RIOReceive(RIO_RQ rq, RIO_BUF* buf, ULONG n, DWORD flags, void* reqCtx) 수신 요청 제출 BOOL (FALSE)
RIOSend(RIO_RQ rq, RIO_BUF* buf, ULONG n, DWORD flags, void* reqCtx) 송신 요청 제출 BOOL (FALSE)
RIONotify(RIO_CQ cq) "다음 완료가 오면 통지해달라"를 1회 무장 INT (0 = 성공)
RIODequeueCompletion(RIO_CQ cq, RIORESULT* arr, ULONG size) 완료를 배치로 걷음 (커널 진입 없음) 걷은 개수 (RIO_CORRUPT_CQ)

(해제는 대칭으로 RIODeregisterBuffer · RIOCloseCompletionQueue. RQ는 소켓 수명을 따라가 별도 해제 함수가 없다.)

완료에 실려 오는 두 구조체