- 클릭하세요 온라인 게임 네트워크 프로그래밍
- 신동훈 저, 대림
- NETWORK PROGRAMMING FOR MICROSOFT WINDOWS
- Anthony Jones.Jim Ohlund 공저, 김남식 역, 정보문화사
- 배틀넷 개발을 위한 Network Game Server Programming
- 박명식. 최설호 공저, 영진.com
- 박명식. 최설호 공저, 영진.com
3 주요링크 #
- http://rev.knu.ac.kr/teleadam/program/network.asp
- Design Issue When Using IOCP in a Winsock Server
- Overlapped I/O 이야기
- 마이크로 소프트웨어 - 네트워크 강좌
- Handling multiple pending socket read and write operations
- Writing Windows NT Server Applications in MFC Using I/O Completion Ports
- Design Issues When Using IOCP in a Winsock Server
- Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports
4 Overlapped I/O #
오버랩드 ( overlapped ) I/O 란 문자 그대로 중첩된 입출력을 뜻한다. CPU 에 비해 디스크나 통신 디바이스의 입출력에 걸리는 속도는 대단히 느리기 때문에 오버랩드 I/O 를 사용해 디바이스 입출력시에 걸리는 시간 지연을 피할 수 있다. 물론 윈속은 이미 여러 가지 비동기 입출력 방법을 제공하고 있어, 굳이 오버랩드 I/O 를 사용하지 않더라도 거의 같은 성능의 비동기 입출력을 구현할 수 있다. 잠시 후 소개할 IOCP ( IO Completion Port ) 와 함께 사용되기 때문에 한 번쯤 거쳐야 할 관문 정도로만 생각해 두고 부담없이 진행해 나가도록 하자. send, recv 대신 WSASend, WSARecv 를 사용해 오버랩드 I/O 를 할 수 있다.
- WSASend. WSARecv 함수
int WSASend( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine ); int WSARecv( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
함수 파라미터의 구체적인 사용법은 이후에 논하기로 하고, 지금은 WSAOVERLAPPED 구조체를 사용해 함수를 호출한다는 것 정도만 알아두자. 넌블러킹 소켓에서와 마찬가지로 오버랩드를 사용한 WSASend, WSARecv 호출은 특별한 이상이 없는 한 WSAEWOULDBLOCK 을 리턴한다. 오버랩드 I/O 의 완료 여부를 확인하려면 다음의 함수를 호출하면 된다.
BOOL WSAGetOverlappedResult( SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags );
사실은 바로 IOCP 를 설명해도 되지만, 오버랩드 I/O 를 설명하면서 그냥 지나치면 WSAGetOverlappedResult 가 섭섭해 할까봐 한번 등장시켜 보았다. 바로 뒤에 설명할 IOCP 를 사용해 완료 통보를 받게 되면, 더 이상 이 함수는 설 자리가 없어지기 때문에 독자의 기억 속에 그리 오래 머물 것 같진 않다. 오버랩드 I/O 의 다양한 쓰임새나 윈속의 다른 비동기 입출력 방법에 대해 자세히 알고 싶다면, 마이크로소프트 프레스의 「Network Programming for Microsoft Windows」를 참고하기 바란다.
IOCP 는 디바이스의 입출력 완료를 통보하기 위한 포트로서, 빠른 입출력 통보 외에 최적화된 쓰레드 풀링 기술을 포함하고 있다. 디바이스와 IOCP 를 연결하는 데 개수 제한이 없고, 최적화된 쓰레드 풀링을 통해 고성능 서버를 구축하는데 큰 도움이 되기 때문에, 현재 많은 윈도우 서버 프로그래머들의 사랑을 받고 있는 귀여운 녀석이기도 하다. 제공되는 성능에 비해 사용법 자체는 의외로 간단해 프로그래머는 IOCP 를 만들고, 적절한 수의 워커 쓰레드를 생성한 다음 입출력 완료 통보를 기다리기만 하면 된다.
HANDLE CreateIoCompletionPort ( HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads );
IOCP 를 만들어 주는 좀 웃기는(?) 함수다. 이 함수는 사실상 두 가지 역할을 하는데, 하나는 이름 그대로 IOCP 를 생성하는 것이고 ( 네 번째 파라미터만 사용 ), 다른 하나는 오버랩드 속성을 지닌 소켓과 IOCP 를 연결하는 것이다 ( 앞의 세 파라미터만 사용 ). 「Programming Server-Side Applications for Microsoft Windows 2000」의 저자 제프리 리처 ( Jeffrey Richter ) 도 언급한 것이지만, 함수를 왜 저렇게 만들어 놨는지 도저히 이해되지 않는 부분이다. 어쨌든 꽤 중요한 함수이기에 다음의 일련의 흐름을 보면서 IOCP 체계를 확실히 이해해 둘 필요가 있다.
HANDLE h = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0);
처음엔 당연히 IOCP 를 만들어야 한다. 먼저 IOCP 를 만들 때는 앞의 세 파라미터가 쓰이지 않으므로 가볍게 INVALID_HANDLE_VALUE, 0, 0 을 넘겨주자. NumberOfConcurrentThreads 에 오버랩드 I/O 를 처리하기 위해 동시에 실행할 수 있는 쓰레드의 수를 지정하는데, 0 을 넘기면 시스템은 설치된 프로세서 ( CPU ) 의 수만큼 할당한다.
SYSTEM_INFO si GetSystemInfo( &si ); numThreads = si.dwNumberOfProcessors * 2; for( i = 0; i < numThreads; i++ ) _beginthreadex( NULL, 0, WorkerThread, ... );
IOCP 의 완료 통보를 받을 쓰레드를 생성한다. 좀 이상한 부분이 눈에 띄지 않는가? IOCP 를 만들 때 CPU 수만큼의 쓰레드가 동시에 돌아갈 수 있도록 그 수를 제한해 놓고선, 정작 쓰레드는 그 두 배만큼 만들고 있다. 이는 워크 쓰레드가 Wait 상태에 다다를 때 ( 예 : Sleep 호출 ) IOCP 가 또 다른 쓰레드에 완료 통보를 해주기 때문에 여분의 쓰레드를 미리 만들어 두는 것이다. 두 배라고 한 것은 필자 맘대로 정한 수치이고 서버의 구현 방법이나 서비스 내용에 따라 적절한 값을 찾는 것이 좋다.
CreateIoCompletionPort( (HANDLE) my_socket, iocp_handle, completion_key, 0 )
패킷을 주고받기 위해 오버랩드 구조체를 이용한다. WSASend, WSARecv 각각의 파라미터에 주의를 기울일 필요가 있는데, 이에 관한 자세한 설명은 다음 번에 직접 네트워크 라이브러리를 구현하면서 자세히 설명하기로 하고 지금은 IOCP 체계를 이해하는 것에 초점을 맞추자.
오버랩드 I/O 를 IOCP 로 통보받기 위해 소켓 핸들과 IOCP 핸들을 연결시켜야 한다. 세 번째 파라미터인 completion_key 는 나중에 오버랩드 I/O 에 대한 완료 통보를 받을 때, 어떤 소켓으로부터의 완료 통보인지 식별할 수 있게 해주는 것으로 보통 소켓을 포함하고 있는 객체의 주소를 넘긴다. 그리고 앞서 언급했듯 마지막 파라미터는 쓰지 않는다.
WorkerThread() { while( TRUE ) { GetQueuedCompletionStatus( iocp_handle, // HANDLE CompletionPort &bytes_transferred, // LPDWORD lpNumberOfBytes &completion_key, // PULONG_PTR lpCompletionKey &overlapped, // LPOVERLAPPED *lpOverlapped INFINITE // WORD dwMilliseconds ); // completion_key와 오버랩드를 보면 // 어떤 소켓의 오버랩드 I/O인지 구별할 수 있다. } };
처음 보는 함수가 나타났다. 이미 독자도 예상하고 있겠지만 GetQueuedCompletionStatus 가 IOCP 의 부름 ( Thread Wake-Up ) 을 받기 위해 기다리고 있다. 이 함수를 통해 어떤 소켓의 어떤 호출인지, 또 얼마만큼 전송이 되었고 에러 코드는 무엇인지 등을 확인할 수 있다.
WSASend( s, &wsabuf, 1, &bytes_transferred, 0, &overlapped, NULL ); WSARecv( s, &wsabuf, 1, &bytes_transferred, &flag, &overlapped, NULL );
패킷을 주고받기 위해 오버랩드 구조체를 이용한다. WSASend, WSARecv 각각의 파라미터에 주의를 기울일 필요가 있는데, 이에 관한 자세한 설명은 다음 번에 직접 네트워크 라이브러리를 구현하면서 자세히 설명하기로 하고 지금은 IOCP 체계를 이해하는 것에 초점을 맞추자.
두 개의 CPU 가 설치된 윈도우 2000 에서 <그림 1> 과 같은 IOCP 서버가 실행 중이라고 가정해 보자. IOCP 를 만들 때 NumberOfConcurrentThreads 에 0 을 넘겨 동시 쓰레드 ( concurrent thread ) 의 수가 두 개가 되도록 했다. #1 은 이미 완료 통보를 받아 해당 객체의 송수신을 처리 중이고, #2 가 지금 막 완료 통보를 받고 있다. 이렇게 되면 정확히 두 개의 쓰레드가 동시에 실행중인 것이며, IOCP 큐에 완료 통보가 도착하더라도 IOCP 는 다른 쓰레드 ( #3 ) 에 완료 통보를 하지 않는다. 이 시점에서 발생할 수 있는 두 가지 시나리오를 세워 보았다.
◆ 시나리오 1 - #1 이 완료 통보 처리를 마침
완료 처리가 끝났기 때문에, #1 은 다시 GetQueuedCompletionStatus 함수를 호출한다. 이때 IOCP 는 큐에 쌓여 있던 다른 완료 통보를 다시 #1 에 넘겨준다. 먼저 기다리고 있던 #3 에 넘기지 않는 이유는 쓰레드 컨텍스트 스위칭을 줄이기 위해서다.
완료 처리가 끝났기 때문에, #1 은 다시 GetQueuedCompletionStatus 함수를 호출한다. 이때 IOCP 는 큐에 쌓여 있던 다른 완료 통보를 다시 #1 에 넘겨준다. 먼저 기다리고 있던 #3 에 넘기지 않는 이유는 쓰레드 컨텍스트 스위칭을 줄이기 위해서다.
◆ 시나리오 2 - #1 이 처리 도중 Sleep 을 호출
프로그래머가 무슨 생각으로 Sleep 을 호출했는지는 모르겠지만 어쨌든 쓰레드 Wait 상태에 돌입한다. 이 때 기다리고 있던 #3 이 IOCP 로부터 완료 통보를 받는다. 이 시점의 실제 동시 쓰레드 수는 2+1 ( Wait State ) 이며, #1 이 잠에서 깨어날 경우 순간적으로 IOCP 를 만들 때 지정했던 동시성 수의 범위를 초과할 수 있다. 이후 IOCP 는 다시 동시 쓰레드의 수가 2 가 되도록 조절한다. 이러한 이유로 IOCP 생성시에 지정해 준 NumberOfConcurrentThreads 의 수보다 실제로 많은 워커 쓰레드를 생성하는 것이다.
프로그래머가 무슨 생각으로 Sleep 을 호출했는지는 모르겠지만 어쨌든 쓰레드 Wait 상태에 돌입한다. 이 때 기다리고 있던 #3 이 IOCP 로부터 완료 통보를 받는다. 이 시점의 실제 동시 쓰레드 수는 2+1 ( Wait State ) 이며, #1 이 잠에서 깨어날 경우 순간적으로 IOCP 를 만들 때 지정했던 동시성 수의 범위를 초과할 수 있다. 이후 IOCP 는 다시 동시 쓰레드의 수가 2 가 되도록 조절한다. 이러한 이유로 IOCP 생성시에 지정해 준 NumberOfConcurrentThreads 의 수보다 실제로 많은 워커 쓰레드를 생성하는 것이다.
많은 개발자들이 범하는 대부분의 실수는 멀티 쓰레드와 비동기 입출력의 부족한 이해에 기인한다. 멀티 쓰레드 프로그래밍만 하더라도 어렵고 복잡한데, 여기에 비동기 입출력까지 더해지니 네트워크 개발자들이 겪을 그 혼란은 충분히 짐작할 만하다. 이번엔 IOCP 를 이용해 서버 네트워크 코드를 구현할 때 특히 주의해야 점을 알아보기로 하자.
WSASend, WSARecv 등을 통해 오버랩드 I/O 를 할 때 정상적인 경우 WSAEWOULDBLOCK 을 리턴한다. 그러나 원격 호스트가 접속을 끊거나 ( WSAECONNRESET ), 가상 회선에 문제가 발생했을 때 ( WSAECONNABORTED ) 와 같은 문제는 빈번히 발생한다. 이 경우 별 수 없이 이쪽에서도 접속을 끊는 수밖에 없다. 골치 아픈 부분은 WSAENOBUFS 와 같은 에러를 만나는 경우다. 다음 호에서 구현을 통해 자세히 알아보겠지만, 시스템 리소스(커널 리소스) 제한에 걸리게 되면 오버랩드 I/O 는 'WSAENOBUFS 에러' 를 내뱉으며 실패한다. 마찬가지로 '그냥 접속을 끊으면 되는 것 아니냐?' 라고 반문하겠지만, 그것이 클라이언트가 아니라 대량의 클라이언트가 접속한 상황에서의 서버간 송수신에서 발생하는 것이라면 더욱 심각해진다. 대량의 클라이언트가 접속한 상황에서는 언제든지 시스템 리소스가 바닥날 수 있기 때문에 클라이언트의 연결을 적절히 분산시킬 수 있는 메커니즘이 필요하며, 불가피한 경우 클라이언트의 접속을 제한해야 한다.
오버랩드 호출을 걸어두고, 완료 통보를 받기도 전에 오버랩드 버퍼나 소켓 객체가 삭제되어서는 안된다. 또한 한 객체에 대해 둘 이상의 오버랩드 호출이 있는 경우엔 반드시 참조 카운트를 유지해야 하며, 객체를 제거해야 하는 경우에 이 참조 카운트가 0 인지 확인해야 한다. 참조 카운트를 유지하지 않고 완료 통보가 아직 더 남아있는 상태에서 객체를 삭제하면, 당연한 것이지만 그 다음 완료 통보시 엉뚱한 메모리 위치를 ( IOCP 로 말하자면 CompletionKey 나 OverlappedPointer ) 가리켜 크래시를 발생시킨다. 원인을 모르고 객체가 삭제된 것에 분개해 정적 메모리 관리 등으로 당장 급한 불을 끄는 것은 근본적인 해결책이 될 수 없다.
IOCP 의 워커 쓰레드만을 이용해 서비스 코드를 구현할 때 주의해야 할 사항이 있다. 주로 샘플 소스로 쓰이는 에코 ( echo ) 서버나, 실제로 IOCP 로 구현되어 있는 IIS ( Internet Information Server ) 와 같은 서버는 객체간 상호 참조가 발생되지 않아 이러한 문제는 없다. 그러나 채팅 서버와 같은 상호 참조 ( 즉, 한 객체가 다른 객체에 직접적인 접근이 일어나는 것 ) 가 발생하는 서비스에서는 양방향 상호 참조가 동시에 일어나는 경우에 데드락 ( dead-lock ) 이 발생할 수 있다. 따라서 동기화에 각고의 노력을 기울여야 하며, 이것보다는 패킷을 처리하는 전용 쓰레드를 따로 두어 일괄적으로 처리하는 방법을 권한다.
-
- 열혈강의 TCP/IP 소켓 프로그래밍 - 제 21 장. Overlapped 입.출력 모델 예제
- 열혈강의 TCP/IP 소켓 프로그래밍 - 제 21 장. Overlapped 입.출력 모델 예제
- 열혈강의 TCP/IP 소켓 프로그래밍 - 제 22 장. Completion Port 입.출력 모델 예제
'Programing > Windows' 카테고리의 다른 글
Visual Studio 단축키 (0) | 2009.06.11 |
---|---|
CString <--> char * 형변환 (0) | 2009.06.11 |
Thread를 사용하는 프로젝트 디버깅시 먹통되는 현상 해결법 (0) | 2009.05.07 |
Handle leak 찾기 (0) | 2009.04.29 |
Visual Studio에서 메모리 누수 확인하는 방법 (0) | 2009.04.29 |