bdfgdfg

[온라인 서버] I/O Completion Port(IOCP) 모델 이론 본문

게임프로그래밍/서버 책

[온라인 서버] I/O Completion Port(IOCP) 모델 이론

marmelo12 2021. 12. 29. 19:10
반응형

I/O Completion Port 모델

- 윈도우즈에서 제공하는 I/O모델 중 최고의 성능

- Completion Port객체는 Overlapped I/O에서 쓰레드 풀링과 Queue라는 메커니즘을 동시에 접목.

 

쓰레드 풀링

 - 풀(Pool)이란 집합소 또는 수영장에서 물을 모아놓은곳등의 의미. 즉 어떤 것을 모아 놓은 곳을 말한다.

 - 쓰레드 풀(Pool)은 실제로 프로그램에서는 시작할 때 여러개의 쓰레드를 대기상태로 미리 생성해놓은 것을 쓰레드 풀     이라 한다.

 - 그리고 이 쓰레드 풀에 대기중인 쓰레드들 중에 현재 필요한 만큼 꺼내어 실행 상태로 바꾸어서 사용하고

   다 사용한 스레드는 파괴하지 않고 다시 대기상태로 바꿔서 쓰레드 풀에 넣어주는 일련의 과정을 쓰레드 풀링

 

쓰레드 풀링을 사용하는 이유

1. 쓰레드 생성과 파괴에 따른 CPU소모를 줄이기 위함.

 -> 쓰레드를 미리 생성해놓고 상태만 바꿔서 사용함으로써 쓰레드의 잦은 생성과 파괴를 하지 않아 할당된 CPU의 시간(타임 슬라이스)을 낭비하지 않고 다른일을 할 수 있기 때문.

 -> 이 말은 곧 게임서버의 성능을 더 향상 시킬 수 있다는 의미

2. 쓰레드에서 다른 쓰레드로 작업 전환을 할때에 컨텍스트 스위칭(Context-switching)이 일어나는데, 이 Context-           switching또한 CPU소모가 많으므로 자주 일어나면 성능이 떨어진다.

 -> 쓰레드풀링을 사용하면 실행 상태의 쓰레드를 상황에 맞춰 조절하므로 잦은 컨텍스트 스위칭을 피한다.

 

위의 과정은 I/O CompletionPort모델의 입출력 핸들의 등록비동기 I/O요청, I/O의 완료통지 방법에대한 그림이다.

 

1. 유저모드. 즉 우리의 프로그램에서 코딩하는 영역에서는 단순히 생성한 CP객체에 입출력 핸들 + 완료 키 등록.

2. 그리고 비동기 I/O(Overlapped I/O)를 요청

 -> 서버에서 클라이언트와 연결된 소켓이 생성되었으면 무조건 WSARecv함수를 호출.

 -> 클라이언트가 언제 패킷을 보낼지모르며 미리 WSARecv를 걸어둠으로서 수신이 완료되었을 때 처리.(이때 다시 IO       요청)

3. 결과 확인을 위해 WSAGetOverlappedResult함수 호출이 아닌 GeqQueuedCompletionStatus(통칭 GQCS)함수를 호출.

 -> 커널 내부 처리과정은 전자와 많은 차이를 보인다. (자료구조 및 메커니즘)

 

위 과정을 좀 더 풀어서 보자.

1. CreateIoCompletionPort(CP객체를 생성할때도 이 함수 호출)함수를 통해 입출력 핸들 + 완료키를 CP객체에 등록

 -> 커널 내부에서 등록한 입출력 핸들과 완료 키(Completion Key)를 관리해주는 Device List 자료구조에 추가된다.

   --> Device List는 우리가 등록한 입출력 핸들을 관리한다.

 

2. IOCP Queue(IO 완료큐)라는 자료구조의 추가

 -> 이전 Overlapped IO모델은 요청한 작업이 완료되면 사용자에게 이벤트(Event)나 콜백(CallBack)방식으로 알림.

 -> IOCP모델은 커널에게 우리가 요청한 Overlapped I/O작업이 완료 되었을 때 사용자에게 바로 알리는게 아닌

     완료된 작업 결과를 IOCP Queue에 넣은 후에 CP객체를 이용하여 사용자에게 알린다.

   --> 커널은 쓰레드 풀링 메커니즘을 이용하여 IOCP Queue에서 완료된 Overlapped I/O작업을 가져와 뒤 처리.

 

IOCP모델에서 쓰레드 풀링 메커니즘이 어떻게 동작하는지 보자.

 

IOCP Queue에 완료된 작업결과가 들어오면 WaitingThread Queue(LIFO)에서 대기하고 있던 쓰레드들 중

가장 나중에 추가된 몇개를 실행 상태로 만든다음 WaitingThread Queue에서 삭제한 후 ReleaseThread List에 추가한다.

그리고 실행상태로 바뀐 쓰레드들은 IOCP Queue에서 완료된 작업 결과를 가져와 유저 모드의 GQCS함수로 반환하고

완료된 작업의 뒤 처리를 계속해서 진행한다.

 

실행상태가 된 쓰레드들은 완료된 작업의 뒤처리를 하는데 만약 뒤처리 도중 대기작업을 하는 함수를 호출했다면

해당 쓰레드는 대기 상태로 바뀌고 PausedThread List에 추가된다.

또한 뒤처리 도중 대기상태로 들어간 쓰레드가 후에 다시 실행상태가 되면 그 쓰레드는 PausedThread List에서 삭제된

후 ReleaseThread List에 다시 추가된다. 그리고 전에 하던 작업을 뒤이어 계속하게 된다.

그리고 모든 뒤처리 과정을 마친후 다시 GQCS함수를 호출한다. IOCP Queue에 완료된 작업이 있다면 위의 과정을 반복.

없다면 쓰레드는 대기 상태로 바뀌고 ReleaseThread List에서 삭제된 후 WaitingThread Queue로 들어가게 된다.

 

즉 간단하게 말하면 GQCS함수에 진입한 쓰레드들은 IOCP Queue에 완료된 작업이 있을때까지 대기하다가,

작업이 완료된게 있으면 빠져나와(Release상태) 일을 처리하고 대기 함수에 진입하면(Pause)상태에 빠지고 빠져나오면

이어서 처리. 일을 다 처리하면 다시 다음 일감을 처리하기위해 대기한다.

 

여기서 중요하게 볼 세가지가 있다.

1. WaitingThread Queue의 쓰레드 입출력 순서가 FIFO가 아니고 LIFO로 동작한다.

 -> 컨텍스트 스위칭을 피하기 위함. 즉 방금 대기한 쓰레드가 일감이 들어오면 바로 일처리. CPU최대활용.

 -> 컨텍스트 스위칭은 서로 다른 쓰레드간의 전환이 일어나는 것.

     여기서 대기 상태였던 쓰레드를 다시 실행상태로 바꾼것이고 서로 다른 쓰레드간의 전환은 일어나지 않았다.

 -> 만약 마지막에 들어온 쓰레드가 아닌 다른 쓰레드가 실행상태로 바뀐다면 컨텍스트 스위칭이 일어난단 의미.

      그렇기에 LIFO구조를 사용하는 것.

     즉 A(실행)->A(대기)->A(실행)의 과정은 컨텍스위칭x. A(실행)->A(대기)->B(실행)의 과정은 컨텍스위칭O.

 

2. 동시에 실행상태에 있는 쓰레드의 개수.

 - 사용자는 커널이 실행 상태에 있는 쓰레드의 개수를 조절하고 있다는것을 알아야한다.

 만약 쓰레드 풀에 총 5개의 쓰레드가 미리 생성되었고 이중 2개의 쓰레드만 동시에 실행상태로 될수있다고 가정해보자.

여기서 동시에 실행 상태에 있는 2개의 쓰레드들 중 하나가 대기 작업을 하는 함수를 호출하여 대기 상태로 들어간다면

현재 실행상태(Release List)에 있는 쓰레드는 1개밖에 없다.

 -> 그렇다면 커널은 WaitingThread Queue에서 한개의 쓰레드를 실행상태로 바꾼다.

     이어서 WaitingThread Queue에서 삭제한 후 ReleaseThread List에 추가할것임.

그러면 다시 동시에 실행가능한 쓰레드의 수는(실행상태의 쓰레드수) 현재 다시 2개가 된 것.

 

그런데 이번엔 실행도중 대기작업 함수 호출로 인해 대기 상태에 들어간 쓰레드가 대기작업을 마치고 다시 실행상태가

된다면?

다시 ReleaseThread List에 들어가게 될 것이고 현재 실행상태에 있는 쓰레드의 개수가 3개가 되버린다.

이렇게 되면 동시에 실행 상태가 될 수 있는 쓰레드의 최대 허용 개수가 2개를 초과하게 되지만,

커널은 실행 상태에 있는 쓰레드들 중 먼저 뒤처리 작업을 마치고 GQCS함수를 호출한 쓰레드를 다시 대기 상태로만들어 동시에 실행 상태가 되는 쓰레드의 개수가 2개가 될 수 있도록 조정한다.

 

이렇듯 커널은 동시에 실행 상태가 되는 쓰레드의 개수가 부족해지면 쓰레드 풀에서 대기중인 쓰레드들을 꺼내서

실행상태로 만들고, 초과하는 경우에는 대기 상태로 만들어 다시 쓰레드 풀에 넣어주는 역할을 한다.

 -> 즉 동시에 실행 상태에 있는 쓰레드 개수를 사용자가 설정한 일정 개수로 유지될 수 있도록 조절하고 있다는 것을 기억해야 한다.

 

3. WatingThread Queue에 대기중인 쓰레드가 충분히 있어야 한다는 것이다.

 - 실행상태에 있는 쓰레드가 완료된 작업의 뒤처리를 하고있는 도중 대기 작업 함수를 호출했다면

   그 쓰레드는 ReleaseThread List에서 삭제된 후 대기 작업이 끝날 때까지 PausedThread List에서 대기 상태로 기다리고 WaitThreadQueue에서 대기 중인 다른 쓰레드가 실행 상태로 바뀐다고 말했다.

   근데 만약 WaitThreadQueue에서 대기중인 쓰레드가 없다면 사용자가 설정한 동시에 실행중인 쓰레드 개수가

   유지되지 못하며(동시에 실행가능한 쓰레드 수) 최악의 경우에는 실행중인 쓰레드가 모두 대기작업 함수를 호출한다면 실행 상태에 있는 쓰레드가 없을 수 있다.

 

이런 경우를 피하려면 우선은 충분한 개수의 쓰레드를 WaitThread Queue에 대기 시켜놔야 한다.

일반적으로 권장하는 쓰레드의 개수는 2n + 1개인데 여기서 n은 CPU의 개수(CPU의 코어). 만약 2개의 CPU가 있는

머신이라면 총 5개의 쓰레드를 미리 만들어 놓는것이 적당하다. 하지만 이 쓰레드의 개수는 작업의 종류에 따라 달라질

수 있으며 스트레스 테스트를 통해 적절히 조정하는 것이 필요하다. 

 -> 또한 완료된 작업의 뒤처리를 하는 도중 대기시간이 긴 작업이나 대기시간을 알 수 없는 작업은 피하는게 좋다.

 -> 왜냐하면 그런작업이 많아지면 쓰레드 풀이 금방 바닥나서 더이상 작업을 처리할 수 없게 될지도 모른다.

 

 

반응형
Comments