Overlapped IO
- 개념: 기본적으로 IO는 동기방식이다. ReadFile()하면 디스크에서 파일이 다 읽어질때까지 CPU는 대기한다. 이게 성능저하를 초래하기 때문에 비동기로 IO를 처리하는 방법인 Overlapped IO이다. 비동기로 여러번 데이터를 전송하면 데이터 전송이 중첩된다는 의미로 Overlapped형태라고 한다.
- 원리:
- 비동기 방식으로 WSASend()를 호출하면 이 IO처리의 책임이 쓰레드에서 Device Driver로 넘어간다. 즉 IO처리는 Driver가 담당하고 쓰레드는 다른 작업을 계쏙 진행할 수 있다는 것이다.
- Device Driver는 IO처리가 끝나면 이를 다시 쓰레드에 알려줘야 하는데 OVERLAPPED 구조체에 있는 이벤트 객체를 통해서 알려주기도 하고 아니면 지정된 Callback함수로 알려주는 방법도 있다. 하지만 이런 방법은 WSASend()를 요청한 바로 그 스레드로 응답이 돌아와 이놈이 다시 후속 작업을 처리하는 방식이다.
- IOCP방식은 위와는 달리 WSASend()로 IO처리를 요청하는 쓰레드 따로, Device Driver에서의 완료처리를 받아 후속작업을 전담하는 쓰레드가 따로 있는 방식.
- 장점:
- Buffer사용에서 장점을 갖는다. Traditional IO방식에서는 OS가 가지는 Buffer따로, 내가 전달하는 Buffer가 각각 따로라서 이 Buffer간 데이터를 복사하는데 시간이 걸리는데 Overlapped IO모델에서는 내가 전달하는 Buffer가 OS에서도 바로 쓰이기 때문에 데이터 복사 시간이 줄어 전체 IO시간이 줄어든다.
- 사용방법:
- socket()을 사용할때는 OverlappedIO를 사용할 때 별도 옵션이 필요 없지만 WSASocket()을 사용할때는 WSA_FLAG_OVERLAPPED옵션을 꼭 넣어줘야한다. (socket()은 winsock1.1이고 WSASocket()은 winsock 2.x함수이다.)
SOCKET sock = WSASocket( AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
- send(), recv()는 Overlapped IO를 지원하지 않는다. 그래서 WSASend(), WSARecv()를 사용해야 한다.
- socket()을 사용할때는 OverlappedIO를 사용할 때 별도 옵션이 필요 없지만 WSASocket()을 사용할때는 WSA_FLAG_OVERLAPPED옵션을 꼭 넣어줘야한다. (socket()은 winsock1.1이고 WSASocket()은 winsock 2.x함수이다.)
IOCP란 (IO Completion Port)
- Win32에서 제공하는 Kernal Object중 하나. 비슷한 예로 Mutex가 있다. IO를 감시하고 Port를 통해 IO데이터를 전달하는.. 그런 기능을 함.
- 특징:
- Thread Pooling을 지원한다. 즉 Thread를 여러개 미리 만들어놓고 계속 재사용한다. 이렇게 함으로써 Thread를 새로 만드는데 드는 비용을 아낄 수 있다. (overhead가 줄어든다.)
- Thread Pooling과는 별도로 IOCP는 CPU에 Dispatch되는 쓰레드의 수를 조절해서 Context switching에 낭비되는 비용을 줄인다. 즉 쓰레드 수를 조절하여 너무 많은 쓰레드가 생성되어 CPU 작업을 기다리는 일을 방지한다.
- 동작원리:
- WSARecv(), WSASend()에 의해서 OS에 비동기 처리를 위탁하면 IO처리가 끝났을 때 응답이 I/O CompletionQ Queue에 쌓이게 된다.
- WorkerThread는 이 I/O Completion Queue에 값이 쌓이면 wait상태에서 깨어나 값을 읽어들이고 후속작업을 처리하게된다.
- 사용법:
HANDLE CreateIoCompletionPort( HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads )
- Parameter1: IOCP와 연결할 Device에 대한 Handle. File Object일수도 있고 Socket일수도 있다.
- Parameter2: CreateIoCompletionPort()는 두가지 기능을 하는데 첫째는 IOCP를 생성하는 역할, 두번째는 만들어진 IOCP와 Device Handle을 연결하는 역할. 첫번째 역할일때는 Parameter2는 NULL, 두번째 역할일때는 이미 만들어진 IOCP handle을 여기에 입력해주면 된다.
- Parameter3: IO가 완료되면 IOCP는 작업쓰레드에 IO작업이 완료되었음을 알려주는데 이 때 어떤 IO가 완료되었는지 알려면 이 CompletionKey를 사용한다. CompletionKey는 Parameter1의 Device Handle을 구분해주는 역할로 써야한다. 왜냐면 다른 방법이 없기 때문이다. (주로 이 역할로만 사용하지는 않고 이걸 확장해서 사용한다.)
- IO가 완료되었을 때 IOCP handle과 CompletionKey가 작업스레드로 넘어오는데 Parameter1의 socket정보는 따로 넘어오지 않는다. 그래서 이 Parameter1의 socket정보를 꼭 CompletionKey에 미리 담았다가 나중에 작업스레드에 CompletionKey가 넘어올 때 열어보고 아 이놈이구나 확인해야 하는 것이다.
- 타입은 ULONG_PTR인데 크기는 DWORD와 같은 4byte 즉 포인터의 크기다. 여기에 어떤 포인터가 들어오든 상관없다. 그래서 우리가 임의로 구조체를 만들어 동적할당한 후 포인터를 넘겨버려도 상관없다. (근데 그러면 free는 언제?)
- Parameter4: CPU에 Dispatch되는 쓰레드 수를 조절하는 옵션. 0으로 넣으면 디폴트로 CPU갯수만큼 쓰레드 수가 조절된다. 이건 WorkerThread숫자와는 좀 다른 의미같은데... 테스트 하면서 확인해야할듯. (일반적으로 쓰레드 숫자는 cpu*2이고 여기에 들어가는 옵션은 0로 cpu숫자이다.) 또한 IOCP생성 시에는 여기에 들어간 값이 유효하지만 IOCP연결시에는 여기에 어떤 값이 들어가도 무시된다.
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds )
- IOCP Queue에 들어있는 데이터를 꺼내가는 API이다.
- IOCP Queue에 데이터가 들어오면 GetQueuedCompletionStatus()는 리턴한다. (그 전까지는 Blocking하고 Thread는 suspend상태가 된다. CPU를 안먹는다.)
- WorkerThread가 이 API를 호출하면 WorkerThread는 곧바로 WaitingThreadQueue에 들어간다. 이 WaitingThreadQueue가 바로 쓰레드풀이다.
- IOCP Queue에 데이터가 들어와 Thread가 awake되어서 일을 시작하면 Released Thread List에 들어가게 된다.
- 일을 하던 Thread가 어떠한 이유로 잠시 멈춘다면(WaitForSingleObject()등의 이유로) 이 Thread는 Paused Thread List에 들어간다.
- WaitingThreadQueue에는 쓰레드가 무한정 들어갈 수 있는데 IOCP는 context switching에 들어가는 비용을 줄이고자 이 쓰레드 중 일하는 놈들의 수를 일정하게 관리한다. 이 숫자가 위에서 CreateIoCompletionPort()에서 인자로 넣어줬던 NumberOfConcurrentThreads이다.
- 정확히 말하자면 NumberOfConcurrentThreads는 Released Thread List에 들어가있는 쓰레드의 숫자이다. 즉 아무리 많은 쓰레드를 WaitingThreadQueue에 넣어도 NumberOfConcurrentThreads이상은 일하지 않는다는 얘기다.
- 그렇다면 우리는 WorkerThread를 몇개 만들고 NumberOfConcurrentThreads에는 얼마를 할당해야할까? WorkerThread중 일부는 일하고(ReleasedThreadList), 일부는 일하다가 잠시 쉬고(PausedThreadList), 일부는 일을 대기하며 잠자고(WaitingThreadQueue)있다. NumberOfConcurrentThreads는 ReleasedThreadList의 제한갯수를 의미한다. 따라서 NumberOfConcurrentThreads는 CPU갯수 즉 0으로 설정, 그리고 WorkerThread는 CPU갯수 X 2 정도로 만들면 일반적으로 바람직 하다고 하겠다.
- 추가적으로 Waiting Thread Queue는 FIFO가 아닌 LIFO 즉 stack의 구조를 가진다. 이 또한 한번 사용했던 쓰레드를 다시 자주 사용함으로써 context switching을 줄이기 위해서이다. (쓰레드는 일정시간 사용되지 않으면 그 쓰레드가 가지고 있는 resource들이 메모리에서 HDD로 Swap된다. 그리고 프로세서의 캐쉬에서도 flush된다. 즉 쓰는 애들만 자주 쓰는게 메모리 효율성에서도 좋다는 얘기다.)
기타 설명
WSASend(): send()함수의 확장형 (WSARecv()도 똑같다.)
- WSASend 함수는 TCP 소켓에서 비동기IO(혹은 Overlapped IO)방식으로 데이터를 전송하기 위해 사용하는 함수다.
- 이 함수를 UDP 소켓에서 사용하기 위해서는 connect(:4100)나 WSAConnect(:4100) 함수로 상대방 인터넷(:12)주소 정보가 바인딩 되어 있어야 한다.
- socket() 함수로 만들어진 소켓은 기본적으로 Overlapped특성을 가지고 있다. socket()대신 WSASocket() 함수를 사용하려면 마지막 parameter로 WSA_FLAG_OVERLAPPED를 설정해야 한다.
- socket()이나 WSASocket()으로 소켓을 만들 때 overlapped옵션을 선택하면 WSASend는 비동기 방식으로 데이터를 전달한다.
- 하지만 소켓을 만들 때 overlapped옵션이 선택되어있었다고 하더라고 WSASend()의 parameter 중 lpOverlapped 와 lpCompletionRoutine가 NULL 이라면 send(:4100)함수와 동일하게 동기 방식으로 데이터를 전달한다.
- WSASend()를 비동기 방식으로 사용하면 리턴값으로 SOCKET_ERROR를 반환하고 WSAGetLastError가 WSA_IO_PENDING(997)을 나타낸다. 이는 비동기 연산을 위한 초기작업이 성공적으로 진행되었으며 실제 데이터 전송은 나중에 일어날 것임을 의미한다.
int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
- 또한 SOCKET_ERROR리턴 시 아직 데이터는 전송되지 않았으므로 lpNumberOfBytesSend값은 수정되지 않는다.
- WSASend()의 다섯번째 Parameter LPWSAOVERLAPPED는 OVERLAPPED구조체의 포인터인데 이 구조체가 IO가 끝나기 전까지 메모리에서 절대 사라지면 안된다. (주의)
- WSASend()의 여섯번째 Parameter LPWSAOVERLAPPED_COMPLETION_ROUTINE은 Overlapped IO가 끝날때 콜되는 Callback function을 넣어주는 자리이다. IOCP에서는 딱히 쓸 필요 없다.
PostQueuedCompletionStatus()
BOOL PostQueuedCompletionStatus( HANDLE CompletionPort, DWORD dwNumberOfBytesTransferred, ULONG_PTR dwCompletionKey, LPOVERLAPPED lpOverlapped);
- 이 API는 WSARecv()나 WSASend()처럼 OS에 의해 IOCP큐에 데이터가 전달되는것과 달리 내가 직접 IOCP큐에 데이터를 집어넣어서 WorkerThread가 이를 처리하도록 할 때 쓰인다.
- 위에서 설명했듯 IOCP는 OS(혹은 드라이버)에 의한 IO비동기 처리에 주로 쓰이지만, WSARecv()나 WSASend()대신 PostQueuedCompletionStatus()를 이용하면 스레드간 데이터 전달 시 '큐'로써 IOCP를 활용할수도 있다는 얘기다.
Overlapped 구조체 확장
typedef struct _PERIODATA{ OVERLAPPED ov; WSABUF wsabuf; char buffer[4096]; } PERIODATA* pPerIOData = new PERIODATA; memset(pPerIOData, 0, sizeof(PERIODATA)); pPerIOData->wsabuf.buf = pPerIOData->Buffer; pPerIOData->wsabuf.len = sizeof(pPerIOData->Buffer); if(::WSARecv(hClientSocket,&pPerIOData->wsabuf,1,&dwReceiveSize,&dwFlag,&pPerIOData->ov,NULL)==SOCKET_ERROR){ if(::WSAGetLastError() != WSA_IO_PENDING)//WSA_IO_PENDING과 ERROR_IO_PENDING아무거나 사용해도 된다. { SLOG(_T("ERROR: WSARecv() %d"), GetLastError()); //GetLastError()는 에러코드를 리턴한다.(숫자) CloseServer(); return false; } }
- 이렇게 하고 WSARecv()함수의 인자로 &pPerIOData->wsabuf를 넣어주면 pPerIOData->Buffer에 리턴된 IO값이 담기게 된다.
'Programming > Windows Socket Programming' 카테고리의 다른 글
Write log to file - Code (0) | 2020.03.14 |
---|---|
IOCP model(Chat Server) - Code (0) | 2020.03.14 |
Basic model(Chat Client) - Code (0) | 2020.03.01 |
Basic model(Client) - Idea (0) | 2020.03.01 |
Basic model(Chat Server) - Code (0) | 2020.03.01 |