본문 바로가기
programing/Networks

Overlapped IO 모델

by RedWiz 2015. 6. 1.

 

 

Overlapped_IO.zip

 

- IO 중첩

> 하나의 쓰레드 내에서 동시에 둘 이상의 영역으로 데이터를 송수신 하여 입출력이 중첩되는 상황

> 호출된 입출력 함수가 바로 반환해야 가능(바로 반환하여 바로 다음 패킷 전송) -> 비동기 IO

> 비동기 IO가 가능하려면 호출되는 입출력 함수는 Non-Blocking 모드

> 입출력 뿐만 아니라 입출력의 완료를 확인하며 송수신

 

- Overlapped IO 소켓 생성

SOCKET WSASocket(

int af,                                                     // 프로토콜 체계

int type,                                                  // 소켓의 데이터 전송 방식

int protocol,                                            // 사용되는 프로토콜

LPWSAPROTOCOL_INFO lpProtocolInfo,    // 소켓의 특성정보(WSAPROTOCOL_INFO) 구조체

GROUP g,                                              // 함수 확장을 위해 예약된 매개 변수, 0

DWORD dwFlags                                    // 소켓 속성

);    // 성공 : 소켓 핸들, 실패 : INVALID_SOCKET

> WSA_FLAG_OVERLAPPED : Overlapped IO가능한 속성

 

- Overlapped IO 송신 함수

int WSASend(

SOCKET s,                                       // 소켓 핸들

LPWSABUF lpBuffers,                        // WSABUF 구조체 배열 주소

DWORD dwBufferCount,                     // WSABUF 구조체 수

LPDWORD lpNumberOfBytesSent,        // 전송된 바이트가 저장될 변수

DWORD dwFlags,                              // 함수의 데이터 전송 특성( ex)MSG_OOB-> OOB 모드 )

LPWSAOVERLAPPED lpOverlapped,    // WSAOVERLAPPED 구조체 주소, 전송 완료 확인 용도

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

// Completion Routine 함수 주소, 전송 완료 확인 가능

);    // 성공 : 0, 실패 : SOCKET_ERROR

 

typedef struct __WSABUF

{

u_long len;         // 전송할 데이터의 크기

char FAR* buf;    // 버퍼의 주소 값

} WSABUF, *LPWSABUF;

 

ex)

WSAEvent event;

WSAOVERLAPPED overlapped;

WSABUF dataBuf;

char buf[BUF_SIZE] = {"전송 데이터"}

int recvBytes = 0;

event = WSACreateEvent();

memset(&overlapped, 0, sizeof(overlapped));

overlapped.hEvent = event;

dataBuf.len = sizeof(buf);

dataBuf.buf = buf;

WSASend(hSocket, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);

 

typedef struct _WSAOVERLAPPED

{

DWORD Internal;

DWORD InternalHigh;

DWORD Offset;

DWORD OffsetHigh;

WSAEVENT hEvent;

} WSAOVERLAPPED, *LPWSAOVERLAPPED;

-> 이중 Internal와 InternalHigh는 운영체제 내부적으로 사용

-> Offset과 OffsetHigh도 예약 되어있는 변수

cf. WriteFileEx(), WriteFile(), ReadFileEx(), ReadFile()을 Overlapped IO로 사용시에 자동으로 포인터를 옮겨주지 않으므로 Offset과 Internal 변수를 이용하여 작업할 포인터를 설정하여 사용해야 한다.

 

> Overlapped IO를 진행하려면 WSASend()의 매개변수 lpOverlapped에는 항상 NULL이 아닌, 유효한 구조체 변수의 주소값을 전달 해야함. NULL일 경우 블로킹 모드로 동작하는 일반 소켓 간주

 

> WSASend() 호출을 통해서 동시에 둘 이상의 영역으로 데이터를 전송하는 경우 여섯 번째 인자로 전달되는 WSAOVERLAPPED 구조체 변수를 별도로 구성해야 함. WSAOVERLAPPED 구조체 변수가 Overlapped IO과정에서 운영체제에 의해 참조 되기 때문

 

> WSASend() 함수 반환과 데이터 전송 완료 시간이 일치

-> 0 반환, lpNumberOfBytesSent가 가리키는 곳으로 실제 전송된 데이터 크기 전달

> WSASend() 함수 반환 후 데이터 전송이 이루어짐

-> SOCKET_ERROR 반환, WSAGetLastError()에서 오류코드로 WSA_IO_PENDING이 등록

-> 의미는 당연히 입출력 진행중....

 

BOOL WSAGetOverlappedResult(

SOCKET s,                                       // 소켓

LPWSAOVERLAPPED lpOverlapped,    // WSAOVERLAPPED 구조체의 주소

LPDWORD lpcbTransfer,                    // 실제 송수신된 바이트 크기 저장할 변수 주소

BOOL fWait,                                     // IO 진행 중일 경우, TRUE : IO 완료까지 대기,

                                   FALSE : FALSE 반환하고 함수반환

LPDWORD lpdwFlags        // WSARecv() 호출시, 부수적인 정보(OOB 등) 확인, 불필요시 NULL

);    // 성공 : TRUE, 실패 : FALSE

> 실제 송수신된 데이터 크기 확인

 

- Overlapped IO 수신 함수

int WSARecv(

SOCKET s,                                        // 소켓

LPWSABUF lpBuffers,                         // WSABUF 구조체 배열 주소 값

DWORD dwBufferCount,                      // WSABUF 구조체 배열의 길이

LPDWORD lpNumberOfBytesRecvd,      // 수신된 데이터의 크기정보 저장될 변수 주소

LPDWORD lpFlags,                            // 전송 특성과 관련 정보 지정하거나 수신

LPWSAOVERLAPPED lpOverlapped,    // WSAOVERLAPPED 구조체 주소

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

// Completion Routine 함수 주소

);    // 성공 : 0, 실패 : SOCKET_ERROR

 

cf. 여러 버퍼에 존재하는 데이터를 모아서 한번에 전송하고(Gather 출력), 수신된 데이터를 여러 버퍼에 나눠서 저장하는(Scatter 입력) 것을 가리켜 Gather/Scatter IO라고 함.

 

- Overlapped IO에서 입출력의 완료 및 결과를 확인하는 방법

> WSASend(), WSARecv()의 여섯 번째 매개 변수 활용 방법, Event 오브젝트 기반

> WSASend(), WSARecv()의 일곱 번째 매개 변수 활용 방법, Completion Routine 기반

 

-1- Event 오브젝트 사용하여 입출력 완료 및 결과 확인

> IO가 완료 되면 WSAOVERLAPPED 구조체 변수가 참조하는 Event 오브젝트가 signaled 상태가 됨

> IO 완료 및 결과를 확인하려면 WSAGetOverlappedResult()를 사용

 

cf.

int WSAGetLastError(void);    // 오류상황에 대한 상태 값(오류의 원인을 알리는 값) 반환

> WSA_IO_PENDING : 함수의 호출 결과가 오류 상황이 아닌 완료되지 않은(Pending) 상황

데이터 입출력 진행중을 의미

 

-2- Completion Routine 사용하여  입출력 완료 및 결과 확인

> Pending된 IO가 완료되면(입출력이 끝나면) 지정 함수 호출

> 중요한 작업을 진행 중에 갑자기 Completion Routine이 호출되면 프로그램의 흐름을 망칠 수 있기 때문에 OS에서는 IO를 요청한 쓰레드가 alertable wait 상태에 놓여 있을 때만 호출하도록 한다.

(그러므로 Completion Routine에 진입하기 위해선 alertable wait 상태를 만들어 줘야 한다.)

> alertable wait 상태 : 운영체제가 전달하는 메시지의 수신을 대기하는 쓰레드 상태

 

> 쓰레드를 alertable wait 상태가 되게 하고 대기하는 함수

1) WaitForSingleObjectEx

2) WaitForMultipleObjectsEx

3) WSAWaitForMultipleEvents

4) SleepEx

-> 마지막 매개변수에 TRUE를 전달하면 해당 쓰레드가 alertable wait 상태가 되고 기다림

완료 루틴 호출이 끝나면 쓰레드는 alertable wait 상태에서 빠져나온다. 수치를 입력해도 무시하고 alertable wait 상태 빠져 나올때 까지 기다림.

 

> IO를 진행시킨 다음 급한 다른 볼일 들을 처리하고 나서, IO가 완료되었는지 확인하고 싶을 때 위의 함수들 중 하나를 호출 하면 됨. 그러면 운영체제는 쓰레드가 alertable wait 상태에 진입한 것을 인지하고 완료된 IO가 있으면 이에 해당하는 Completion Routine을 호출. Completion Routine이 실행되면 위 함수 모두 WAIT_IO_COMPLETION을 반환하면서 함수를 빠져나옴

 

> CompletioniRoutine 함수 형태 (함수 포인터 자료형으로 정해져 있음)

void (CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
    IN DWORD dwError,                                 // 오류정보 (정상 종료 : 0)
    IN DWORD cbTransferred,                        // 완료된 입출력 데이터 크기
    IN LPWSAOVERLAPPED lpOverlapped,     // WSASend(), WSARecv()의 lpOverlapped 전달 값
    IN DWORD dwFlags                                // 입출력 함수 호출시 전달한 특성정보, 0
);

 

* WSARecv(), WSASend() 호출시 Completion Routine을 이용하더라도 WSAOVERLAPPED 구조체 변수의 주소 값은 반드시 전달해 줘야한다. 단, 구조체 멤버 hEvent의 초기화를 위해 Event오브젝트를 생성할 필요는 없다.

 

* Completion Routine 기반의 Overlapped IO에서는 Event 오브젝트가 불필요해서 hEvent에 다른 정보를 넣어 활용해도 상관 없다.

 

* main쓰레드를 alertable wait 상태로 만들기위해 WSAWaitForMultipleEvents()를 사용시 더미 오브젝트를 생성하게 되는데 더미 오브젝트 생성을 피하려면 SleepEx()를 활용해야 함.

 

 

 

CompletionRoutineEcho.zip

 

 

- 하드웨어 성능이나 할당된 대역폭이 충분한 상황에서 응답시간이나 동시접속자 수에 문제 발생

> 비효율적인 IO의 구성 또는 비효율적인 CPU 활용

> 데이터베이스의 설계내용과 쿼리의 구성

 

- Non-Blocking 모드로 동작하는 소켓

SOCKET hRecvSock;

int mode = 1;

hRecvSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

ioctlsocket(hRecvSock, FIONBIO, &mode);    // for non-blocking socket

> ioctlsocket() : IO 방식을 컨트롤 하는 함수

> 핸들 hRecvSock이 참조하는 소켓의 입출력(FIONBIO)를 변수 mode에 저장된 값의 형태로 변경

> 세 번째 인자로 전달된 주소 값의 변수가 0 : blocking, 0이 아니면 : non-blocking

> non-blocking으로 변경

-> 클라이언트의 연결요청이 존재하지 않은상태에서 accept()가 호출 되면 INVALID_SOCKET이 바로 반환. WSAGetLastError() 호출하면 WSAEWOULDBLOCK이 반환

-> accept() 호출을 통해 생성된 소켓도 non-blocking 속성임

* non-blocking 소켓 대상으로 accept() 호출시 INVALID_SOCKET이 반환되면 WSAGetLastError()을 통해서 INVALID_SOCKET 원인을 알아내고 적절히 처리해야함

 

- Overlapped IO 모델 with Completion Routine

> 입출력 완료 시 자동으로 호출되는 Completion Routine 내부로 클라이언트 정보(소켓과 버퍼)를 전달하기 위해서 WSAOVERLAPPED 구조체의 멤버 hEvent를 사용

(Completion Routine 이용시 Event 오브젝트는 불필요하므로 WSAOVERLAPPED 구조체의 멤버 변수 hEvent를 정보 전달 용도로 사용 가능)

> non-blocking 모드의 accept()와 alertable wait 상태로의 진입을 위한 SleepEx()가 번갈아 가며 반복 호출 되는 것은 성능에 영향을 미칠 수 있음

* 해결책 : accept() 호출은 main()쓰레드가 처리하고 별도의 쓰레드를 추가로 하나 생성해서 모든 클라이언트와의 입출력을 담당하게 함. (IOCP에서 제안하는 서버 구현 모델)

-> 입력과 출력은 non-blocking 모드로 동작

-> non-blocking 모드로 진행된 입력과 출력의 완료 확인 방법