IOCP 공부노트: (2) IOCompletionPort

게임 서버 프로그래밍/네트워크 개념정리

2020. 7. 13. 16:00

* 위의 내용은 Windows VIA C/C++ 책에서 대부분 발췌했습니다.

 

1. 등장배경

 윈도우에서 제공하는  Service Application Architecture은 다음 두 가지 형태로 존재한다.

 

(1) 시리얼 모델

 - 하나의 스레드가 사용자의 요청을 대기한다. 사용자 요청이 들어오면 대기하던 스레드가 깨어나

클라이언트의 요청을 처리한다.

(2) 컨커런트 모델

 - 하나의 스레드는 사용자의 요청을 대기하고 있고, 사용자의 요청을 처리하기 위해 새로운 스레드를

생성한다.

 

*시리얼 모델의 문제점*

 - 다수의 동시 사용자 요청을 효과적으로 처리하지 못한다

 - 멀티프로세서를 가진 머신의 성능을 활용할 수 없다

 

 위와 같은 문제점으로 인해 컨커런트 모델이 일반적으로 더 많이 사용된다.

 

*컨커런트 모델의 특징*

 - 각각의 클라이언트 요청을 처리하기 위해 새로운 스레드가 생성된다.

 - 각 클라이언트의 요청을 처리하는 독립된 스레드가 확보되기 때문에 서버 애플리케이션을 확장하기도

   용이하고 멀티프로세서 머신의 장점도 십분 활용할 수 있다.

 

 하지만, 컨커런트 모델을 사용하는 어플리케이션이 기대에 못미치는 성능을 보였고 특히, 많은 수의 클라

이언트 요청이 동시에 들어왔을 때 시스템이 수많은 스레드들을 동시에 수행해야 함에 주목했다.

 

 또한, 대부분의 스레드들이 스케줄 가능 상태였기 때문에 이러한 스레드들간의 컨텍스트 전환을 수행하느라

윈도우 커널이 너무 많은 시간을 허비하고 있으며, 각각의 스레드들은 작업을 수행할 만큼의 충분한 CPU

시간을 받지 못하는 것도 알게 되었다.

 

 이러한 문제점을 해결하기위해 MicroSoft팀은 I/O Completion Port를 세상에 내놓았다.

 

2. I/O Completion Port의 생성

 I/O Completion Port를 구현한 이론적 배경은 동시에 수행할 수 있는 스레드 개수의 상한을 설정할 수 있어야

한다는 것이다. 즉, 500명의 사용자가 동시에 서비스 요청을 하더라도 500개의 스레드를 생성할 수 없도록 제한

하는 것이다.

 

 *컨커런트 모델의 약점*

  (1) 수행 가능한 스레드의 개수를 CPU 개수보다 많이 유지하게 되면, 시스템은 스레드 컨텍스트 전환을 위해 CPU

  시간을 필요로 하게 되고, 이는 결국 중요한 CPU 자원을 낭비하게 된다.

  (2) 클라이언트의 요청이 있을 때마다 새로운 스레드를 생성해야 한다.

 

 *개선방안*

   스레드 풀을 생성하여 종료 시까지 유지할수 있다면 애플리케이션의 성능을 상당부분 개선할 수 있다.

 

 I/O Completion Port를 생성하기 위해서 CreateIoCompletionPort 함수를 사용한다.

HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle,
  _In_opt_ HANDLE    ExistingCompletionPort,
  _In_     ULONG_PTR CompletionKey,
  _In_     DWORD     NumberOfConcurrentThreads
);

 

 FileHandle ->                         장치에 대한 핸들 (파일,소켓,메일..등의 핸들)
 
 ExistingCompletionPort ->         I/O Completion Port로 생성된 Handle 
 
 CompletionKey->                    사용자가 임의로 전달할 수 있는 값. 함수는 오로지 값의 전달만 한다.
                                            (구조체 형식으로 통신에 필요한 데이터를 넣을 수 있다.)
 NumberOfConcurrentThread ->  I/O Completion Port에게 동일 시간에 동시에 수행할 수 있는 스레드의
                                            최대 개수를 알려주는 역할을 한다.
                                            (0을 전달하면 머신에 설치된 CPU의 개수를 동시에 수행 가능한 스레드의
                                             최대 개수로 설정한다.)

 

 

 CreateIoCompletionPort()의 두 가지 기능

(1) I/O CompletionPort의 생성

(2) Device와 I/O CompletionPort의 연계

 

이를 통해서 같은 함수를 두 번호출 해야함을 알 수 있다. 이는 명백한 마이크로소프트의 실수이며 그들도 이 함수는 두 가지로 분리가 됐어야 함을 인정했다. Windows VIA C/C++의 저자 제프리 리처는 이 함수를 두가지로 분해했다.

 

// I/O Completion Port 생성
HANDLE CreateNewCompletionPort(DWORD dwNumberOfConcurrentThreads) {

	return CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
		dwNumberOfConcurrentThreads);
}

//사용
//...
HANDLE hIOCP = CreateNewCompletionPort(0);
// 장치와의 연계
BOOL AssociateDeviceWithCompletionPort(HANDLE hCompletionPort, HANDLE hDevice,
	DWORD dwCompletionKey)
{
	HANDLE h = CreateIoCompletionPort(hDevice, hCompletionPort, dwCompletionKey, 0);
	return (h == hCompletionPort);
}

// 사용
//..
// hIOCP는 이미 생성됨. clntSocket은 accept로 받은 소켓. IOCPstruct는 사용자 임의 구조체
AssociateDeviceWithCompletionPort(hIOCP,clntSocket,IOCPstruct);

 

3. I/O Completion Port의 구성

 I/O CompletionPort를 생성하면 윈도우 커널은 내부적으로 5개의 서로 다른 데이터 구조를 생성한다.

 

 (1) 장치 리스트(Device List)

   장치 리스트는 I/O Completion Port와 연계된 단일 혹은 다수의 장치를 관리하기 위한 리스트다.

새로운 장치를 I/O Completion Port와 연계시킬 때마다 시스템은 I/O 컴플리션 포트의 내부적인 데이터

구조인 장치 리스트에 새로운 항목을 추가한다.

 

*특징*

 항목 추가 시점 : CreateIoCompletionPort가 호출되었을 때

 항목 제거 시점 : 장치 핸들이 닫혔을 때

 

(2) I/O Completion Queue (FIFO 형식)

 

 장치에 대한 비동기 I/O 요청이 완료되면 시스템은 장치와 연계된 I/O Completion Port가 있는지 확인한다.

만일 연계된 I/O Completion Port가 있으면 I/O 컴플리션 큐에 I/O 요청의 완료 통지를 나타내는 새로운 항목을

삽입한다.

 

*특징*

  I/O 요청이 완료됐을 때, PostQueuedCompletionStatus가 호출되었을 때 항목이 추가된다.

  I/O Completion Port가 대기 스레드 큐의 항목을 가져올 때 항목이 제거된다.

 

 (3) Waiting Thread Queue (LIFO)

 

 스레드 풀 내의 여러 개의 스레드들이 각기 GetQueuedCompletionStatus 함수를 호출하면 이 함수를 호출한 스레드의 ID 값이 대기 스레드 큐에 삽입되며, 이를 통해 I/O Completion Port 커널 오브젝트는 어떤 스레드들이 비동기 I/O 요청에 대한 완료통지를 처리할 것인지를 알 수 있다.

 

 I/O Completion Queue에 항목이 추가되면 I/O Completion Port는 대기 스레드 큐에 있는 스레드 중 하나를 깨우게 되고, 이 스레드는 Completion Queue에 삽입된 항목으로부터 송수신된 바이트 수, 컴플리션 키, OVERLAPPED 구조체의 주소를 가져오게 된다.

 

*특징*

 (1) 스레드가 GetQueuedCompletionPort를 호출할 때 항목이 추가된다.

 (2) I/O Completion Queue가 비어 있지 않고 수행 중인 스레드의 개수가 동시 수행 가능 스레드 수를 초과하지 않을 경우 항목이 제거된다. (Released Thread List로 이동됨을 의미한다.)

 (3) LIFO 알고리즘을 이용해 스케줄되지 않는 스레드들이 사용하는 메모리를 디스크로 내보낼 수 있으며(Swap out), 프로세서의 캐시를 비울수도 있다. 반대로 말하면 자주 사용되는 메모리는 Swap Out이 발생하지 않아서 퍼포먼스적으로 LIFO 방식이 우수하다는 것을 의미한다.

 

(4) Released Thread List

 

 I/O Completion Port가 특정 스레드의 수행을 재개시키는 경우 깨어난 스레드의 ID를 기록해 둔다.

 I/O Completion Port는 항상 자신을 생성할 때 지정한 동시 수행 가능 스레드 개수만큼 릴리즈 스레드 리스트의 항목 수를 유지하려 한다.

 

*특징*

 I/O Completion Port가 Waiting Thread Queue에 있는 스레드를 깨우는 경우 항목이 추가된다.

 일시 정지되었던 스레드가 다시 꺠어났을 경우 항목이 추가된다.

 스레드가 다시 GetQueuedCompletionStatus를 호출하였을 때 항목이 제거 된다. (Waiting Queue로 돌아감)

 스레드가 정지되는 함수를 호출하였을 때 항목이 제거 된다. (Paused Thread List로 이동됨을 의미)

 

(5) Paused Thread List

 

  수행 중이던 스레드가 스레드를 정지시키는 함수를 호출하였을 때 항목이 추가된다.

  일시 정지되었던 스레드가 깨어났을 경우 항목이 제거된다. ( Released Thread List로 이동됨을 의미)

 

 

4. I/O Completion Port를 이용한 아키텍처 설계

(1) 풀 내에 몇 개의 스레드를 생성해 두는 것이 좋은가?

 

-> 서비스 애플리케이션을 운영할 머신의 CPU 개수에 2를 곱한 수준에서 스레드를 생성하는것이 가장 일반적이다.

너무 많은 스레드를 생성하면 시스템 자원이 낭비될 수 있다.

 

(2) GetQueuedCompletionStatus

 

-> I/O Completion Port를 통해 완료 통지가 전달될 때 이를 곧바로 처리할 수 있도록 스레드를 대기상태로 유지시킨다.

BOOL GetQueuedCompletionStatus(
  HANDLE       CompletionPort,
  LPDWORD      lpNumberOfBytesTransferred,
  PULONG_PTR   lpCompletionKey,
  LPOVERLAPPED *lpOverlapped,
  DWORD        dwMilliseconds
);

이 함수를 호출한 스레드를 I/O Completion Port 내의 Completion Queue에 새로운 항목이 삽입될 때까지 대기 상태로 유지하며, 적절한 타임아웃 값을 지정할 수도 있다.

5. 정리

IOCP를 이용하기 위한 핵심함수인 CreateIoCompletionPort에 대해 알아보았다. 컨커런트 모델 설정의 이유와 단점을 해결하기 위해서 스레드 풀 방식을 이용한 I/O Completion Port의 내부 구조와 동작 방식을 설명했다. 이외에도 IOCP에 대한 내용은 굉장히 방대하게 존재한다. 나의 역량으로는 단순히 개념을 정리하는 정도라서 도움이 될만한 링크들을 남기며 글을 마무리하겠다.

 

*GitHub - jacking75님의 글에 링크들을 참고하면 도움이 될 것이다.*

 

https://github.com/sanghun219/edu_cpp_IOCP

 

'게임 서버 프로그래밍 > 네트워크 개념정리' 카테고리의 다른 글

IOCP 공부노트: (3) IOCP Echo Server  (0) 2020.07.15
IOCP 공부 노트: (1) _beginthreadex , CreateThread  (0) 2020.07.10
객체 리플리케이션  (0) 2020.03.02
압축  (0) 2020.02.24
직렬화  (0) 2020.02.24