Registered I/O(등록형 I/O, 줄여서 RIO)는 Windows 8 · Server 2012부터 제공되는 고성능 소켓 방식. 목적은 IOCP와 같다 — 많은 연결의 비동기 송수신. 다른 점은 커널과 유저 공간을 오가는 비용을 걷어내는 데 초점.
핵심 아이디어 한 줄: 미리 등록해두고, 유저 공간의 링(ring)으로 완료를 주고받는다.
IOCP에서 초당 수십만 건의 완료를 처리할 때 매 완료마다 GetQueuedCompletionStatus로 커널에 들어갔다 나오는데, RIO는 이 왕복 자체를 없애는 것이 가장 큰 차이.
<aside> 📌
winsock2.h, mswsock.h(RIO 확장·RIO_* 타입과 GUID 정의), ws2tcpip.h.ws2_32.lib.WSAIoctl로 함수 포인터 테이블을 받아 그걸로 호출한다 (아래 부트스트랩 참조).WSASocket 생성 시 WSA_FLAG_REGISTERED_IO를 줘야 하고, 그 소켓에는 일반 send/recv를 못 쓴다(RIO 함수 전용).
</aside><aside> 1️⃣
버퍼를 미리 등록한다. IOCP는 WSARecv/WSASend를 부를 때마다 커널이 그 버퍼의 물리 메모리 페이지를 고정(pin). RIO는 시작할 때 RIORegisterBuffer로 한 번만 고정하고, 이후 송수신은 "등록된 영역 안의 어디"만 가리킨다. 이름의 "Registered"가 바로 이것이다.
</aside>
<aside> 2️⃣
제출과 완료가 유저 공간 링이다. 완료를 걷을 때 RIODequeueCompletion은 유저 공간에 매핑된 완료 큐에서 여러 건을 한 번에 긁어온다 — 커널 진입 없이. IOCP의 "완료 1건당 시스템 호출 1번"이 "여러 건을 시스템 호출 0번으로"가 된다.
</aside>
<aside> 3️⃣
통지는 필요할 때만 요청한다. 바쁠 때는 워커가 잠들지 않고 링을 계속 돌며 완료를 긁는다(커널 진입 0). 완료가 비어 할 일이 없을 때만 RIONotify로 "다음 완료가 오면 깨워달라"를 무장하고 잠든다.
</aside>
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가 제공하는 함수들을 확인한다.
IOCP의 WSARecv/WSASend/GetQueuedCompletionStatus에 해당하는 것들이다.
| 함수 | 하는 일 | 반환 (실패값) |
|---|---|---|
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는 소켓 수명을 따라가 별도 해제 함수가 없다.)