본문 바로가기

CS/프밍 잡지식

Overlapped I/O 알아보기 - 1

개요

오늘은 고성능 서버에서 사용하는 Overlapped IO에 대해서 알아보겠습니다.

Overlapped IO는 말그대로 입출력을 중첩하는 방식입니다.

 

자세히 알아보기 전에 먼저 쉬운 이해를 위해 blocking, non-blocking IO과 동기, 비동기 IO를 생각해보겠습니다.

 

blocking IO

blocking IO는 입출력 요청 시 해당 입출력이 전부 완료될 때까지 해당 스레드가 wait상태에서 대기하고, IO가 완료된다면 그 때 함수가 return되어 이후 로직이 실행되는 방식입니다.

 

non-blocking IO

non-blocking은 입출력의 완료 여부와 상관없이, 함수가 return합니다. 따라서 해당 thread는 blocking과 다르게 바로 wait상태가 되지 않습니다. 만약 실패한다면 WOULDBLOCK(socket) 등의 상태를 얻을 수 있습니다.

 

동기

작업의 요청과 작업의 완료 시점이 같은 방식입니다.

같은 함수에서 입출력을 요청하고 완료하는 것도 확인하는 것이 동기 방식이라고 볼 수 있습니다.

 

비동기

작업의 요청과 작업의 완료 시점이 다른 방식입니다.

함수에서 입출력을 요청하고 다른 곳에서 확인할 수 있습니다.

 

blocking 동기 방식과 blocking 비동기 방식, non-blocking 비동기 방식은 가능하지만

blocking 비동기 방식은 가능하지 않습니다. 입출력이 완료되기를 기다리면서 완료 시점이 다를 수 없기 때문입니다.


non-blocking 방식에서 기대해볼 점

non-blocking 비동기 방식을 사용하면 스레드가 장치로부터 응답을 대기하지 않으면서 장치와 통신을 할 수 있기 때문에 입출력을 기다리는 동안 다른 처리를 기대할 수 있습니다. 

 

또한 non-blocking 동기 방식에서는 polling처럼 해당 입출력이 완료되었는지 확인해야 하지만, 비동기 방식에서는 완료 통지대기 시점을 정해 놓고 통지가 오기 전까지는 thread가 쉴 수 있습니다.

 

추가적으로 Overlapped IO를 통해 여러 입출력을 중첩시킬 수 있습니다.

이로 인해 여러 입출력 요청이 커널에 등록되고, IO장치의 상황에 따라 병렬로 처리될 수 있습니다.

 

이로 인해 더 많은 입출력 처리를 동시에 시도해 빠른 처리를 기대할 수 있습니다.

 


OVERLAPPED 구조체 

https://learn.microsoft.com/ko-kr/windows/win32/api/minwinbase/ns-minwinbase-overlapped

 

OVERLAPPED(minwinbase.h) - Win32 apps

비동기(또는 겹치는) 입력 및 출력(I/O)에 사용되는 정보를 포함합니다.

learn.microsoft.com

Windows에서 overlapped io사용을 위해서 하나의 입출력마다 OVERLAPPED구조체를 사용해 식별합니다.

 

OVERLAPPED 구조체에 대해 보겠습니다.

typedef struct _OVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    } DUMMYSTRUCTNAME;
    PVOID Pointer;
  } DUMMYUNIONNAME;
  HANDLE    hEvent;
} OVERLAPPED, *LPOVERLAPPED;

 

typedef struct _WSAOVERLAPPED {
  DWORD    Internal;
  DWORD    InternalHigh;
  DWORD    Offset;
  DWORD    OffsetHigh;
  WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

union을 제외한다면 같은 구조체이다.

 

Internal: IO요청에 대한 상태코드로, STATUS_PENDING이라면 아직 IO가 완료되지 않은 상황입니다.

InternalHigh: 송수신 바이트 등 추가 정보를 담습니다. 

해당 정보들은 시스템 내부적으로 사용하며 직접 유저가 사용하지는 않습니다.

 

Offset, OffsetHigh: 파일의 오프셋을 지정합니다. 소켓의 경우 오프셋을 지정할 필요는 없습니다.

 

hEvent: Alertable Event를 사용할 때 지정합니다.


WSABUF

Overlapped IO에서 사용하는 WSASend와 WSARecv는 여러 개의 입출력을 한번에 할 수 있습니다.

각각의 입출력을 식별하기 위해 시작위치 buffer 포인터와 보내는 데이터 크기 len을 WSABUF에 설정하고 WSABUF배열을 넘기게 됩니다.

typedef struct _WSABUF {
  ULONG len;
  CHAR  *buf;
} WSABUF, *LPWSABUF;

WSASend와 WSARecv

비동기 입출력을 위해서는 send, recv 대신 Overlapped 구조체를 인자로 받는 WSASend, WSARecv를 사용해야합니다. 

int WSAAPI WSASend(
  [in]  SOCKET                             s,
  [in]  LPWSABUF                           lpBuffers,
  [in]  DWORD                              dwBufferCount,
  [out] LPDWORD                            lpNumberOfBytesSent,
  [in]  DWORD                              dwFlags,
  [in]  LPWSAOVERLAPPED                    lpOverlapped,
  [in]  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

s: 통신을 실행할 소켓이다.

lpbuffers: buffer의 시작 주소를 담습니다. 여러 버퍼를 사용한다면 배열의 시작 위치를 담습니다.

dwBufferCount: WSABUF의 개수입니다. WSABUF에는 버퍼 길이가 존재하기 때문에 WSABUF가 여러개인 경우 배열의 시작 주소만 넘겨줘도 됩니다.

lpNumberOfBytesSent: 송신한 바이트를 받습니다. 이 변수는 송신이 즉시 끝난 경우 = retval이 0인경우에만 사용하는 값입니다.

lpoverlapped: 사용할 Overlapped구조체를 넣습니다.

lpCompletionRoutine: completion rountine을 사용하는 구조의 경우 해당 인자에 콜백 함수의 주소를 넣는다면 송수신이 완료된 후 해당 루틴을 실행합니다.

 

실제로 사용하는 경우 아래와 같이 사용해볼 수 있습니다. 만약 Completion Routine을 사용하지 않는 구조라면 NULL을 넣으면 됩니다.


Overlapped I/O를 사용하는 구조

중첩 입출력을 사용하는 구조로는 대표적으로 3가지가 있습니다.

1. Event 객체의 Signal상태를 통해 입출력의 완료를 인지합니다.

2. 입출력이 완료되면 Completion Routine을 실행시켜 이후 처리를 진행합니다.

3. Completion Port를 생성하여 이를 통해 입출력의 완료를 통지받습니다.

 

저는 3번 방법에 대해서 더욱 집중적으로 알아볼 것이므로 다음 포스팅에서는 3번 방식인 IOCP에 대해 알아보고

기회가 된다면, 1번과 2번 방법에 대해서도 알아보겠습니다.