본문 바로가기

4.개발 및 운영 환경

Socket에서 select사용하기

Socket를 사용하는 방법은 다양합니다. 그중에 비동기를 사용한 방법이 있습니다. 대표적인 것이 linux의 select나 poll 등이 있습니다. 여기서는 select라는 것을 사용한 구현을 보도록 하겠습니다. 물론 poll이나 다른 방식들도 있지만, 기본적인 개념을 이해하기에는 select가 충분하다고 봅니다.

일단 기본 적인 socket 사용 법에 대해 알고 있다고 생각하고 select를 이용하여 구현하겠습다.

코드는 100%완벽하지 않으므로 중간에 문제점이 있을 것 같은 부분은 언급은 하겠지만, 일단 select 자체에 대해 더욱 집중을 해서 설명하도록 하겠습니다.

작성:http://ospace.tistory.com/(ospace114@empal.com) 2009.01.15

Select 살펴보기

Select는 라이브러리 함수일 뿐입니다. 이 함수를 사용하여 소켓 통신 방법을 구현하는 것이 기존 방법과 다릅니다. 보통 socket 프로그래밍을 하다보면 블럭킹와 넌블럭킹 모드를 말하는데, 전자는 전송한후에 응답을 수신해야 다음 작업을 하고, 후자는 전송 직후 바로 다른 작업을 할 수 있다는 입니다. 즉, 넌블록킹은 명령 종료할때까지 기다리는 것이 아니라 바로 빠져나오는 것입니다.

구현 적인 측면에서는 전자는 쓰레드 형태로 작동이 되며, 후자는 이벤트 드리븐 방식으로 된다고 보면 무난합니다. 물론 꼭 그렇게 하는 것은 아닙니다.

일반적인 select 시그니처

int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds,
              struct timeval *restrict timeout);
  • nfds 인자: 변경 테스트할 디스크립터 개수. 0~(nfds-1) 범위을 테스트 한다.
  • readfds 인자: null포인트가 아니면 입력이 있음을 표시하는 fd_set 값.
  • writefds 인자: null포인트가 아니면 출력이 있음을 표시하는 fd_set값
  • errorfds 인자: null포인터가 아니면 에러가 있음을 표시하는 fd_set값
  • timeout 인자: null이면 변화가 있을 때까지 계속 대기(블럭킹), 아니면 일정 시간 대기후 시간 초과 리턴됨.

위의 함수를 호출 하기 위해서는 다음 include 파일을 포함시켜야 합니다.

#include <sys/select.h>

에러 처리는 에러 값은 errno에 저장되며, 실패시 -1 값이 반환이 되며 errno로 에러를 판단합니다. 그렇기에 에러 발생을 감지하고 간단한 에러 메시지 출력하고 싶다면 다음과 같이 하면 됩니다.

if(select(...) < 0) {
perror("select"); // 에러 출력
... // 에러 처리
}

이상으로 select 함수를 살펴보았다. 그럼 함수 시그니처에서 fd_set에 대해 살펴보자.

fd_set은 무엇

먼저 fd_set는 구조체로서 "sys/types.h"에 정의 되어 있습니다. 이 부분은 select할 때 동시 테스트할 디스크립터를 표시하는 자료형입니다. 실제 내부구조는 단순 자료형으로 구성되어 있습다. 사용할 디스크립터 개수에 따라 해당 자료형도 달라집니다. 만약 기본 설정으로 사용한다면 64bit인 long형으로 되어 있을 것입니다.

디스크립터는 File descriptor(이후 fd)를 말하며, 단순히 파일만이 아닌 소켓, 파이프 등 다양하게 적용됩니다.

fd_set는 64bit long형은 총 64개 fd를 관리할 수 있습니다. 즉, 한 비트가 해당 fd를 테스트 결과를 저장합니다. 그리고 비트 순번이 해당 fd 값이라고 보변 됩니다.

fd_set에서 0 값은 아무런 변화가 없습니다. 즉 관심대상이 아닙니다. 1 값이 있으면 관심대상으로 뭔가 작업을 해야한다라는 의미입니다.

그럼 64개 아닌 다른 값을 사용하고 싶다면, FD_SETSIZE를 정의해주면 됩니다. 예를 들어 128개를 사용하고 싶다면,

#define FD_SETSIZE 128

앞에서 fd_set를 비트 단위로 테스트한다고 했는데, 비트 단위를 위해서 일일히 시프트 연산을 사용하면서 계산하기 힘듭니다. 그래서 select.h에서는 다음과 같은 fd_set연산관련 매크로를 제공하고 있습니다.

void FD_ZERO(fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
  • FD_ZERO는 fdset에 있는 모든 값을 0으로 초기화합니다. 변수를 선언하면 초기화하는 것은 당연합니다. 처음 fd_set를 선언후 반드시 호출해야 합니다.
  • FD_CLR는 fdset의 값에서 fd에 해당 하는 비트(0부터 시작하기에 실제는 fd-1비트)를 0으로 클리어합니다.
  • FD_SET는 fdset의 값에서 fd에 해당 하는 비트(0부터 시작하기에 실제는 fd-1비트)를 1으로 설정합니다.
  • FD_ISSET는 fdset의 값에서 fd에 해당 하는 비트(0부터 시작하기에 실제는 fd-1비트)가 1로 설정되었는지 확인합니다. 즉, 1로 설정되었다면 1를 반환, 0으로 설정되었다면 0으로 반환합니다.

Select 작동

Select에 대해 간단한 작동을 보고 간단한 예제를 다음에 보도록 하겠습니다.

실제 select 코드를 보고 서술한 것이 아니라서 실제 구현과 설명이 다룰 수 있음을 알아 두기 바랍니다. 단지, 개인관점에서 가장 적합한 구현 관점에서 서술하였습니다.

사용 법은 다음과 같습니다.

  1. fd_set에 테스트할 fd 비트를 설정합니다.
  2. select에서 fd_set중에서 1로 설정된 fd만 테스트 합니다.
  3. 테스트 결과를 다시 fd_set에 기록합니다.
  4. 테스트 결과 중에 1로 설정된 fd에 대해서 처리합니다.

이를 그림으로 간단히 그리면 다음과 같습니다.

fd_set이 select에 입력되면, 그 결과를 해당 fd_set에 저장되며, 입력 fd_set중에 1로 설정된 fd 비트만 테스트 하며, 0은 테스트 없이 패스됩니다.

간혹 예를 보다보면 출력된 fd_set를 바로 입력으로 사용되는 경우가 있습니다. 그러면 관심 갖는 fd 종류가 달라지기 때문에 원하는 결과가 나오지 않을 수 있습니다.

그리고 select에 timeout값을 설정할 경우, 일단 처리후 timeout이 발생하면 관심 대상이었던 fd들의 비트값이 모두 0으로 바뀝니다. 즉, 해당 시간안에 테스트 결과가 아무런 변화가 없다라는 의미입니다.

그렇기에 select 처리 전에 fd_set 재설정에 의해서 관심 fd의 해당 비트를 설정하는게 좋습니다. 아니면 다른 자료형 변수에 저장해두었다가 적용해도 상관없습니다.

간단한 예제

이번 예제는 소켓을 사용하지 않고 select만 사용한 예제이므로 실제 작동이 되지 않습니다.

코드를 보기 전에 다음과 같은 가정을 하겠습니다.

  • 사용할 fd개수가 다르지만, 기본 FD_SETSIZE를 사용
  • 관심 대상이 되는 fd는 3개: 0, 1, 3
  • select 실행 중에 fd 1에 변경이 발생

간단한 예제를 살펴보겠습니다.

int i = 0;
int max_set = 5; // 최대 fd_set에서 fd 개수. fd 최대 값이 3이기에 총 4개 fd(0, 1, 2, 3)에 대한 값.
fd_set readsets; // select에서 처리해서 결과가 저장될 부분
fd_set readsets_orig; // 관심 대상이 될 fd를 등록 관리. readsets으로 설정이된다.

FD_ZERO ( &readsets );
FD_ZERO ( &readsets_orig );

FD_SET ( 0, &readsets_orig );
FD_SET ( 1, &readsets_orig );
FD_SET ( 3, &readsets_orig );

while ( 1 ) {
// readsets 재설정
    for ( i = 0; i < max_set; ++i ) {
        if ( FD_ISSET ( i, &readsets_orig ) ) {
            FD_SET ( i, &readsets );
        } else {
            FD_CLR ( i, &readsets );
        }
    }

    if ( select ( max_set, &readsets, 0, 0, 0 ) < 0 ) {
        ...// 에러 처리
    }

    for ( i = 0; i < max_set; ++i ) {
        if ( FD_ISSET ( i, &readsets ) {
        if ( i == 0 ) {
                ...// fd 0 변경될 경우 처리
            } else
                if ( i == 1 ) {
                    ...// fd 1 변경될 경우 처리
                } else
                    if ( i == 3 ) {
                        if ( fd 처리 종료 ) {
                            FD_CLR ( i, &readsets_orig ); // 원래 관심 fd 목록에서 클리어
                            ... // 종료 처리
                        }
                    }
        }
    }
}

여기서 중요한 것은 처음 fd_set이 readsets과 readsets_orig 두개가 사용되는 것입니다. readsets_orig가 관심대상이 되는 fd에 대한 셋을 저장관리합니다. 즉, 관심대상이 되는 fd는 readsets_orig에 저장해야 적용이 됩니다.

readsets는 readsets_orig에서 값을 가져옵니다.

if( i==3 ) 구분에서 fd처리가 끝나서 종료 된다면, readsets_orig에서도 해당 fd를 클리어해줘야 합니다. 그래야 select에서 해당 fd를 테스트하지 않습니다.

타임 아웃 설정

타임 아웃 값은 항상 select전에 초기화되어야 합니다. select를 거치면 타임 아웃 값이 변경이 되기에 그대로 계속 사용하게 되면, 원하는 타임 아웃이 작동하지 않습니다. 반드시 select전에 새로 초기화 시켜야 합니다.

struct timeval sel_timout;

//... 반복 순환문
sel_timeout.tv_sec = 3;
sel_timeout.tv_usec = 0;

if(select(max_set, &readsets, 0, 0, &sel_timeout) < 0) {
...// 에러 처리
}

해당 타임 아웃 시간 내에 fd 테스트가 종료 되었다면, 이는 fd 변경이 없다는 뜻입니다. 그러므로 fd_set에 있는 모든 fd의 비트는 0으로 설정됩니다.

결론

이상으로 간단하게 select문을 살펴보았습니다. 인터넷에서도 설명이 부족하고, 코드도 제대로 되어 있지 않아서 실제 사용하는데 시간이 많이 걸렸습니다. 계속 테스트 하면서 여러 샘플을 만들어서 시험해보면서 얻은 결과입니다.

select는 가장 큰 문제점은 필요없는 fd에 대한 처리도 포함되어 있고 불필요한 루프가 반복된다는 것입니다. 사실 앞에 예제에서는 fd 2에 대한 테스트는 없지만 그래도 fd 2에 대한 검사는 해야될 것입니다. 이를 최소화 하려면, 관심대상이되는 fd만을 일정 공간에 저장해두었다가, 이를 사용하는 것입니다.

앞에 예제를 간단히 응용하면 다음과 같습니다.

if ( FD_ISSET ( i, &readsets_orig ) ) {
    if ( FD_ISSET ( i, &readsets ) ) {
        if ( i == 0 ) {
            ...
        }
        ...
    }
}

그럼 또 중요 한 것이 테스트할 fd 개수입니다.

아니면, 앞에 처음 예제에서는 그냥 최대 값이 고정되어 있습니다. fd 3번이 종료되면 fd 3에 대한 테스트가 없기에 max_set는 5에서 3으로 변경됩니다. 이는 중간에 fd 2도 없기때문입니다.

이런 계산은 어떻게 할까? 뭐 알아서 해야겠지.. 간단하게 생각하면 read_sets 재설정시 최대 fd 값을 측정해두 었다가 fd_sets에 2을 더해 주면 됩니다. 2를 더해주는 이유는 0부터 fd_set이 시작되며, select에서 비교할 fd개수보다 1이 커야되기에 2를 더해줘야 합니다. 그러면 총 비교되는 fd 개수는 (max_set - 1)개가 됩니다.

위의 예제로 관심 대상 fd에 대해서만 비교한다면 변경된 fd 겁색 수는 실제 관심 fd 개수일 것입니다. 그러나 select의 테스트 fd개수는 이전 처럼 똑같이 사용할 수 밖에 없습니다. 실제 관심 fd개수는 어떻게 유지하면 될까?

이것도 간단히 생각하면 매번 fd가 열려질때마다 개수를 증가하고, 닫히면 감소시킵니다. 아니면 readsets 재설정시 1로 설정된 비트 개수를 계수하면 될 것입니다.

참조

[1] linux man page (man 3 select)
[2] 멀티플렉싱 SELECT 함수 사용, http://ngi.kyungnam.ac.kr/file/EM2/lec7.ppt?PHPSESSID=2a038da589760ca935307bc0afb0a314

덧글

이상으로 select에 대한 내용를 마치겠습니다. 이 글 내용이 여러분에게 도움이 되었다면 다행이네요. ^^;

내용상 이상하거나 틀린 부분이 있으면 가차 없이 태클을 걸어주기 바랍니다. 백테클은 아프기에 가급적이면 부드러운 테클을 부탁드립니다. ^^ㅋ

모두 즐프하세요~~Ospace.끝.

History

2009.01.15 Ospace 신규 작성

반응형

'4.개발 및 운영 환경' 카테고리의 다른 글

데비안에 나눔글꼴 적용  (0) 2009.03.24
Cygwin에서 resolv 라이브러리  (0) 2009.03.24
Bash shell coloring  (0) 2009.01.05
[CUnit] C에서 Unit test 하기  (0) 2008.12.31
도스창 팁: Command Prompt Tip  (0) 2008.12.24