2014. 2. 10. 17:06
들어가기

Socket를 사용하는 방법은 다양하다. 사용하는 플래폼에 따라, 혹은 라이브러리나 프로그래밍 언어에 따라 구현 방법도 다양하다.
여기서는 linux의 socket에서 제공되는 select라는 것을 사용한 구현을 보도록 하겠다.
일단 기본 적인 socket 사용 법에 대해 알고 있다고 생각하고 select를 이용하여 구현하겠다.
코드는 100%완벽하지 않으므로 중간에 문제점이 있을 것 같은 부분은 언급은 하겠지만, 일단 select 자체에 대해 더욱 집중을 하겠다.


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 값이 반환이 된다. 그렇기에 에러 발생을 감지하고 간단한 에러 메시지 출력하고 싶다면 다음과 같이 하면 된다.

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로 설정된 비트 개수를 계수하면 될 것이다.

참조

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

덧글
이상으로 select에 대한 내용를 마치겠습니다. 이 글 내용이 여러분에게 도움이 되었다면 다행이네요. ^^;
내용상 이상하거나 틀린 부분이 있으면 가차 없이 태클을 걸어주기 바랍니다. 백테클은 아프기에 가급적이면 부드러운 테클을 부탁드립니다. ^^ㅋ
모두 즐프하세요~~Ospace.끝.


'OLD - 일 > Linux' 카테고리의 다른 글

Linux - TCP/IP 수정본  (0) 2014.02.13
Linux - 소켓(LInux socket) / 소켓 제한 해제  (0) 2014.02.10
socket - FD 설정  (0) 2014.02.10
socket - PF_INET 와 AF_INET 의 차이  (0) 2014.02.10
Linux - TCP/IP 1차 완료(2월 10일 수정)  (0) 2014.02.05
Posted by 평범한직장인 토끼씨