Overlapped IO 모델을 이해해보자
Overlapped??
overlapped는 중첩이라는 뜻을 가지고 있다. IO를 중첩한다는 것이다.
하나의 스레드 내에서 동시에 둘 이상의 영역으로 데이터를 수신해, 입출력이 중첩되는 상황을 'IO 중첩'이라고한다.
IO중첩이 일어나기 위해선 입출력 함수가 논-블로킹 모드로 작동해야한다!
Overlapped IO의 핵심
overlapped IO는 이런 동작 원리에 있는 것이 아닌, 이 완료된 입출력의 확인방법에 있다. 논블로킹 모드로 진행되면, 완료 결과를 따로 확인해줘야한다. 윈도우에서 말하는 Overlapped IO는 입출력만을 뜻하는 게 아닌 완료를 확인하는 방법까지 포함한 것이다.
Overlapped 입출력 관련 함수
Overlapped IO 모델을 위해선 특별한 소켓을 사용할 수 있다.
소켓은 우리가 잘아는 그냥 SOCKET도 있지만, 윈도우 함수들을 좀 더 효율적으로 사용하기 위한 WSASocket이 있다.
WSASocket 사용하기
WSASocket은 기존 소켓과 생성 시 들어가는 인자값에 차이가 있다.
#include <winsock2.h>
SOCKET WSASocket(
int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWOORD dwFlags);
→ 성공 시 소켓, 실패시 INVALID_SOCKET을 반환함
세 번째 인자까지는 기본 소켓과 동일하다.
lpProtocollinfo | 생성되는 소켓의 정보를 담고있는 WSAPROTOCOL_INFO 구조체 주소, 필요없으면 NULL |
g | 함수의 확장을 위해 있는 것, 0을 넣기 |
dwFlags | 소켓 속성정보 |
Overlapped 전용 소켓을 생성하기 위해선 dwFlags에 'WSA_FLAG_OVERLAPPED'를 넣어주면 된다.
WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
Overlapped IO 진행을 위한 입/출력 함수
소켓간의 연결 과정은 일반 소켓과 차이가 없지만 송/수신에 있어선 다른 함수들이 존재한다.
Overlapped IO를 진행하기 위해 있는 송/수신 함수를 알아보자.
WSASend 함수
#include <winsock2.h>
int WSASend(
SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRouine);
→ 성공 시 0, 실패 시 SOCKET_ERROR를 반환함.
s | 소켓 - Overlapped IO 속성이 부여된 소켓이 전달 되어야 Overlapped IO모델로 진행함. |
lpBuffers | 전송할 데이터 정보를 지닌 WSABUF 구조체 주소 전달 - 1개도 가능하고 배열로도 가능함. |
dwBufferCount | 두 번째 인자로 전달된 배열의 길이 - 1개라면 1을 전달하면 됨. |
lpNumberOfbytesSent | 전송된 바이트 수가 저장될 변수 주소값 |
dwFlags | 함수의 데이터 전송특성 변경 시 사용. (MSG_OBB모드 등등) |
lpOverlapped | WSAOVERLAPPED 구조체 변수 주소 - Event 방법을 사용할 때 사용되는 매개변수 |
lpCompletionRoutine | Completion Routine 함수 주소 전달 (이 함수로 전달 완료 여부 확인 가능) |
IO가 비동기로 처리되니 바로 몇 바이트 전송이 완료 됐는 지 알 수 없다. 그래서 4번째 인자를 통해 입출력 후 전송된 바이트 크기를 알 수 있다는 것을 알 수 있다.
추가적으로 생소한 WSABUF, WSAOVERLAPPED 구조체가 어떤 식으로 이루어져 있는지 살펴보자.
WSABUF
typedef struct __WSABUF
{
u_long len; // 데이터 크기
char FAR* buf; // 버퍼 주소 값
} WSABUF, *LPWSABUF;
WSABUF는 버퍼의 정보를 담고 있다. 데이터 크기, 버퍼의 시작 주소 값 등을 전달해준다.
왜 사용할까?? => 나중엔 큰 버퍼를 잘게잘게 쪼게 사용하기도 할 것이다. 그럴 때 WSABUF가 유용하게 쓰일 것이다.
WSAOVERLAPPED
typedef struct _WSAOVERLAPPED
{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
우리가 집중해야 하는 것은 'hEvent' 이다. Internal InternalHigh는 IO가 진행될 때 운영체제 내부적으로 사용되는 변수들이고, OFffset, OffsetHigh 역시 사용하는 곳이 예약 되어있다. 우리는 'hEvent' 멤버만 사용하게 될것이다.
※주의점
- Overlapped IO를 진행 할 때 WSASend 함수의 매개변수 lpOverlapped는 NULL이 아닌, 사용 가능한 구조체 변수 주소를 전달해야한다. => NULL값이 전달되면 블로킹 모드로 동작하는 일반적인 소켓으로 간주됨!
- WSASend 함수로 동시에 둘 이상 영역으로 데이터를 전송하는 경우엔 인자로 전달되는 OVERLAPPEDD 구조체를 별도로 구성해야한다. => IO 과정에서 운영체제에 의해 참조되고 사용되기 때문에 같은 구조체가 들어가면안됨!
WSARecv 함수
#include <winsock2.h>
int WSARecv(
SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, DWORD lpFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRouine);
놀랍게도 WSASend와 차이가 없다. 수신이 송신관련으로 바꼈다고만 생각해주면 된다.
결과 확인 함수
#include <winsock2.h>
BOOL WSAGetOverlappedResult9
SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbtransfer, BOOL fWait, LPDWORD lpdwFlags);
→ 성공 시 TRUE, 실패 시 FALSE를 반환함
s | Overlapped IO 진행 소켓 |
lpOverlapped | Overlapped IO 진행 시 전달했던 WSAOVERLAPPED 구조체 변수 주소 |
lpcbTransfer | 실제 송수신된 바이트 크기를 저장할 변수 주소 |
fWait | 아직도 IO가 진행중일 때 대기 여부 - TRUE시 IO 완료까지 대기하고 FALSE시 false를 반환함. |
lpdwFlags | WSARecv함수가 호출 시 부수적인 정보(OOB 메세지 등) - 불필요 시 NULL 전달 |
입/출력 함수 동작
WSASend 나 WSARecv를 비동기적으로 호출 했을 때 우리는 두 가지 경우의 수로 결과를 얻을 수 있다.
- 운 좋게 입/출력 실행 시 바로 완료한 경우
- 반환 후 입/출력이 나중에 완료되는 경우
운 좋게 바로 완료가 되면 매개변수로 넣은 변수로 결과값을 얻을 수 있다.
하지만 바로 완료가 되지 않을 때도 있다. 이 상황을 Pending 상황이라고 한다. 그 상황엔 WSA_IO_PENDING이라는 오류가 발생한다.(오류가 아니지만 오류로 상황을 알리는 것)
WSAGetLastError라는 마지막 오류를 반환해주는 함수를 통해 이런 코드를 작성할 수 있다.
.....
if(WSASend(_socket, &dataBuf, 1, &sendBytes, 0, &overlapped,NULL) == SOCKET_ERROR){
if(WSAGetLastError() == WSA_IO_PENDING){
//Pending 상황
}
else{
// 진짜 오류 상황
}
}
Overlapped IO 입출력 완료 확인 방법
이제 본격적으로 입출력을 어떤 식으로 확인할 수 있는지 알아보자. 위에서 확인 방법엔 두 가지가 존재한다.
- Event활용하기
- Completion Routine 활용하기
두 가지 모두 차근차근 알아보자.
Event 오브젝트 활용
Event 오브젝트를 사용하면 이러한 순서가 나온다.
- IO 완료 시 WSAOVERLAPPED 구조체 변수가 참조하는 Event 오브젝트가 signaled 상태가 된다.
- IO의 결과 확인을 위해 WSAGetOverlappedResult 함수를 사용한다.
이외의 것은 WSAEventSelect 모델과 유사한 지식을 요구하니 아직 모르겠다면 이 글을 읽어보자.
활용
SOCKET hSocket;
.....
WSAEvent evObj;
WSAOVERLAPPED overlapped;
.....
evObj = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = evObj; // 이벤트 등록
if(WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL == SOCKET_ERROR){
if(WSAGetLastERROR() == WSA_IO_PENDING)
{
WSAWaitForMultipleEvnets(1, &evObj, TRUE, WSA_INFINITE, FALSE);
WSAGetOverlappedResult(hSocket,&overlapped, &sendbytes, FALSE, NULL);
}
}
printf("Send %d Bytes", sendbytes);
Completion Routine 활용
Completion Routine은 이러한 활용용도를 가지고 있다.
"Pending된 IO가 완료되면 이 함수를 호출해줘!"
하지만 중요한 작업 중 Completion Routine을 호출하면 프로그램 흐름이 엉망이 될 수 있다. 그래서 운영체제는 IO 요청 스레드가 alertable wait 상태에 놓여 있을 때만 Completion Routine를 호출한다.
alertable wait 상태? => 운영체제의 메세지 수신을 대기하는 스레드 상태, alertable wait 상태 진입시 각 스레드 마다 존재하는 APC큐에서 모든 routine을 실행시킴.
스레드를 alertable wait 상태로 만드는 함수에는 네 가지 정도가 있다.
- WaitForSingleObjectEx
- WaitForMultipleObjectEx
- WSAWaitForMultipleEvents
- SleepEx
Completion Routine 함수 인터페이스
completion routine에 함수를 활용하고 싶다면 함수의 인터페이스를 맞춰줘야한다.
void CALLBACK CompletionROUTINE(
DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);
dwError | 오류정보 - 오류가 없다면 0이 전달됨. |
cbTransferred | 입출력 데이터 크기 |
lpOverlapped | 입출력 함수 매개변수로 전달한 overlapped |
dwFlags | 입출력 함수로 전달된 특성 정보 or 0 |
void 옆 CALLBACK은 함수의 호출규약이니 꼭 삽입해주어야한다.
활용
if(WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, RecvCallback
== SOCKET_ERROR){
if(WSAGetLastError() == WSA_IO_PENDING)
::SleepEx(INFINITE, TRUE); // alertable wait 상태 진입
}
else{
printf("Recv %d Bytes", recvBytes);
}
.....
void CALLBACK RecvCallback(DWORD error, DWORD recvLen, LPWSAOVERLAPPED overlapped, DWORD flags) {
if(error != 0){
// 에러 발생
return;
}
printf("Recv CALLBACK %d Bytes", recvLen);
}
pending 되어 completion routine으로 실행됐다면 Recv CALLBACK 이 출력되고 바로 됐다면 Recv 가 출력될 것이다.
Event 방식과의 차이
Event 방식은 Event와 Overlapped를 일대일 매칭을 시켜줬어야했다. 다수를 처리해야한다면 정말 힘들어지지만 completion routine 방식은 alertable wait에 들어가면 모두 처리해준다.
※ 입/출력 함수에 들어가는 overlapped의 hEvent 멤버는 이벤트 방식일 때만 사용한다. 이 곳에 다른 데이터를 넣어서 더 많은 데이터를 Routine함수에 넣어줄 수 있다!
마무리
overlapped IO의 두 가지 방법에 대해 공부해봤다. overlapped IO는 한 가지 아쉬운 점이 남아있다. APC큐가 스레드 별로 있기 때문에 다른 스레드의 IO는 대신 처리해줄 수가 없어서 자신의 IO가 없다면 계속 alertable wait 모드에 남아있어야한다. 이러한 문제도 있지만, 현재는 간단한 입/출력 방법만 알아봤지만 accept도 계속 받아줘야하는 서버를 만들 경우 빈번한 alertable wait으로 인해 성능 또한 저하된다.