- 기본 동작 구조 (출처: http://www.slideshare.net/sm9kr/windows-registered-io-rio)
: I/O initiation -> I/O processing -> I/O completion
- 1_IOCP
: worker thread 도입
#pragma comment(linker, "/subsystem:console")
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <WinSock2.h>
#include <Windows.h>
#pragma comment(lib, "ws2_32.lib")
#pragma comment(linker, "/subsystem:console")
// Overlapped 작업의 종료를 대기할 스레드
DWORD __stdcall EchoThread( void* p )
{
// 여기서 비동기작업의 종료를 대기하면 됩니다.
// 즉 WSAWait....()
}
int main()
{
WSADATA w;
int ret = WSAStartup( MAKEWORD(2,2), &w);
//-------------------------------------------
// 1. Overlapped io를 위한 소켓 생성
// 표준 C 네트워크 함수 : 소문자()
// WSAxxxx() 함수들 : windows socket 2.0 부터 지원되는
// windows만의 개념들..
int listen_sock = WSASocket( PF_INET, SOCK_STREAM,
0, 0, 0,
WSA_FLAG_OVERLAPPED);
// 2. 소켓에 주소 지정(bind)
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = INADDR_ANY;
bind( listen_sock, (struct sockaddr*)&addr, sizeof addr);
listen(listen_sock, 5);
while(1)
{
struct sockaddr_in addr2;
int sz = sizeof addr2;
int link_sock = accept( listen_sock,
(struct sockaddr*)&addr2, &sz);
printf("클라이언트가 접속되었습니다\n");
// Overlapped로 수신 하는 코드
WSAEVENT ev = WSACreateEvent();
WSAOVERLAPPED ov = {0};
ov.hEvent = ev;
// 수신 버퍼..
char s[1024] = {0};
WSABUF buf;
buf.buf = s;
buf.len = 1024;
DWORD flag = 0;
DWORD recvBytes = 0;
int n = WSARecv( link_sock,
&buf, 1, // 버퍼와 버퍼 갯수
&recvBytes, // 받을 data 크기
&flag,
&ov, // overlapped 구조체
0);
// 새로운 스레드를 생성해서 비동기 작업의 완료를 대기한다.
CreateThread( 0, 0, EchoThread, (void*)ev, 0, 0);
}
closesocket( link_sock);
//------------------------------
WSACleanup();
}
- 2_IOCP
: worker thread 구현 : IOCP 생성
#pragma comment(linker, "/subsystem:console")
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <WinSock2.h>
#include <Windows.h>
#pragma comment(lib, "ws2_32.lib")
DWORD __stdcall EchoThread( void* p )
{
HANDLE hPort = (HANDLE)p;
WSAOVERLAPPED* pov;
DWORD bytes, key;
while( 1 )
{
// IOCP에 있는 완료큐에 작업이 들어올때를 대기한다.
GetQueuedCompletionStatus( hPort, &bytes, &key,
&pov, INFINITE);
printf("비동기 작업 완료 : %d bytes, key : %d\n",
bytes, key);
}
return 0;
}
int main()
{
WSADATA w;
int ret = WSAStartup( MAKEWORD(2,2), &w);
//--------------------
// 1. 입출력 완료 포트(IOCP)를 생성합니다.
HANDLE hPort = CreateIoCompletionPort(
(HANDLE)-1, // IOCP에등록할 파일(소켓)
0, // 이미 존재 하는 IOCP핸들
0, // 완료키
2);// IOCP에서 비동기작업을 대기할 스레드
// 갯수 (CPU의 갯수만큼이 가장좋다.)
// 2. 비동기 작업이 완료 될때를 처리할 스레드 생성
HANDLE h1 = CreateThread( 0, 0, EchoThread, (void*)hPort,
0, 0);
HANDLE h2 = CreateThread( 0, 0, EchoThread, (void*)hPort, 0, 0);
//-------------------------------------------
int listen_sock = WSASocket( PF_INET, SOCK_STREAM,
0, 0, 0,
WSA_FLAG_OVERLAPPED);
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = INADDR_ANY;
bind( listen_sock, (struct sockaddr*)&addr, sizeof addr);
listen(listen_sock, 5);
int cnt = 0;
while(1)
{
struct sockaddr_in addr2;
int sz = sizeof addr2;
int link_sock = accept( listen_sock,
(struct sockaddr*)&addr2, &sz);
// Client와 연결된 소켓의 핸들을 IOCP에 등록한다.
// IOCP를 만들때와 IOCP에 장치(파일)을 등록할때 모두
// 아래 함수를 사용합니다.
CreateIoCompletionPort( (HANDLE)link_sock,
hPort,
cnt++,
2);
printf("클라이언트가 접속되었습니다\n");
// Overlapped로 수신 하는 코드
WSAEVENT ev = WSACreateEvent();
WSAOVERLAPPED ov = {0};
ov.hEvent = ev;
// 수신 버퍼..
char s[1024] = {0};
WSABUF buf;
buf.buf = s;
buf.len = 1024;
DWORD flag = 0;
DWORD recvBytes = 0;
int n = WSARecv( link_sock,
&buf, 1, // 버퍼와 버퍼 갯수
&recvBytes, // 받을 data 크기
&flag,
&ov, // overlapped 구조체
0);
}
//------------------------------
WSACleanup();
}
- 3_IOCP
: overlapped 구조체 확장 -> 입출력당 1개의 자료 구조
#pragma comment(linker, "/subsystem:console")
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <WinSock2.h>
#include <Windows.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib")
#define READ_MODE 1
#define WRITE_MODE 2
// OVERLAPPED 구조체는 보통 확장해서 사용하게 됩니다.
// 입출력당 1개의 자료 구조
struct IO_DATA
{
WSAOVERLAPPED ov;
WSABUF wsaBuf;
char buff[1024];
int mode;
};
DWORD __stdcall EchoThread( void* p )
{
HANDLE hPort = (HANDLE)p;
WSAOVERLAPPED* pov;
DWORD bytes, key;
while( 1 )
{
// IOCP에 있는 완료큐에 작업이 들어올때를 대기한다.
GetQueuedCompletionStatus( hPort, &bytes, &key,
&pov, INFINITE);
printf("비동기 작업 완료 : %d bytes, key : %d\n",
bytes, key);
IO_DATA* ioData = (IO_DATA*)pov;
printf("수신된 data : %s\n", ioData->buff);
CloseHandle( ioData->ov.hEvent);
free(ioData);
}
return 0;
}
int main()
{
WSADATA w;
int ret = WSAStartup( MAKEWORD(2,2), &w);
//--------------------
// 1. 입출력 완료 포트(IOCP)를 생성합니다.
HANDLE hPort = CreateIoCompletionPort(
(HANDLE)-1, // IOCP에등록할 파일(소켓)
0, // 이미 존재 하는 IOCP핸들
0, // 완료키
2);// IOCP에서 비동기작업을 대기할 스레드
// 갯수 (CPU의 갯수만큼이 가장좋다.)
// 2. 비동기 작업이 완료 될때를 처리할 스레드 생성
HANDLE h1 = CreateThread( 0, 0, EchoThread, (void*)hPort,
0, 0);
HANDLE h2 = CreateThread( 0, 0, EchoThread, (void*)hPort, 0, 0);
//-------------------------------------------
int listen_sock = WSASocket( PF_INET, SOCK_STREAM,
0, 0, 0,
WSA_FLAG_OVERLAPPED);
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = INADDR_ANY;
bind( listen_sock, (struct sockaddr*)&addr, sizeof addr);
listen(listen_sock, 5);
int cnt = 0;
while(1)
{
struct sockaddr_in addr2;
int sz = sizeof addr2;
int link_sock = accept( listen_sock,
(struct sockaddr*)&addr2, &sz);
// Client와 연결된 소켓의 핸들을 IOCP에 등록한다.
// IOCP를 만들때와 IOCP에 장치(파일)을 등록할때 모두
// 아래 함수를 사용합니다.
CreateIoCompletionPort( (HANDLE)link_sock,
hPort,
cnt++,
2);
printf("클라이언트가 접속되었습니다\n");
// IO작업당 아래 구조체를 만들어서 사용한다.
IO_DATA* ioData = (IO_DATA*)malloc(sizeof(IO_DATA));
memset(ioData, 0, sizeof(IO_DATA));
ioData->wsaBuf.buf = ioData->buff;
ioData->wsaBuf.len = 1024;
ioData->mode = READ_MODE;
ioData->ov.hEvent = WSACreateEvent();
DWORD flag = 0;
DWORD recvBytes = 0;
int n = WSARecv( link_sock,
&(ioData->wsaBuf), 1, // 버퍼와 버퍼 갯수
&recvBytes, // 받을 data 크기
&flag,
(WSAOVERLAPPED*)ioData, // overlapped 구조체
0);
}
//------------------------------
WSACleanup();
}
- 4_IOCP
: 소켓 구조체 구현 : write 구현 -> WSASend 도 완료되면 GetQueuedCompletionStatus로 통보된다.
#pragma comment(linker, "/subsystem:console")
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <WinSock2.h>
#include <Windows.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib")
#define READ_MODE 1
#define WRITE_MODE 2
// 클라이언트와 연결된 소켓정보를 관리하는 구조체
struct SOCKET_DATA
{
int sock;
SOCKADDR_IN addr;
};
// OVERLAPPED 구조체는 보통 확장해서 사용하게 됩니다.
// 입출력당 1개의 자료 구조
struct IO_DATA
{
WSAOVERLAPPED ov;
WSABUF wsaBuf;
char buff[1024];
int mode;
};
DWORD __stdcall EchoThread( void* p )
{
HANDLE hPort = (HANDLE)p;
WSAOVERLAPPED* pov;
DWORD bytes, key;
while( 1 )
{
GetQueuedCompletionStatus( hPort, &bytes, &key,
&pov, INFINITE);
printf("비동기 작업 완료 : %d bytes, key : %d\n",
bytes, key);
SOCKET_DATA* pSock = (SOCKET_DATA*)key;
IO_DATA* ioData = (IO_DATA*)pov;
if ( ioData->mode == READ_MODE )
{
printf("수신된 data : %s\n", ioData->buff);
// 수신에 사용한 버퍼를 사용해서 송신한다.
// 수신중에 overlapped 구조체의 내용은 변경되어 있게됩니다
memset( &(ioData->ov), 0, sizeof(WSAOVERLAPPED));
strcat( ioData->buff, " from server");
ioData->mode = WRITE_MODE;
//-----------------------------------
WSASend( pSock->sock,
&(ioData->wsaBuf), 1,
&bytes, 0, &(ioData->ov), 0);
//-----------------------------
}
else
{
closesocket(pSock->sock);
free( pSock );
CloseHandle( ioData->ov.hEvent);
free(ioData);
}
}
return 0;
}
int main()
{
WSADATA w;
int ret = WSAStartup( MAKEWORD(2,2), &w);
//--------------------
// 1. 입출력 완료 포트(IOCP)를 생성합니다.
HANDLE hPort = CreateIoCompletionPort(
(HANDLE)-1, // IOCP에등록할 파일(소켓)
0, // 이미 존재 하는 IOCP핸들
0, // 완료키
2);// IOCP에서 비동기작업을 대기할 스레드
// 갯수 (CPU의 갯수만큼이 가장좋다.)
// 2. 비동기 작업이 완료 될때를 처리할 스레드 생성
HANDLE h1 = CreateThread( 0, 0, EchoThread, (void*)hPort,
0, 0);
HANDLE h2 = CreateThread( 0, 0, EchoThread, (void*)hPort, 0, 0);
//-------------------------------------------
int listen_sock = WSASocket( PF_INET, SOCK_STREAM,
0, 0, 0,
WSA_FLAG_OVERLAPPED);
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = INADDR_ANY;
bind( listen_sock, (struct sockaddr*)&addr, sizeof addr);
listen(listen_sock, 5);
int cnt = 0;
while(1)
{
struct sockaddr_in addr2;
int sz = sizeof addr2;
int link_sock = accept( listen_sock,
(struct sockaddr*)&addr2, &sz);
// 소켓당 하나의 구조체
SOCKET_DATA* pSock = (SOCKET_DATA*)malloc(
sizeof(SOCKET_DATA));
pSock->sock = link_sock;
pSock->addr = addr2;
// Client와 연결된 소켓의 핸들을 IOCP에 등록한다.
// IOCP를 만들때와 IOCP에 장치(파일)을 등록할때 모두
// 아래 함수를 사용합니다.
CreateIoCompletionPort( (HANDLE)link_sock,
hPort,
(ULONG_PTR)pSock,
2);
printf("클라이언트가 접속되었습니다\n");
// IO작업당 아래 구조체를 만들어서 사용한다.
IO_DATA* ioData = (IO_DATA*)malloc(sizeof(IO_DATA));
memset(ioData, 0, sizeof(IO_DATA));
ioData->wsaBuf.buf = ioData->buff;
ioData->wsaBuf.len = 1024;
ioData->mode = READ_MODE;
ioData->ov.hEvent = WSACreateEvent();
DWORD flag = 0;
DWORD recvBytes = 0;
int n = WSARecv( link_sock,
&(ioData->wsaBuf), 1, // 버퍼와 버퍼 갯수
&recvBytes, // 받을 data 크기
&flag,
(WSAOVERLAPPED*)ioData, // overlapped 구조체
0);
}
//------------------------------
WSACleanup();
}
- PAGE_LOCKING (참고: http://www.slideshare.net/sm9kr/windows-registered-io-rio, http://ozt88.tistory.com/26)
: 완벽한 통지모델로 보이는 IOCP에도 문제는 있다.
: 하나의 I/O operation마다 버퍼 영역에 대한 page-lock/unlock
-> 특정 메모리에 대한 pin/unpin은 많은 CPU cycle 요구
-> 그래서 RECV를 posting 할 때, page-locking을 피하여 CPU cycle을 줄이기 위해 zero-byte recv로 최소화
: 하나의 I/O operation마다 시스템콜 호출
-> 유저모드-커널모드 전환 발생
■ Zero Byte Recv?
: 실제 수행을 시작하는 때를 감지할 수 있다면 이 문제를 해결할 수 있다.
: 비동기 작업을 명령하기 전에 먼저 0바이트를 읽는 Recv작업을 요청하는 것이다. 0바이트 읽는 작업이므로 필요한 버퍼도 0바이트, 그러므로 PAGE_LOCKING이 발생하지 않는다. 비동기 명령 프로세서를 사용하기 때문에 이 명령이 요청된 시점에는 작업이 바로 가능한지 불확실하지만, 이 명령이 완료된 시점에는 다음 작업을 바로 시작할 수 있을 가능성이 매우 높다. 그러니까 이때부터 본격적인(큰 용량의 버퍼를 사용하는) 작업을 요청하는 것이다. 이렇게 하면 최대한 쓸데없이 LOCKING된 메모리를 줄일 수 있다.
■ SO_RCVBUF 옵션
이 옵션은 소켓 버퍼의 크기를 설정하는 옵션이다. 이 옵션을 사용하여 버퍼 크기를 0으로 설정하는 경우, 커널이 따로 소켓버퍼(send, recv 버퍼)를 사용하지않고 직접 유저가 설정한 버퍼(메모리)에 직접 I/O를 때려박는다. 필자는 Memory-mapped file같은 느낌이라고 이해하고 있다. 0으로 설정하면 커널 버퍼를 거치지 않고 직접 유저의 버퍼에 데이터가 저장되기 때문에, 불필요해 보이는 복사가 수행되지 않아서 성능상에 이점이 있다. 위에서 살짝 언급한것처럼 가상메모리의 영역이 하드웨어 메모리 영역과 깊은 커플링을 맺게되면서, PAGE_LOCKING의 원인이 된다. 그렇다면 유저는 복사를 하지 않는 성능 이점과 PAGE_LOCKING의 이슈를 저울질 하여 옵션을 선택해야 할 것이다.