IOCP

songtofu·2024년 2월 21일
0

STUDY

목록 보기
4/4

출처 :https://ozt88.tistory.com/23

개념

앞선 통지모델은 싱글스레드, 멀티 플렉싱을 위한 확장같음. 하지만, 요즘 굳이? 하이엔드 스펙을 갖춘 서버에서 싱글스레드를 쓸 필요 없음.
이전, 멀티스레드형 서버의 문제는 컨텍스트 스위칭 비용(https://inpa.tistory.com/entry/%F0%9F%91%A9%E2%80%8D%F0%9F%92%BB-Is-more-threads-always-better)이다. 하지만, CPU 개수만큼의 스레드를 사용한다면, 컨텍스트 스위칭 비용은 문제가 되지 않는다.

딱 CPU개수 만큼만 스레드를 쓰는 서버를 만들자는 희망~

구현

수많은 I/O 요청 속에서 CPU 개수만큼만 스레드를 사용하여 처리하는 것은 어렵다. 하지만 대충
1. I/O 장치와 통지하는 객체를 연결
2. 여러 I/O 장치로 부터 나오는 동시다발적 I/O요청을 효율적으로 관리하기 위해 스레드 풀 만들기
3. I/O가 종료된 이후 발생한 완료 통지를 쓰레드 풀에 의해 통제된 쓰레드에 하나씩 맡긴다.
4. 쓰레드 활용이 제한적이니 완료된 I/O들의 정보를 종료 순서대로 큐에 담는다.
5. 쓰레드가 하는 일이 끝나면 큐에서 완료된 I/O를 새로하나 받아옴.
6. 쓰레드에서 I/O정보에 따라 적절한 처리


만약 후처리하는 쓰레드가 Blocking되어 CPU를 제대로 안쓰고 있는 상황 = 리소스 낭비.

이런 쓰레드는 잠시 냅두고 새로운 쓰레드가 활성화 될 수 있게 하자.

주의할 점) 전체 활성화 된 쓰레드가 반드시 CPU 개수만큼이여야 된다.

쓰레드 풀은 주의할 점을 잘 지켜 쓰레드를 활성화 시킨다.

이렇게 만들어진 구조의 도움을 받아 프로그래머는 디바이스를 통지객체에 연결한 다음 일을 처리할 쓰레드들을 통지객체에 묶어서 대기.

그리고 Overapped I/O 방식을 적용한채 I/O정보를 잘 구조화해 Overapped 구조체나 전달 가능한 인자를 통해 통지객체에 등록. 쓰레드들은 완료시 어떻게 처리할지 명시, I/O가 완료될 때 마다 노는 쓰레드가 알아서 I/O작업을 처리

료된 I/O의 대기열. 완료I/O으의 정보를 받아서 실행할 수 있는 쓰레드들의 대기열. 현재 활성화되어 처리중인 쓰레드들의 리스트. 잠시 작업을 위해 CPU사용을 중단하고 있는 쓰레드들의 리스트. 여기에 IOCP가 처리할 I/O 디바이스들의 리스트를 추가하면 IOCP의 동작에 핵심적인 자료구조들을 다 나열 하였다.

동작 원리

디바이스 리스트

I/O 처리를 위해 우선 I/O 디바이스(소켓, FD)를 IOCP에 등록.
CreateIoCompletionPort 함수를 통해 디바이스와 CompletionPort(CP)를 바인딩.

CreateIoCompletionPort의 기능 두가지
1. 통지모델인 CP를 생성하고 그 핸들을 반환
2. 디바이스와 CP를 바인딩

HANDLE WINAPI CreateIoCompletionPort(
  _In_      HANDLE FileHandle, // I/O 디바이스 핸들을 넘겨서 사용
  _In_opt_  HANDLE ExistingCompletionPort, // CP의 핸들을 넘겨서 사용
  _In_      ULONG_PTR CompletionKey, // 유저가 원하는 데이터
  _In_      DWORD NumberOfConcurrentThreads // 동시에 작동할 쓰레드의 최대 수
);

CPU개수 만큼 스레드를 쓰는 것, 여기에 CPU 개수를 집어넣으면 된다. 물론 MS도 많은 이들이 같은 목표를 공유한다는 사실을 알기 때문에 0만 넣어도 CPU 개수로 맞춰준다.

  • CP를 처음 만들때. CP의 핸들이 반환된다. (이 핸들은 종종 사용된다)
HANDLE hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
  • 디바이스와 연결할 때, Key에 디바이스에
    고유한 정보 넣어주면 편리
HANDLE port = CreateIoCompletionPort(socket, hPort, (ULONG_PTR)session, 0);

I/O Completion Queue

완료된 I/O들의 정보가 저장되는 대기큐, I/O 완료시 말고도 유저가 직접 PostQueuedCompletionStatus 함수PQCS)를 호출하여 정보를 직접 집어 넣을 수도 있음.

BOOL WINAPI PostQueuedCompletionStatus(
  _In_      HANDLE CompletionPort,
  _In_      DWORD dwNumberOfBytesTransferred,
  _In_      ULONG_PTR dwCompletionKey,
  _In_opt_  LPOVERLAPPED lpOverlapped
);

통지할 CP를 인자를 결정한다. I/O의 경우 이미 I/O 디바이스와 바인딩된 CP에 전달 될 것이다. 그리고 전송된 바이트의 크기와 Key를 넘길 수 있다. I/O의 경우 디바이스 바인딩할 때 미리 넣어둔 그 Key가 전달된다. 마지막으로 버퍼가 담긴 Overlapped 구조체를 넘겨줄 수 있다. 기존 Overlapped I/O 함수에서 전달하는 Overlapped 구조체가 이에 해당한다. 위에서 언급한 정보들을 활용하면 완료된 I/O에 대해서 충분히 잘 처리할 수 있다. 이렇게 전달한 정보들은 I/O가 완료되면 Completion Queue 에 저장되어 쓰레드가 나타나 자신을 처리해주기만을 기다리게 된다.

Waiting Thread Queue

쓰레드들이라고 노는건 아니다. 다만 바쁘거나 나설 자리가 없을 뿐 (우리가 활성화할 쓰레드 개수를 제한한 것을 잊지말자). 사용자는 쓰레드를 만들어 대기상태로 만들어야 한다. 이때 사용하는 함수가 GetQueuedCompletionStatus (이하 GQCS) 이다. 이름만 보면 바로 I/O 정보를 뽑아올 것 같지만, 우리가 앞에서 말했던것을 곰곰히 생각해보면 왜 그러면 안되는지를 알 수 있을 것이다. 정보를 받아 올 수 있는 상황이 될때까지, 즉 자신이 올라갈 빈 CPU공간이 있고(허용된 활성화 쓰레드 > 현재 활성화된 쓰레드) && 자신을 필요로 하는 완료된 I/O가 있는 경우에 비로소 이 정보를 받아 올 수 있다. 이런 조건이 맞아 떨어지지 않으면, 설정한 timeout까지 이 쓰레드는 대기상태가 된다. 

Release Thread List

대기상태가 풀리고 쓰레드가 활성화 됬다는 것은 (timeout 제외), 드디어 I/O를 처리할 수 있다는 뜻이다. I/O장치 또는 PQCS로 보낸 정보를 GetQueuedCompletionStatus 함수를 통해서 받아온다.

BOOL WINAPI GetQueuedCompletionStatus(
  _In_   HANDLE CompletionPort,
  _Out_  LPDWORD lpNumberOfBytes,
  _Out_  PULONG_PTR lpCompletionKey,
  _Out_  LPOVERLAPPED *lpOverlapped,
  _In_   DWORD dwMilliseconds
);

생긴것을 보면 딱 Post와 짝이다. 마지막 인자는 미리 언급한 timeout 설정 시간이다. 마음껏 I/O 처리를 하고난 다음 다시 GetQueuedCompletionStatus를 호출하면 다시 대기상태로 들어간다. 이 함수를 다시 호출하지 않으면 다른 쓰레드가 동작할 수 없으니 조심해야한다. 

Paused Thread List

엄밀히 말하자면 Released Thread Queue의 마지막 문장은 잘못되었다. Released List에 있는 쓰레드, 즉 활성화된 쓰레드 중에서 Blocking 상태에 빠진녀석들을 제대로 처리하지 않는다면 비효율적이다. GQCS를 호출하지 않아도 쓰레드가 Blocking상태에 빠지면, IOCP는 똑똑하게 그것을 감지하여 이 Paused Thread List에 집어넣는다. 그러면 가용 쓰레드 공간이 늘어나서 CPU가 필요한 다른 쓰레드에게 리소스를 양도할 수 있는 것이다. 

profile
읽으면 머리에 안들어와서 직접 쓰는 중. 잘못된 부분 지적 대환영

0개의 댓글