반응형
728x90
반응형

 이전 포스팅 때, 두 개념을 언급했다.

Level-Trigger와 Edge Trigger의 차이를 그냥 Epoll 함수 공부를 할 때에는 잘 몰랐는데, 아래와 같은 상황에서 다시 생각해보게 되었다.

select 로 넘어온 디스크립터 변화의 갯수만큼 처리를 하지 않고 그냥 넘어가면 어떻게 될까?

라는 의문에서 부터 시작 되었다. 

Level Trigger 란? 

아래부터는 윤성우, 『열혈 tcp/ip 소켓 프로그래밍』, 오렌지미디어(2009), p376-p377. 이렇게 인용이 됩니다

윤성우 선생님/교수님의 책에 잘 정리가 되어 있어서, 비유 예시를 인용합니다. 내용을 좀 바꾸려다가 인용하는거 그냥 쓸께유 ㅋㅋ

아들 : 엄마 세뱃돈으로 5,000원 받았어요.
엄마 : 훌륭하구나

아들 : 엄마 옆집 숙희가 떡볶이 사달래서 사줫더니 2,000원 남았어요.
엄마 : 장하다 우리 아들 ~

아들 : 엄마 변신가면 샀더니 500원 남았어요.
엄마 : 그래 용돈 다 쓰면 굶으면 된다!

아들 : 엄마 여전히 500원 갖고 있어요. 굶을 수 없잖아요
엄마 : 그래 매우 현명하구나!

아들 : 엄마 여전히 500원 갖고 있어요. 끝까지 지켜야지요.
엄마 : 그래 힘내거라!

레벨 트리거의 핵심은 굵을 글씨이다.

5,000원이 들어온 이후에는 더 이상 돈이 발생하거나 더해지지 않았다.
>> 5,000원 어치의 이벤트 변화가 발생했다라고 생각하면 된다.

500원에서 계속 변화가 없음에도 불구하고 500원이 있다고 알란다.
>> 파일 디스크립터의 변화가 없더라도 처음에 들어온 것에 비해 남아있기만 한다면 계속해서 이벤트가 등록된다.

이러한 동작 방식이 레벨 트리거 방식이다. 

select 로 넘어온 디스크립터 변화의 갯수만큼 처리를 하지 않고 그냥 넘어가면 어떻게 될까?

그러므로 위와 같은 의문은 잘 해결된다. 등록된 이벤트만큼 처리하지 않더라도 이벤트가 다시 알려지기 때문에 당장 바로 처리하지 않아도 해당 이벤트가 무시되는 것이 아니라, 시간이 흘러감에 따라 처리할 수 있다는 뜻이다. 

그렇다면 엣지 트리거 방식은 무엇일까?

 Edge Trigger 란?

아래부터는 윤성우, 『열혈 tcp/ip 소켓 프로그래밍』, 오렌지미디어(2009), p376-p377. 이렇게 인용이 됩니다

아들 : 엄마 세뱃돈으로 5,000원 받았어요.
엄마 : 음 다음엔 더 노력하거라.(근데 과연 무슨 노력을 해야하는 것일까...? ㅋㅋㅋㅋ)

아들 : ..........................................
엄마 : 말 좀 해라! 그 돈 어쨌냐? 계속 말 안 할거냐?

딱 한 번 5,000원이 들어왔을 때, 이벤트가 등록되고, 그 이후에는 아무리 계속 남아있다고 해도 이벤트가 추가로 등록되지 않는다.

이런 차이가 있다.

다음 번에는 Select 모델의 한계에 대해서 포스팅 할 예정이다.

728x90
반응형
728x90
반응형
 

C기반 I/O Multiplexing - 2. Select 함수 사용하기 전에 컨셉을 보자

C기반 I/O Multiplexing - 1. Multiprocessing과 Multiplexing 비유 최근, 아니 비교적 최근에 어떤 게임 클라이언트를 활용한 게임 서버를 만드는 프로젝트를 진행했던 적이 있고, 지금도 조금씩 조금씩 하고

typingdog.tistory.com

여기서 개념과 컨셉을 확인했다.

코드 분석도 중요하다. 왜냐하면 코드에서 드러나는 select 기반 모델의 특징들이 있기 때문이다.

먼저, 필요로 하는 변수들이다.

코드는 중요한 변수만 넣었고, 그림에는 주석과 내가 작성한 그대로 캡춰한 것이니 함께 보면 좋을 것 같다.

struct timeval timeout; // select 함수의 시간을 지정.
fd_set fd_pool; // 비트 배열이라고 생각.

 

다음으로는 초기화 과정이다

FD_ZERO(&this->fd_pool);
FD_SET(this->serv_sock, &this->fd_pool); 
 // this->serv_sock = socket(PF_INET, SOCK_STREAM, 0); 에서의 리턴 값이다. 
this->fd_max = this->serv_sock; //초기화 하는 시점을 기준으로 가장 나중에 생성된 파일 디스크립터.

 

이어서, 접속 요청이나 데이터 수신을 위해 select 함수를 호출하는 부분이다

// select 함수 호출 시, 값이 리셋되므로 this->fd_pool을 바로 사용하지 않고 임시 변수 fd_temp_pool로 사용한다.
fd_set fd_temp_pool = this->fd_pool; 

// 타임 설정
this->timeout.tv_sec = select_sec;
this->timeout.tv_usec = select_usec;

this->fd_num = select(this->fd_max + 1, &fd_temp_pool, 0, 0, &this->timeout);
// fd_max+1인 이유는 파일 디스크립터는 0부터 시작하며 생성될 때마다 1씩 증가한다. 
// 그러므로 생성된 파일 디스크립터 중 최대값에 +1을 하면 그 갯수가 나온다.

fd_temp_pool에 대입하는 부분이 중요하다. select 함수가 호출하면 변화되는 부분을 표현할 때, 인자로 넘어온 fd_set 주소 값을 이용하여 변화된 파일 디스크립터 외에 나머지에 해당하는 부분은 값을 탈락시킨다.

그렇기 때문에 등록해놓은 파일 디스크립터 정보들이 탈락할 수 있는, 원본이 바뀌는 구조이기 때문에 꼭 임시 변수에 최신화된 파일 디스크립터 정보들을 넣어주고, 임시 변수를 통해서 select 함수로의 인자 전달이 이루어져야 한다.

다음은 select 함수 호출로 받아온 리턴 값별 처리이다.

--

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
if (this->fd_num > 0 ) // >0:성공,
    if (FD_ISSET(this->serv_sock, &fd_temp_pool)) 
    {    // fd_pool에 serv_sock에 대한 변화가 존재한다면 양수 반환 -> 서버 소켓의 변화 -> 접속 의미.
        AcceptConnectionRequest(this->serv_sock);
        this->PrintConsoleNowUser();
    }
    else // 기존 접속된 클라이언트의 수신 및 종료
    {
        int r_var;
        for (int i = 0; i < this->now_user_count; i++// 이렇게 순회를 도는 이유는 다음에 나오는 this->client[i] 때문에.
        {
            if (FD_ISSET(this->client[i], &(fd_temp_pool)))
            {
                r_var = this->ReceiveData(this->client[i]);
                if (r_var == Server::SERVER__RETURNVAR__DISCONNECT_USER) {
                    this->Disconnect(this->client[i]);
                    this->PrintConsoleNowUser();
                    break// 동시에 여러 디스크립터들이 변화할 수 있는가를 확인해 본후 주석
                 } else {
                    if (this->HandlePacketMessage(this->client[i]) == Server::SERVER__RETURNVAR__TERMINATE_BY_ADMIN)
                        return Server::SERVER__RETURNVAR__TERMINATE_BY_ADMIN;
                    break// 동시에 여러 디스크립터들이 변화할 수 있는가를 확인해 본후 주석
                }
            }
        }
    }
else if (this->fd_num == 0// 0:timeout,
    return 0;
else // -1:실패
    HandleStatus("SERVER"true"Select() Error\n");
cs

fd_num이라는 이름의 정수형 변수로 리턴 값을 받는다. 이 때 넘어온 리턴 값에 따라 처리를 하는데, 일단 fd_num이 0보다 크다는 소리는 등록된 파일 디스크립터들 중에서 수신 관련하여 변화가 그 수 만큼 있었다는 소리이다.

아니, 그러면! select 한 번 호출 뒤 반환 값이 5가 나오면 5개의 변화가 있었다는 소리이고, 그러면 다섯 번을 처리해줘야 하는데, 현재 조건문 구조로는 다섯 번을 절대 처리하지 않는다. 이게 도대체 무슨 이유일까? 아니 무슨 말을 하고 있는지 이해가 가는가?

위 그림과 같다. 현재 5개의 파일 디스크립터에 변화가 발생했다.( 1번의 접속 요청 변화 , 4번의 수신 요청 변화 )

2번 줄을 보면 serv_sock의 변화가 생겼다. serv_sock에 수신할 데이터가 있다는 소리고 listen 소켓에서 수신할 데이터가 있다는 소리는 접속 처리를 한다는 소리이다. 근데 2번과 7번이 if ~ else 구조이기 때문에 if문에서 한 번 처리해버리면 다른 처리는 그냥 넘어가게 된다. 무시된다는 소리다.

하지만 이는 select 모델의 레벨 트리거적인 특성으로 인해 처리되지 않은 이벤트가 다음 select 함수 호출 때, 다시 드러나게 되고 이를 처리하게 된다. 

아니 그러면 이벤트가 발생했을 때 다 처리할 수 있게끔 하면 되지 않는가? -- 이건 그냥 코드 작성 스타일이다. 이벤트가 전부 감지 되었을 때 다 처리를 하려거든 매 select 마다 많은 루프를 돌려야 한다. 아래와 같이 말이다.

for( i < fd_num ) // 모든 접속자 혹은 리스너 파일 디스크립터들에 대해서 
{
    if(FD_ISSET(i, fd_temp_pool)) // 변화가 있었는지 검사를 하고 다녀야 한다.
    { 
        ...
    }
}

 

뭐 아무튼 본론으로 돌아와서, 위 코드에서 2번 줄의 접속 요청 수신 관련 listen 소켓에서의 변화가 일어나지 않았다면 else 구문인 7번 줄에 들어가고 이 경우는 데이터 수신에 관련된 경우이다. 

그러니까 파일 디스크립터의 변화가 있긴 있었는데 (1번 줄) 접속 요청은 아니고(2번줄), 수신에 관련된 파일 디스크립터의 변화가 있었던 것이다. 이 경우에는 어떤 파일 디스크립터에서 변화가 생겼는지 알아보기 위해 순회한다. 12번 줄을 통해 처리한다.

그리고 27번 줄에서는 select 반환 값이 0인 경우로, select에 지정된 시간 동안 파일 디스크립터들에서 아무 변화가 없었던 케이스가 된다. 뭐 이 경우에는 다시 select 함수를 호출하면 된다.

좀 더 세분화 했을 때, 접속 요청에 따른 수락 시 클라이언트 파일 디스크립터의 등록이다.

좀 더 세분화 했을 때, 변화가 발생한 클라이언트 파일 디스크립터로 부터 수신처리이다

접속한 클라이언트가 종료할 경우를 확인한다

FD_CLR(disconnect_target_fd, &this->fd_pool);
close(disconnect_target_fd);
this->now_user_count--;

종료하려는 클라이언트의 파일 디스크립터를 당연히 원본 fd_set의 배열에서 빼줘야한다. 종료하는데 지니고 있어서 뭐 하는가 ㅎㅎ..

대충 큰 처리 방법들은 한 번씩 다 훑어봤다.

다음은 간단하게 Level Trigger 와 Edge Trigger에 대해서 정말 간단하게 정리해보고자한다.

728x90
반응형

+ Recent posts