Programming - cpueblo.com

[강좌] IOCP 사용 방법


글쓴이 : 유광희 날짜 : 2002-12-17 (화) 19:39 조회 : 15449
IOCP 사용 방법

IOCP 사용 방법

IOCP의 목적

IOCP는 프로그램이 어떤 핸들( 파일, 소켓 등 )에 대해 I/O 작업을 할 때, 블록 되지 않게 함으로써 프로그램의 대기 시간을 줄일 수 있는 방법이다.

블록이 되면 생기는 문제 제시하면서, 타당성 제공.

IOCP의 사용하기 위한 절차.

마치 IOCP에게 작업을 해줘라고 신청하고 신청한 작업이 끝나면 이런 작업을 누군가 하라고 했는데 그 작업이 지금 완료 되었다고 알려주는 것과 같은 절차를 가진다.

1. IOCP 핸들을 생성한다.

2. 핸들과 키의 쌍으로 IOCP핸들에 등록을 한다.

3. 핸들에 I/O 작업을 한다.( I/O작업을 신청한다는 의미가 더 어울릴 것 같다. )

4. 어떤 핸들에 대한 I/O작업이 완료되면 IOCP가 프로그램에게 알려준다.

5. 완료된 작업에 대해 해야 할 일을 한다.

1. IOCP 핸들을 생성한다.

I/O작업을 대신 해줄 IOCP 핸들을 생성해야 한다.

다음은 IOCP 핸들을 생성하기 위한 함수의 원형이다. 뒤에 보겠지만, 핸들과 키의 쌍을 IOCP 핸들에 등록하는 것도 이 함수를 사용한다.

HANDLE CreateIoCompletionPort (
HANDLE
FileHandle, // handle to file
HANDLE ExistingCompletionPort, // handle to I/O completion port
ULONG_PTR CompletionKey, // completion key
DWORD NumberOfConcurrentThreads // number of threads to execute concurrently);

인자를 보면, hFile은 IOCP에 등록하는 I/O 핸들( 소켓, 파일 등 )이다. 이렇게 함으로써 IOCP는 hFile에서 일어나는 I/O를 감시하면서 작업이 끝났을 때, 알려줄 수 있게 되는 것 이다. 다음의 hExistingPort는 새로운 포트를 생성하려면 NULL, 기존에 있는 IOCP에 연결을 하려면 CreateIoCompletionPort()함수가 이전에 반환했던 IOCP 핸들을 넘겨준다. 세 번째 CompletionKey는 위에서 말했던 키를 나타낸다. 나중에 핸들에서의 작업이 끝나면 IOCP는 어떤 핸들에서의 작업인지 알려줄 때 핸들 값 자체를 넘겨주는 것이 아니라, 이 키 값을 알려준다. 따라서, 여러 개의 핸들을 등록했다면, 등록할 때 이 키 값을 고유하게 해주므로써 각각을 구분할 수 있도록 한다. 마지막 NumberOfConcurrentThreads는 IOCP가 입출력 작업을 할 때 얼마나 많은 쓰레드를 사용하여 작업을 할 지 설정하는 것으로 특별히 정확하게 얼마나 설정해야 할 지 모를 때는 0의 값으로 설정하면 가장 최적화된 방법으로 스레드를 생성하여 사용한다. 생성이 성공하면 IOCP핸들을 반환하고 실패 하면 NULL값을 반환한다.

Tip dwCompletionKey값은 여러 가지로 사용할 수 있다. 타입이 DWORD이므로 어떤 형식의 포인터로 사용하는 것도 가능하고 정수로써도 사용할 수 있다.

HANDLE hIOCP;

hIOCP = ::CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, NULL, NULL );

그저 생성하는 것이므로 다음과 같이 생성을 한다.

2. 핸들과 키의 쌍으로 IOCP핸들에 등록을 한다.

위에서 설명한 CreateIoCompletionPort함수를 사용하여 핸들과 키를 등록할 수 있다.

HANDLE hFileHandle; //IOCP에 등록하고 싶은 핸들.

m_hIOCP = ::CreateIoCompletionPort(

hFileHandle,

hIOCP,

dwCompkey,

NumberOfCurrentThread );

이와 같이 hFileHandle-dwCompkey 쌍으로 등록하면 나중에 I/O출력함수를 hFileHandle을 인자로 호출을 하면 I/O가 끝나면 IOCP가 dwCompkey값을 주면서 I/O작업이 끝났다고 알려준다. 그러므로 dwCompkey값을 어떤 값으로 사용하냐에 따라 프로그램을 작성하는 방법이 많이 달라질 수 있다.

IOCP의 기법은 어떤 핸들( 파일, 소켓 등등 )과 특정 값을 일치 시켜서 나중에 작업을 알수 있도록 하는 것이다.

사용자는 어떤 핸들에 대한 작업이 완료 되었을 때, 어떤 값을 알려달라고 시스템에 알려준다.

그리고, 핸들에 I/O작업을 실시한다. 다른 작업을 하는 도중 핸들에 I/O작업이 완료 되었으면 시스템은 프로그램에게 처음에 등록했던 값을 알려준다.

3. 핸들에 I/O 작업을 한다.

윈도우에서는 소켓이나 파일 핸들이나 같이 사용되기 때문에 아무 I/O함수나 사용하여 I/O작업을 신청한다.

단 여기서 중요한 것은 IOCP는 중첩된(Overlapped) I/O 방식을 사용한다는 것이다. 따라서, WSARecv()/WSASend(), ReadFile()/WriteFile()을 사용할 것을 권장한다.

OVERLAPPED *pPacket = new OVERLAPPED;

if( FALSE == ReadFile( (HANDLE)hFileHandle,

szBuffer,

MAX_BUFFER,

NULL,

pPacket ) )

{

if( GetLastError() != ERROR_IO_PENDING )

{

printf("ReadFile() failed with error %d\\n", GetLastError());

return 0;

}

}

위와 같이 프로그램을 작성하여 입출력을 등록한다. 보통 우리가 알고 있는 것과 차이가 있다면 OVERLAPPED 구조체를 생성하여 인자로 넘겨준다는 것이다. 이 구조체에 대한 자세한 정보는 전문서적을 참조하기 바란다. 하지만 이 구조체의 가장 큰 역할은 나중에 IOCP가 작업이 완료되었다고 알려줄 때, 읽기 작업을 끝냈는지 쓰기 작업을 끝냈는지 또는 10개의 작업을 하라고 했는데 도대체 어떤 작업을 끝냈는지 알려 줄 방법이 없으므로 이 구조체를 사용하여 작업을 신청하기 전에 나중에 끝났을 때 알아볼 수 있도록 흔적을 남기기 위한 것이다. ( 원래는 더 많은 의미를 담고 있지만, 잘 사용 안 하니까 ) 따라서, OVERLAPPED 구조체를 상속하여 흔적에 사용할 정보를 담을 수 있도록 만들어서 사용한다.

4. 어떤 핸들에 대한 I/O작업이 완료되면 IOCP가 프로그램에게 알려준다.

IOCP가 작업의 완료를 알려주는 것을 받는 방법은 여러 가지가 있으나 여기서는 GetQueuedCompletionStatus() 함수를 사용하는 법을 알아보도록 한다.

BOOL GetQueuedCompletionStatus(

HANDLE CompletionPort, // handle to completion port

LPDWORD lpNumberOfBytes, // bytes transferred

PULONG_PTR lpCompletionKey, // file completion key

LPOVERLAPPED *lpOverlapped, // buffer

DWORD dwMilliseconds // optional timeout value

);

첫 번째 CompletionPort는 이전에 생성했던 IOCP 핸들이고, lpNumberOfBytes I/O작업에서 읽거나 쓴 바이트의 수를 반환해 준다. 세 번째 인자는 핸들을 IOCP에 등록할 때 키로 등록했던 값을 반환해준다. 이 값을 가지고 어떤 핸들로 부터의 작업이 완료가 되었는지 알수 있다. 네 번째 인자는 작업을 등록할 때, 어떤 작업인지 흔적으로 남겼던 OVERLAPPED 구조체를 반환해 준다. 이 구조체에 남겨진 정보를 가지고 어떤 작업이 끝난건지 알 수 있도록 수동으로 만들어야 한다. 여섯 번째 인자는 IOCP로부터 오는 완료 메시지를 얼마나 대기 하고 있을 것인지 설정하는 값이다.

if( FALSE == GetQueuedCompletionStatus(

hIOCP,

&dwBytesTransferred,

&dwCompKey,

(LPOVERLAPPED *)&pOverlap,

INFINITE ) )

{

if( pOverlap != NULL )

{

if( 64 != GetLastError() )

{

printf( "Error Thread : GetQueueCompletionStatus( %d )\\n", GetLastError() );

return 0;

}

}

}

위와 같이 사용한다. 주의 해야 할 것은 GetQueuedCompletionStatus함수가 반환하는 값이 FALSE라고 해서 에러가 아니라 pOverlap이 NULL이고 FALSE를 발생했던 에러의 값이 64가 아닐 때만 진짜로 에러가 발생한 것이라는 것이다.

5. 완료된 작업에 대해 해야 할 일을 한다.

위에 GetQueuedCompletionStatus함수에서 전달 받은 값들을 가지고 3번 과정에서 I/O작업을 신청할 때, I/O작업이 끝나면 하려고 했던 작업을 하면된다.

if( dwBytesTransferred == 0 )

{

printf("Closing socket %d\\n", g_CClients[dwCompKey].GetSocket() );

if( SOCKET_ERROR == closesocket( g_CClients[dwCompKey].GetSocket() ) )

{

if(GetLastError() == 10038)

continue;

else

{

printf("closesocket() failed with error %d\\n", WSAGetLastError());

return 0;

}

}

continue;

}

//recv

if( )

{

// 또 하나 중요한 것은 3번에서 신청한 I/O작업에 사용한 버퍼의 포인터를

// 여기서 알고있어야 한다는 것이다.

Printf( %s\\n, szBuffer );

}

else if( )

{

printf( 쓰기가 완료되었네요\\n );

}

else

{

// 남겼던 흔적이 없거나 에러가 발생한것임.

}

위의 GetQueuedCompletionStatus함수에서 반환해준 dwBytesTransferred의 값이 0이란 의미는 해당 핸들이 끊어졌다는 것이다. ( 소켓의 경우 ) 따라서, 핸들을 닫아 주면 된다. 그 다음에는 아까 전에 말했던 OVERLAPPED구조체에 3번에서 I/O작업 신청할 때 남겼던 흔적에 따라서 나머지를 처리해 주면 된다.

IOCP를 사용할 때 주의 할 점

IOCP 기법을 사용할 때의 주의 할 점은 I/O작업에 사용할 버퍼가 I/O작업을 신청한 시점부터 IOCP로부터 완료했다는 메시지를 받기 전까지 변경이 되면 안 된다는 것이다. 따라서, I/O 작업에 사용하는 버퍼는 전역 변수이거나 동적으로 할당된 메모리 공간이어야 한다. 특히 WSASend()나 WriteFile() 작업을 할 때, 함수를 호출하는 시점에 사용했던 공간을 함수이 끝나자 마자 지워버리거나( 지역 변수 ), 다른 값으로 변경하게 되면, 나중에 IOCP가 시간이 생겨서 이 작업을 할 때, 엉뚱한 공간을 참조하거나, 원래 쓰려고 했던 데이터가 아닌 다른 데이터를 쓰게 되므로 조심해야 한다.

OVERLAPPED구조체도 마찬가지다. 작업이 끝나기 전에 지워지면 안 된다.