Windows에서 IOCP를 사용하는 경우의 설계에 관한 문제

MSDN에서…

개요

이 자료는 Windows NT IOCP(I/O Completion Port)의 입출력 모델에 대해서 이미 이해하고 관련된 API에 대한 자세한 지식이 있는 유저를 대상으로 하고 있다. IOCP에 대해서는 IOCP의 구현과, IOCP를 사용하기 위해서 필요한 API에 대해서 기재된 『 Advanced Windows 개정 제3판 』의 “제15장 디바이스 I/O”(Jeffery Richter)를 참조한다.

IOCP를 사용하면 아주 고성능이고 확장성이 뛰어난 서버 프로그램을 개발할 수 있다. IOCP를 직접 지원하는 기능이 Winsock2에 추가되어, Windows NT 플랫폼에는 이 기능이 완전히 구현되어 있다. 다만 IOCP는 모든 Windows NT 입출력 모델 중에서 가장 복잡하고, 구현이 어렵다. 이 자료는 IOCP를 사용하여 보다 뛰어난 소켓 서버를 설계하는데 도움이 될 몇가지 힌트를 소개한다.

상세

힌트 1: WriteFile 및 ReadFile 등의 Win32파일 입출력 함수의 상위에서 WSASend 및 WSARecv 등의 Winsock2 IOCP 대응 함수를 사용한다.

마이크로 소프트 기반의 프로토콜 프로바이더의 소켓 핸들은 IFS 핸들이다. 그래서 이 핸들을 사용하여 Win32 파일 입출력 호출을 행할 수 있다. 다만, 프로토콜 프로바이더와 파일 시스템의 상호 작용으로 커널 모드와 사용자 모드 사이의 이행, 스레드 컨텍스트 전환 및 파라미터 마셜링이 다수 발생하여 퍼포먼스가 크게 저하한다.
IOCP에는 Winsock2 IOCP 대응 함수만을 사용하자.

이것 말고 ReadFile와 WriteFile에서 파라미터의 마셜링 및 모드의 이행이 발생하는 것은 프로바이더에 의해서 WSAPROTOCOL_INFO 구조체의 dwServiceFlags1에 XP1_IFS_HANDLES 비트가 설정되지 않은 경우 뿐이다.

주: 이들 프로바이더는 WSASend 및 WSARecv를 사용한 경우에도 모드의 이행이 발생하지만, ReadFile 및 WriteFile에서는 보다 높은 빈도로 모드 이행이 발생한다.

힌트 2: 동시 실행을 허용하는 워커 스레드 수와 기동하는 워커 스레드 수를 지정한다.

워커 스레드 수와 IOCP에서 사용되는 동시 실행 스레드의 수는 똑같지 않다. IOCP에서 사용되는 동시 실행 스레드는 최대 2개까지이고, 스레드 풀의 워커 쓰레드 수는 10까지이다. Pool 내의 워커 스레드 수는 IOCP에서 사용되는 동시 실행 스레드의 수 이상이기 때문에 큐에서 취득한 완료 패킷을 처리하는 워커 스레드에서, 큐 내의 다른 입출력 패킷 처리를 늦추지 않고 Win32의 임의의 “wait” 함수를 호출할 수 있다.

큐에서 꺼내 지기만을 기다리고 있는 완료 패킷이 존재할 경우 다른 워커 스레드가 실행된다. 그 뒤 최초의 스레드에서 wait 종료 조건이 충족되면 실행을 재개한다. 이 때 실행할 수 있는 스레드 수는 IOCP에서 허용되는 동시 실행 스레드 수(NumberOfConcurrentThreads 등)보다 더 많다. 다만, 다음 워커 쓰레드가 GetQueueCompletionStatus를 호출하고 대기 상태에 들어갔을 때, 이 스레드는 기동 되지 않는다. 즉, 시스템에서는 동시 실행 워커 스레드가 사용자 지정한 수에 유지한다.

보통은 IOCP용 CPU 하나에 대해서 동시 실행 워커 쓰레드가 1개 있으면 충분한다. 이 설정을 위해서는 IOCP를 처음 작성할 때 CreateIoCompletionPort 호출의 NumberOfConcurrentThreads에 0을 입력한다.

힌트 3: 큐에서 취득한 완료 패킷과 포스트된 입출력 조작을 연결 짓는다.

완료 패킷을 큐에서 꺼낼 때에 GetQueuedCompletionStatus에 의한 입출력의 완료 키와 OVERLAPPED 구조체가 반환된다. 이들 2개의 구조체를 사용하여 핸들 별 입출력 조작마다 각각 정보를 돌려줄 필요가 있다. 핸들 별 정보를 제공하기 때문에 IOCP에 소켓을 등록할 때에는 소켓 핸들을 완료 키로 사용할 수 있다. 입출력 별 조작을 제공하려면 애플리케이션 고유의 입출력 상태 정보를 포함하도록 OVERLAPPED 구조체를 “확장” 한다. 또 중복된 입출력마다 유일의 OVERLAPPED 구조체를 사용하도록 한다. 입출력이 완료되면 중복된 입출력 구조체로의 포인터가 반환된다.

힌트 4: 입출력 완료 패킷의 큐로의 입력 처리

입출력 완료 패킷이 IOCP에서 큐에 삽입되는 순서는 Winsock2 입출력 호출 순서와 반드시 일치하지 않는다. 게다가 Winsock2 입출력 호출에서 SUCCESS 또는 IO_PENDING을 돌려주는 경우 소켓 핸들의 개폐 상태에 관계 없이 완료 패킷은 입출력이 완료했을 때 확실히 IOCP의 큐에 삽입된다. 소켓 핸들을 닫으면, 이후 WSASend, WSASendTo, WSARecv 또는 WSARecvFrom의 호출은 SUCCESS 및 IO_PENDING 이외의 리턴 코드를 반환하면서 실패하고 완료 패킷은 생성되지 않는다. 이 경우 사전에 포스트된 입출력에 대해서 GetQueuedCompletionStatus를 실행하면 반환되는 완료 패킷의 상태는 실패로 되어 있는 것이 있다.

IOCP 자체를 삭제한 경우 IOCP 핸들 자체가 무용지물이 되기 때문에 IOCP에 입출력을 포스트 할 수 없다. 다만, 시스템을 구성하는 IOCP 커널의 구조는 정상적으로 포스트된 입출력이 모두 완료될 때까지 잃지 않는다.

힌트 5: IOCP의 클린업

IOCP의 클린업을 실행할 때 가장 중요한 것은 중복된 입출력을 사용할 때와 마찬가지로 입출력이 완료되기까지는 OVERLAPPED 구조체를 해방하지 않는 것이다. OVERLAPPED 구조체의 입출력이 완료되었는지는 HasOverlappedIoCompleted 매크로를 사용함으로써 검출할 수 있다.

일반적으로 서버를 종료하는 경우에는 2가지 경우가 있다. 1번째의 경우는 처리되지 않은 입출력 완료 상태에 관계 없이, 최대한 빨리 정지하는 경우이다. 2번째의 경우 서버를 종료할 때 미처리의 각 입출력 완료 상태를 확인할 필요가 있는 경우이다.

처음의 경우 PostQueueCompletionStatus를 N회(N은 워커 쓰레드 수)호출함으로써 워커 쓰레드를 곧 종료하도록 통보하는 특별한 완료 패킷을 포스트하고 모든 소켓 핸들과 그것과 관련된 OVERLAPPED 구조체를 닫은 후 완료 포트를 닫는다. 여기에서도 HasOverlappedIoCompleted를 사용하여 OVERLAPPED 구조체를 해방하기 전에 구조체의 완료 상태를 확인하도록 한다. 소켓을 닫으면 이 소켓에서의 미 처리 입출력은 모두 신속하게 완료한다.

2번째의 경우 모든 완료 패킷이 정상적으로 큐에서 꺼낼 수 있도록 기존의 워커 쓰레드를 지연시킨다. 우선 모든 소켓 핸들과 IOCP를 닫는다. 다만 미처리 입출력의 건수 카운터를 보유하고, 워커 스레드가 안전하게 쓰레드를 마칠 수 있는 타이밍을 인식하게 할 필요가 있다. 큐 내에 완료 패킷이 남아 있는 한 적극적인 워커 쓰레드가 없어질 수 없다. 이 때문에 IOCP 서버의 임계 구역에서 글로벌 입출력 카운터를 보호하더라도 퍼포먼스는 예상할 정도로 저하하지 않는다.

출처: https://support.microsoft.com/ko-kr/help/192800/info-design-issues-when-using-iocp-in-a-winsock-server


이 글은 2017-07-10에 작성되었습니다.