EPOLL API 소개 및 Echo Chat Server

2014. 1. 20. 21:28Programing/C Language

epoll은 poll의 일종이며 edge trigger 인터페이스 또는 level trigger 인터페이스로서 사용하는 것이 가능하고 감시하는 파일 디스크립터의 수가 많은 경우에도 사용할 수 있다epoll 세트를 설정하거나 제어하거나 하기 위해서 다음의 3개의 시스템 콜이 제공되고 있다.

epoll 세트는 epoll_create 으로 작성되는 파일 디스크립터에 접속된다그리고 특정의 파일 디스크립터에 대한 관심 (역주어떤 이벤트를 감시할까 등)을 epoll_ctl 로 등록한다
마지막에 epoll_wait 로 실제의 이벤트 대기를 개시한다

EPOLL의 핵심 API 함수

int epoll_create(int size);
epoll_create()는 이벤트를 저장하기 위한 size만 큼의 공간을 커널에 요청한다. 
커널에 요청한다고 해서 반드시 size만큼의 공간이 확보되는 건 아니지만 커널이 
대략 어느 정도의 공간을 만들어야 할지는 정해줄 수 있다. 수행된 후 파일 지정자를 되돌려 주는데, 
이 후 모든 관련작업은 리턴된 파일 지정자를 통해서 이루어지게 된다. 모든 작업이 끝났다면 close()를 
호출해서 닫아주어야 한다. 

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
실제 이벤트가 발생하는걸 기다리고 있다가, 이벤트가 발생하면 이벤트 관련 정보를 넘겨주는 일을 한다. 
epfd는 epoll_create(2)를 이용해서 생성된 epoll지정자이다. 만약 이벤트가 발생하면 리턴하게 되는데, 
리턴된 이벤트에 관한 정보는 events에 저장된다. maxevents는 epoll이벤트 풀의 크기다. 
timeout는 기다리는 시간이다. 0보다 작다면 이벤트가 발생할 때까지 기다리고, 0이면 바로 리턴, 0보다 크면 
timeout 밀리세컨드 만큼 기다린다. 만약 timeout시간에 이벤트가 발생하지 않는다면 0을 리턴한다. 


int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
이벤트풀을 제어하기 위해서 사용한다. poll(2)와 매우 비슷하게 작동한다. op는 fd에 대해서 어떤 작업을 할것인지를 
정의하기 위해서 사용된다. op가 실행된 결과는 event구조체에 적용된다. 

다음은 epoll_event구조체의 모습이다.
typedef union epoll_data {      
void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
struct epoll_event { __uint32_t events; /* 발생된 이벤트 */ epoll_data_t data; /* 유저 데이터로 직접 설정가능하다 */ };
epoll_data_t를 유심히 볼필요가 있다. 이것은 유저 데이터가 직접 설정이 가능한데, 여기에서 설정한 값은
epoll_wait를 통해서 넘어오는 epoll_event구조체값으로 그대로 사용할 수 있다.
예를들어 여기에 pid값이라든지 소켓지정번호등을 지정해 놓게되면 나중에 이벤트가 발생했을 때 이벤트가 발생한
파일등에 대한 정보를 쉽게 얻어올 수 잇다.
op는 다음과 같은 종류의 작업명령들을 가지고 있다. poll(2)와 비교해보면 매우 유사함을 알 수 있을 것이다.
  • EPOLL_CTL_ADD
    fd를 epoll 이벤트 풀에 추가하기위해서 사용한다. 
  • EPOLL_CTL_DEL
    fd를 epoll 이벤트 풀에서 제거하기 위해서 사용한다.
  • EPOLL_CTL_MOD
    이미 이벤트 풀에 들어 있는 fd에 대해서 event의 멤버값을 변경하기 위해서 사용한다.
  • EPOLLIN
    입력(read)이벤트에 대해서 검사한다. 
  • EPOLLOUT
    출력(write)이벤트에 대해서 검사한다.
  • EPOLLERR
    파일지정자에 에러가 발생했는지를 검사한다. 
  • EPOLLHUP
    Hang up이 발생했는지 검사한다.
  • EPOLLPRI
    파일지정자에 중요한 데이터가 발생했는지 검사한다.
  • EPOLLET
    파일지정자에 대해서 Edge 트리거 행동을 설정한다. Level 트리거가 기본설정 된다.

동작 순서
1. 파이프의 읽기 측을 나타내는 파일 디스크립터 (RFD가 epoll 디바이스의 내부에 추가된다
2. 파이프에 쓰기를 하는 프로그램이 2Kb 의 데이터를 파이프의 쓰기측에 기입한다
3. epoll_wait 를 호출하면 읽기 가능(ready)인 파일 디스크립터로써 RFD가 돌아간다
4. 파이프로부터 읽어내는 프로그램이 1Kb 의 데이터를 RFD로부터 읽어낸다
5. epoll_wait의 호출을 한다

RFD 파일 디스크립터가 EPOLLET 플래그를 사용해 epoll 추가되고 있으면 이용 가능한 데이터가 파일 입력 버퍼에 아직 존재해 리모트의 접속처(peer)가 이미 보내진 데이터에 근거해 응답을 기대하고 있기 때문에 스텝 5의 epoll_wait 의 호출로 행할 가능성이 있다이것은 edge trigger 이벤트 배송에서는 모니터 하고 있는 파일로 이벤트가 떠났을 때에만 이벤트가 배송되기 때문에 있다상기의 예에서는 2로 행해진 쓰기에 의해 RFD 에 관한 이벤트가 생성되어 3로 이벤트가 소비(consume) 된다4 로 행해지는 읽기 조작에서는 전부의 버퍼 데이터를 소비하지 않기 때문에 스텝 5 에서 행해지는 epoll_wait 의 호출이 무기한으로 잠글지도 모른다EPOLLET 플래그 (edge trigger)와 함께 사용하는 경우 epoll 인터페이스는 블록 하지 않는 파일 디스크립터를 사용해야 하는 것이다이것은 블록 되는 읽기나 기입에 의해 , 복수의 파일 디스크립터를 취급하는 태스크를 굶주림(starve) 시키지 않게 하기 위한 것이다epoll  edge trigger (EPOLLET인터페이스로서 사용하기 위해서 제안되는 방법은 이하와 같고 흔히 있는 함정을 피하는 방법도 계속해 말한다

반대로 level trigger 인터페이스로서 사용하는 경우는 epoll 은 정말로 보다 고속의 poll이며 사용법이 같아서 poll이 사용되고 있는 곳은 어디에서라도 사용할 수가 있다. edge trigger를 사용했을 경우에서도 복수의 데이터를 수신하면 복수의 epoll 이벤트가 생성되므로 호출 측에는EPOLLONESHOT 플래그를 지정하는 옵션이 있다이 플래그는 epoll에 대해서 epoll_wait 에 의한 이벤트를 수신한 다음에 관련하는 파일 디스크립터를 무효로 시킨다EPOLLONESHOT 플래그가 지정되었을 경우 epoll_ctl 에 EPOLL_CTL_MOD 를 지정해 파일 디스크립터를 재차 사용할 수 있도록 하는 것은 호출 측의 책임이다.

참고 - 리눅스 프로그래밍 API REFERENCE

EPOLL을 이용한 간단한 ECHO(채팅Chatting) 서버

사용되는 전역 변수
 /* definition */
#define MAX_CLIENT   10000
#define DEFAULT_PORT 9006
#define MAX_EVENTS   10000

/* global definition */
int g_svr_sockfd;              /* global server socket fd */
int g_svr_port;                /* global server port number */

struct {
         int  cli_sockfd;  /* client socket fds */
         char cli_ip[20];              /* client connection ip */
} g_client[MAX_CLIENT];

int g_epoll_fd;                /* epoll fd */

struct epoll_event g_events[MAX_EVENTS];

사용할 접속자 구조 초기화
 for(i = 0 ; i < MAX_CLIENT ; i++)
  {
     g_client[i].cli_sockfd = -1;
  }

EPOLL 생성 및 초기화
struct epoll_event events;

  g_epoll_fd = epoll_create(MAX_EVENTS);
  if(g_epoll_fd < 0)
  {
     printf("[ETEST] Epoll create Fails.\n");
     close(g_svr_sockfd);
     exit(0);
  }
  printf("[ETEST][START] epoll creation success\n");

  /* event control set */
  events.events = EPOLLIN;
  events.data.fd = g_svr_sockfd;

  /* server events set(read for accept) */
  if( epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, g_svr_sockfd, &events) < 0 ) 
  {
     printf("[ETEST] Epoll control fails.\n");
     close(g_svr_sockfd);
     close(g_epoll_fd);
     exit(0);
  }
  printf("[ETEST][START] epoll events set success for server\n");

서버가 이벤트를 기다리는 로직
  struct sockaddr_in cli_addr;
  int i,nfds;
  int cli_sockfd;
  int cli_len = sizeof(cli_addr);

  nfds = epoll_wait(g_epoll_fd,g_events,MAX_EVENTS,100); /* timeout 100ms */

  if(nfds == 0return/* no event , no work */
  if(nfds < 0)
  {
      printf("[ETEST] epoll wait error\n");
      return/* return but this is epoll wait error */
  }

  for( i = 0 ; i < nfds ; i++ )
  {
      if(g_events[i].data.fd == g_svr_sockfd)
      {
          cli_sockfd = accept(g_svr_sockfd, (struct sockaddr *)&cli_addr,(socklen_t *)&cli_len);
          if(cli_sockfd < 0/* accept error */
          {  
             perror("error : ");
          }
          else
          {
             printf("[ETEST][Accpet] New client connected. fd:%d,ip:%s\n",cli_sockfd,inet_ntoa(cli_addr.sin_addr));
             userpool_add(cli_sockfd,inet_ntoa(cli_addr.sin_addr)); // 이벤트 소켓 리스트에 추가
             epoll_cli_add(cli_sockfd);   // 클라이언트 접속자 정보 구조에 추가
          }
          continue/* next fd */
      }
          /* if not server socket , this socket is for client socket, so we read it */
         client_recv(g_events[i].data.fd);

  } /* end of for 0-nfds */

누군가 접속했을때 처리하는 로직 (ACCEPT)
void epoll_cli_add(int cli_fd)
{

  struct epoll_event events;

  /* event control set for read event */
  events.events = EPOLLIN;
  events.data.fd = cli_fd;

  if( epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, cli_fd, &events) < 0 )
  {
     printf("[ETEST] Epoll control fails.in epoll_cli_add\n");
  }

}

void userpool_add(int cli_fd,char *cli_ip)
{
  /* get empty element */
  register int i;

  for( i = 0 ; i < MAX_CLIENT ; i++ )
  {
     if(g_client[i].cli_sockfd == -1break;
  }
  if( i >= MAX_CLIENT ) close(cli_fd);

  g_client[i].cli_sockfd = cli_fd;
  memset(&g_client[i].cli_ip[0],0,20);
  strcpy(&g_client[i].cli_ip[0],cli_ip);

}

누군가 WRITE를 요청했을시의 READ해서 받는 처리
void client_recv(int event_fd)
{
  char r_buffer[1024]; /* for test.  packet size limit 1K */
  int len;
  /* there need to be more precise code here */
  /* for example , packet check(protocol needed) , real recv size check , etc. */

  /* read from socket */
  len = recv(event_fd,r_buffer,1024,0);
  if( len < 0 || len == 0 )
  {
      userpool_delete(event_fd);
      close(event_fd); /* epoll set fd also deleted automatically by this call as a spec */
      return;
  }
  userpool_send(r_buffer);
}

void userpool_delete(int cli_fd)
{
  register int i;

  for( i = 0 ; i < MAX_CLIENT ; i++)
  {
       if(g_client[i].cli_sockfd == cli_fd) {           
          g_client[i].cli_sockfd = -1;
          break;
       }
  }
}

접속자에게 SEND 전송하는 로직 처리
void userpool_send(char *buffer)
{
  register int i;
  int len;

  len = strlen(buffer);

  for( i = 0 ; i < MAX_CLIENT ; i ++)
  {
      if(g_client[i].cli_sockfd != -1 )
      {
          len = send(g_client[i].cli_sockfd, buffer, len,0);
          /* more precise code needed here */
      }
  }
}