입출력 완료 포트(IOCP)

비동기 입출력과 완료 포트를 이용하고, 스레드를 효율적으로 관리하며, GQCS에 의해 필요한 스레드만이 깨어난다.

APC 큐는 스레드 별로 확인할수 있지만, IOCP는 제약이 없다.

  1. 입출력 완료 포트는 CreateIoCompletionPort()을 통해 생성하고, CloseHandle로 파괴한다.
  2. 입출력 완료 포트에 접근하는 스레드를 별로도 두는데, 이를 작업자 스레드라고 부르며, 이를 관리해준다.
  3. 입출력 완료 포트에 저장된 결과를 처리하려면 GetQueuedCompletionStatus() 함수를 통해 처리한다.

동작원리

  1. 응용 프로그램을 구성하는 임의의 스레드에서 비동기 입출력 함수를 호출함으로써 OS 입출력 작업을 요청한다.
  2. 모든 작업자 스레드는 GQCS 함수를 호출하여 입출력 완료 포트를 감시한다. 완료된 비동기 입출력 작업이 아직 없다면, 모든 작업자 스레드는 대기 상태가 된다. 이때 대기 중인 작업자 스레드 목록은 입출력 완료 포트 내부에 저장된다.
  3. 비동기 입출력 작업이 완료되면, 운영체제는 입출력 완료 포트에 결과를 저정한다. 이때 저장되는 정보를 입출력 완료 패킷이라고 부른다.
  4. 운영체제는 입출력 완료 포트에 저장된 작업자 스레드 목록에서 하나를 선택하여 깨운다. 깨어난 작업자 스레드는 비동기 입출력 결과를 처리한다. 이후 작업자 스레드는 필요에 따라, 다시 비동기 입출력 함수를 호출할수 있다.

IOCP에 등록된 핸들(소켓)이 CloseSocket을 한경우, GQCS로 해당 소켓이 오류형식으로 워커스레드가 깨어나고 IOCount를 차감시키는데 소켓은 재활용되므로, 새로 들어온 클라가 끊길수있다.

Example Note

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<포함>
클라이언트
{
SOCKET socket;
OVERLAPPED Recv;
OVERLAPPED Send;
SendQ
RecvQ;
}

or

<확장>
class MyOverlapped : public OVERLAPPED
{
int Mode;
}


ConnectedFail => Connect Queue가 넘쳣을때 - Accept에서 제대로 못빼고있다. => Accept Thread는 Accept만 처리하게 만들거다.
// IOCP 핸들

// 워커스레드
IOCP 모델에서 PostQueuedCompletionStatus(IOCP, 0, 0, NULL); 을 넣어서 종료시킨다.
종료 시점
모든 클라이언트가 정상 종료가 되었다는것을 확인뒤에 종료시킨다.

-> LPOVERLAPPED, Key, Transfered 변수가 전부 0이라면 정상적으로 워커스레드를 종료시키면된다.

GetQueuedCompletionStatus에서 LPOVERLAPPED 가 Nullptr || Transfer = 0로 나왓다면 에러 상황이다. -> 종료시키자. (물론 로그는 남겨야.. )

ERROR_NETNAME_DELETE -> 이런 예외처리도 존재한다(해당 소켓이 갑자기 4 Way HandShake 없이 끊어진상태 그냥 closesocket만 해주자. 실제로는 이렇게 처리안해줘도 된다.)


IO Reference Count 방식을 통해

Session
{
Client;
IOCount;
}
// IOCount의 증감, 감소를 위해 Interlocked함수 계열을 써야된다.
// 이 세션을 종료시킬 조건은 IOCount = 0일때


IOCount는 처리하기전에 먼저 올려야된다. -> 큐에 올라가버린다.
에러가 떳는데 IO_PENDING 아니라면 완료 통지가 안오니까 IO--;


IO 완료되었을때 1 감소시키는데 감소시키는 시점은
Recv 완료, Send 완료시에 IO차감 시킨다.
IOCount는 초기화는 맨처음에만 한다.

WorkerThread()
{
GQCS()

if(Recv 종료라면)
{
// IO차감

// Recv 재등록
}

if(Send 종료라면)
{
// IO차감

// Send 재등록
}
}

// 차감하는 곳에는 모두 이렇게 들어간다.
IO--;
if(IO <= 0)
Release(); // -> 이러면 릴리즈가 2번 걸릴수 있다.

// 차감을
if(InterlockedDecrement(IOCount) == 0)
{
Release(); // 외부스레드에서 따로 막아야된다.
}



Thread1(-)
{
IO++;
WSARecv();
if(에러)
{
if(InterlockedDecrement(IOCount) == 0)
{
Release(); // 외부스레드에서 따로 막아야된다.
}
}
}

WorkerThread(-)
{
GQCS(-)

if(InterlockedDecrement(IOCount) == 0)
{
Release();
}
}

// 이렇게 모든 IO 참조카운트방식의 감소할때는 릴리즈도할수있게 만들어줘야된다.

closesocket
윈도우 객체 핸들이니깐, 리소스핸들 반환도 한다. GQCS에서 등록된 소켓을 closesocket을 했다면 GQCS 오류로 처리된다.
-> 소켓 번호는 재사용된다. (그 순간 Accept 된다면 같은 소켓번호를 등록해버린다. ///) -> 예전 꺼의 작업완료통지가 안와버린다.
-> 끊고싶다면 Release()로 하는게 안전함.


TIME_WAIT이 왜뜨고 어디에 남는가?
TCP 상태의 가장 마지막 단계.

A B
(FIN_WAIT1) -> FIN 보낸다. -> (LAST_ACK)

(TIME_WAIT) <- FIN,ACK를 보낸다. <- (LAST_ACK)
(이쪽에서 TIME_WAIT 상태가 된다.)
-> ACK를 보낸다. ->


<!!!!!!!!!!!! 접속 종료 !!!!!!!!!!!!>

TIME_WAIT을 안걸리기 위해서
SO_LINGER 옵션을 주고 끊어야 된다.

-> TIME_WAIT의 문제 : 리소스 문제(포트, 커널)

Shutdown -> FIN을 넘긴다. -> 우리는 TIME_WAIT을 포기한다.


======================================================================================================================================================

Client
{
OVERLAPPED Recv;
OVERLAPPED Send;

CStreamQ SendQ;
CStreamQ RecvQ;

long SendFlag; // 지금 Send를 보내고 있는지 확인 (Interlocked함수 써야된다.)
DWORD IOCount; // 여기도 Interlocked함수
}

==> 완료 통지에 대해선 순서가 없다.
==> Transfered 변수가 있다라는건 Send 후에 완료 통지를 보고 버퍼 위치를 옮기면 된다. => 완전히 보낼때까지 완료 통지가 안온다.
==> 링버퍼 존재가 달라졌다.