Overlapped I/O
논블록 소켓 단점을 보완한 네트워크 통신 방법이 Overlapped I/
논블로킹 소켓 프로세스
소켓 I/O 함수가 리턴한 코드 would block 인 경우 재시도 호출 낭비 발생.
소켓 I/O 함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산 발생.
CPU 안에 있는 캐시 메모리에 메모리 내용이 복사되어 있으면 데이터 액세스는 매우 빠르지만
캐시에 없는 데이터를 액세스할 때는 메인 메모리 RAM을 액세스하는데, 이 속도는 매우 느림.
물론 하드디스크나 네트워크 데이터보다는 빠르지만 고성능 서버 개발 시 이 복사 연산 무시할 수 없음.
TCP, UDP 논블록 소켓에서 재시도용 호출 낭비
TCP 소켓 send() 함수를 호출하면 would block 은 절대 발생하지 않음.
보내려는 데이터가 5바이트인데 송신 버퍼에 빈 공간이 1바이트라면, 1바이트만 소켓 송신 버퍼에 채워지고 성공 리턴.
TCP, UDP 소켓 receive() 처리 부분 에서 수신 버퍼에 1바이트라도 들어 있으면 I/O 가능하여 would block 발생하지 않음.
하지만 UDP send() 의 경우 보내려는 데이터가 5바이트인데 송신 버퍼 빈 공간이 1아이트라면, 넣을 수 있는 크기를 넘어서 would block 발생. 결국 CPU 낭비로 이어짐.
논블록 소켓 프로세스
1. 소킷에 I/O 가능인 것이 있을 때까지 기다림.
2. 소켓에 대해 논블록 액세스.
3. would block이 발생했으면 그대로 두고, 그렇지 않으면 실행 결과 리턴 값을 처리.
이런 문제를 해결하는 기법이 Overlapped 또는 Asynchronous(비동기) I/O 재시도용 호출 낭비 문제와 소켓 함수에 데이터 블록 복사 부하 문제를 모두 해결.
Overlapped I/O 프로세스
1. 소켓에 대해 Overlapeed 액세스 시도
2. Overlapped 액세스가 성공했는지 확인한 후 성공했으면 결과값을 얻어 와서 나머지를 처리.
void OverlapedSocketOperation()
{
var overlapeedSendStatus;
(result, length) = s.OverlappedSend(
data,
overlappedSendStatus);
if (length > 0)
{
}
else if (result == WSA_IO_PENDING)
{
// Overlapped I/O가 진행 중
while (true)
{
(result, length) = GetOverlappedresult(s, overlappedSendStatus);
if (length > 0)
{
// 보내기 성공
}
else
{
// I/O pedding 중
}
}
}
}
Overlapped I/O 는 논블록 소켓과 비교했을 때 앞서 두 가지 이유로 성능 향상 Overlapped I/O 함수는 즉시 리턴되지만, 운영체제로 해당 I/O실행이 별도로 동시간대에 진행되는 상태. 운영체제는 소켓 하뭇에 인자로 들어갔더 데이터 블록을 백그라운드에서 액세스. 즉, 운영체제가 마음대로 데이터를 액세스 하기 때문에 이름이 중첩된 overlapped
주의사항은 호출한 Overlapped I/O 전용 함수가 비동기로 하는 일이 완료될 때까지 소켓 API에 인자로 넘긴 데이터 블록을 제거하거나 내용을 변경해서는 안됨. 그뿐만 아니라 Overlapped I/O 전용 함수의 인자로 Overlapped status 구조체가 같이 들어가는데, 완료 여부는 이 구조체로 알 수 있음. Overlapped status 구조체 또한 운영체제에서 백그라운드로 액세스 중이기 떄문에 중간에 없애거나 내용을 변경해서도 안됨.
특징
1. Overlapped I/O 객체가 서로 다르고 데이터 블록도 서로 다르면 중첩 가능.
2. 운영체제는 송신할 데이터나 수신할 데이터가 있으면 데이터 블록을 복사하지 않고 그 데이터블록 자체를 그대로 사용.
3. 윈도우에서만 사용 가능.
4. accept, connect 함수 계열의 초기화 복잡
5. 완료되기 전까지 Overlapped status 객체가 데이터 블록을 중간에 훼손하지 말아야 함.
6. send, receive, connect, accept 함수를 한 번 호출하면 이에 대한 완료 신호는 딱 한 번만 오기 때문에
프로그래밍이 간결해짐.
epoll
Overlapped I/O 또한, 소켓 개수에 비례해서 루프를 돌기 때문에 성능 문제는 존재함. 예를 들어 소켓이 1만개 이고 각 소켓이 초당 100번씩 I/O 가능 이벤트 혹은 I/O 완료 이벤트가 발생하면, 초당 100 만번의 처리를 해야 함. 소켓 개수가 많을 때 이러한 루프 없이 한 번에 끝내는 방법이 IOCP, epoll 이다. epoll은 소켓이 I/O 가능 상태가 되면 이를 감지해서 사용자에게 알림을 해 주는 역할.
소켓 2가 I/O 가능이 되는 순간 epoll은 내장된 큐에 푸시 진행. 사용자는 큐에 푸시된 epoll 이벤트 정보를 pop 하여 사용 가능. 따라서 소켓이 만 개라고 하더라도 이 중에서 I/O 가능이된 것들만 epoll을 이용해서 바로 이용 가능.
epoll = new epoll();
foreach(s in sockets)
{
epoll.add(s, GetUserPtr(s));
}
// 사용자가 원하는 시간까지만 블로킹되며, 그 전에 이벤트가 생기는 순간 즉시 리턴
events = epoll.wait(100ms);
foreach(event in events)
{
s = event.socket;
// 위 epoll, add에 들어갔던 값을 얻는다.
userPtr = events.userPtr;
// 수신? 송신?
type = event.type;
if (type == ReceiveEvent)
{
(result, data) = s.recv();
if (data.length > 0)
{
// 수신된 데이터를 처리한다.
Process(userPtr, s, data);
}
}
}
만약에 select()를 썼다면 모든 소켓에서 루프를 돌아야 한다. 하지만 epoll을 쓰면 I/O 가능인 상태의
소켓에서만 루프를 돌면 된다.
epoll 장점
상태변화의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요 없다.
select 함수에 대응하는 epoll_wait 함수호출 시 관찰대상의 정보를 매번 전달할 필요가 없다.
이벤트가 발생한 파일 디스크립터의 정보가 별도로 묶이기 때문에 select 방식에서 보인 전체 파일 디스크립터를 대상으로 하는 반복문의 삽입이 불필요하다.
레벨트리거
레벨 트리거 방식에는 입력버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 등록된다. 참고로 select 모델은 레벨 트리거 방식으로 동작한다.
엣지트리거
엣지 트리거 방식에서는 데이터가 수신되면 딱 한번 이벤트가 등록된다. 이러한 특성 때문에, 일단 입력과 관련해서 이벤트가 발생하면, 입력버퍼에 저장된 데이터 전부를 읽어 들여야 한다. 따라서 앞서 설명한 다음 내용을 기반으로 입력버퍼가 비어있는지 확인하는 과정을 거쳐야 한다. 입력버퍼에 저장된 데이터를 전부를 읽어 들이지 않으면 남은 데이터를 영원히 꺼내지 못 할 수도 있다. 또한, 엣지트리거는 소켓을 넌-블로킹 모드로 만드는 이유는 read & write 함수의 호출은 데이터 분량에 따라서 IO로 인한 서버를 오랜 시간 멈추는 상황으로까지 이어지게 할 수 있다. 때문에 엣지 트리거 방식에서는 반드시 넌-블로킹 소켓을 기반으로 read & write 함수를 호출해야 한다.
엣지트리거의 가장 강력한 장점은 데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있다. 엣지 트리거가 좋은 성능을 발휘할 확률이 상대적으로 높다.
foreach(event in events)
{
s = event.socket;
// 위 epoll, add에 들어갔던 값을 얻는다.
userPtr = events.userPtr;
// 수신? 송신?
type = event.type;
if (type == ReceiveEvent)
{
while (true)
{
(result, data) = s.recv();
if (data.length > 0)
{
// 수신된 데이터를 처리한다.
Process(userPtr, s, data);
}
}
if (result == EWOULDBLOCK)
break;
}
}
IOCP
epoll은 논블록 소켓을 대량으로 갖고 있을 때 효율적으로 처리해 주는 API 이다.
Overlapped I/O를 다루는 운영쳊에서 대응한 것이 바로 I/O Completion Port 혹은 IOCP라는 것이다.
IOCP는 소켓의 Overlapped I/O가 완료되면 이를 감지해서 사용자에게 알려 주는 역할을 한다.
사용자는 IOCP에서 I/O가 완료되었음을 알려 주는 완료 신호(complection event)를 꺼낼 수 있다. 소켓 개수가 1만 개라고 하더라도 이 중에서 I/O가 완료된 것들만 IOCP를 이용해서 바로 얻을 수 있다.
iocp = new iocp()
foreach(s in sockets)
{
iocp.add(s, GetUserPtr(s));
s.OverlappedReceive(data[s], receiveOverlapped[s]);
}
events = iocp.wait(100ms);
foreach(event in events)
{
// iocp.add에 들어갔던 값을 얻는다.
userPtr = event.userPtr;
ov = event.overlappedPtr;
s = GetSocketFromUserptr(userPtr);
if (ov == receiveOverlapped[s])
{
// overlapped receive가 성공했으니,
// 받은 데이터를 처리
Process(s, userPtr, data[s]);
// 추가로 I/O를 계속 하고 싶으면 Overlapped I/O를 또 걸면 됨
s.OverlappedReceive(data[s], receiveOverlapped[s])
}
}
epoll 과 IOCP 비교
epoll 과 가장 차이점은 epoll은 I/O 가능인 것을 알려 주지만, IOCP는 I/O 완료인 것을 알려 준다는 것. IOCP는 epoll에서 할 수 없는 유리한 기능 스레드 풀링 활용법이 있다 스레드 몇 개가 골고루 분담해서 하는 스레드 풀을 쉽게 구현 가능. 반면, epoll은 쉽지 않다.
순서를 안다고 해도 두 스레드가 동시에 같은 일을 하므로, 어느 것을 먼저 처리하고 어느 것을 나중에 처리할지 교통 정리를 할 로직이 필요.
어떤 소켓에 대해 Overlapped I/O를 하지 않는 이상 그 소켓에 대한 완료 신호는 전혀 발생하지 않는다. 즉, 소켓 하나에 대한 완료 신호를 스레드 하나만 처리할 수 있게 보장. 이러한 특징 덕분에 IOCP 하나를 여러 스레드가 기다리도록 구현하기 쉬움. 많은 소켓에 대한 I/O 처리를 동시다발적으로 수행할 때, 여러 스레드가 완료 신호 처리를 골고루 나누어서 처리 가능.
스레드 개수만큼 epoll 객체를 둔다. 각 스레드는 자기만의 epoll를 처리 여러 소켓은 이들 epoll 중 하나에만 배정가능.
여러 스레드가 있지만, 한 소켓의 이벤트는 지정된 한 스레드에서만 발생. 그래도 소켓들이 여러 스레드 중 하나에 배정되기 때문에 아에 스레드 풀링이 없는 것보다는 효율이 좋음. 하지만 IOCP 만큼 균형 있게 분담해 주는 효율적인 처리 성능은 떨어짐.
윈도우 서버의 커널 함수 호출이 더 적기 때문에 게임 서버는 한 번 TCP 연결을 맺으면 게임 클라이언트가 나갈 때까지 거의 유지되지만, 웹 프로토콜(HTTP)처럼 메시징을 할 때마다 TCP 연결을 맺는 상황에서는 윈도 서버가 유리함.
[출처] Overlapped I/O, 비동기 I/O, epoll, iocp|작성자 피카츄
'CS > 네트워크' 카테고리의 다른 글
데드 레커닝 (Dead Reckoning) 개념 (0) | 2023.07.02 |
---|---|
스레드 풀 (Thread Pool) (0) | 2022.11.25 |
TCP와 UDP의 특징과 차이 (0) | 2022.11.04 |
C# 원자적 연산 (Interlocked 클래스) (0) | 2022.07.26 |
C++ 원자적 연산 (atomic) (0) | 2022.07.26 |