Epoll의 이해와 활용

select 기반의 IO 멀티플렉싱이 느린 이유

  • select 함수호출 이후에 항상 등장하는 모든 파일 디스크립터를 대상으로 하는 반복문
  • select 함수를 호출할 때마다 인자로 매번 전달해야 하는 관찰대상에 대한 정보들

epoll의 경우는 다음의 장점이 있다.

  • 상태변화의 확인을 위한,전체 파일 디스크립터를 대상으로 하는 반복문이 필요 없다.
  • select함수에 대응하는 epoll_wait 함수호출 시, 관찰대상의 정보를 매번 전달할 필요가 없다.

Epoll의 함수

  • epoll_create: epoll 파일 디스크립터 저장소 생성
  • epoll_ctl: 저장소에 파일 디스크립터 등록 및 삭제
  • epoll_wait: select 함수와 마찬가지로 파일 디스크립터의 변화를 대기한다.

select 방식에서는 관찰대상인 파일 디스크립터의 저장을 위해서 fd_set형 변수를 직접 선언했었다. 하지만 epoll 방식에서는 관찰대상인 파일 디스크립터의 저장을 운영체제가 담당한다. 이때 사용되는 함수가 epoll_create로, 파일 디스크립터의 저장을 위한 저장소의 생성을 운영체제에게 요청한다.

관찰대상인 파일 디스크립터의 추가, 삭제를 위해서 select 방식에서는 FD_SET, FD_CLR 함수를 사용하지만, epoll 방식에서는 epoll_ctl 함수를 통해서 운영체제에게 요청하는 방식으로 이뤄진다. select 방식에서는 파일 디스크립터의 변화를 대기하기 위해서 select함수를 호출하지만, epoll에서는 epoll_wait함수를 호출한다. select 방식에서는 인자로 전달한 fd_set형 변수의 변활르 통해서 관찰대상의 상태볌화를 확인하지만, epoll 방식에서는 구조체 epoll_event를 기반으로 상태변화가 발생한 파일 디스크립터가 별도로 묶인다.

struct epoll_event
{
    __unit32_t events;
    epoll_data_t data;
}

    typedef union epoll_data
    {
        void *ptr;
        int fd;
        __unit32_t u32;
        __unit64_t u64;
    } epoll_data_t;

위의 구조체 epoll_event기반의 배열을 넉넉한 길이로 선언해서 epoll_wait 함수호출 시 인자로 전달하면, 상태변화가 발생한 파일 디스크립터의 정보가 이 배열에 별도로 묶이기 때문에 select 함수에서 보인, 전체 파일 디스크립터를 대상으로 하는 반복문은 불필요하다.

epoll_create

#include <sys/epoll.h>

int epoll_create(int size);
// 성공 시 epoll 파일 디스크립터, 실패 시 -1 반환

epoll_create 함수호출 시 생성되는 파일 디스크립터의 저장소를 가리켜 ‘epoll 인스턴스’라 한다. 매개변수 size를 통해서 전달되는 값은 epoll 인스턴스의 크기를 결정하는 정보로 사용된다. 하지만 이 값은 단지 운영체제에 전달하는 힌트에 지나지 않는다. 즉, 인자로 전달된 크기의 epoll 인스턴스가 생성되는 것이 아니라, epoll 인스턴스의 크기를 결정하는데 있어서 참고로만 사용된다.

리눅스 커널 2.6.8 이후부터 epoll_create 함수의 매개변수 size는 완전히 무시된다.

epoll_create함수호출에 의해서 생성되는 리소스는 소켓과 마찬가지로 운영체제에 의해서 관리된다. 이 함수는 소켓이 생성될 와 마찬가지로 파일 디스크립터를 반환한다. 즉, 이 함수가 반환하는 파일 디스크립터는 epoll 인스턴스를 구분하는 목적으로 사용이 되며, 소멸 시에는 다른 파일 디스크립터들과 마찬가지로 close 함수호출을 통한 종료의 과정을 거칠 필요가 있다.

epoll_ctl

epoll 인스턴스 생성 후에는 이곳에 관찰대상이 되는 파일 디스크립터를 등록해야 하는데, 이 때 사용하는 함수가 epoll_ctl이다.

#include <sys/epoll.h때

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 성공 시 0, 실패 시 -1 반환
  • epfd: 관찰대상을 등록할 epoll 인스턴스의 파일 디스크립터
  • op: 관찰대상의 추가, 삭제 또는 변경여부 지정.
  • fd: 등록할 관찰대상의 파일 디스크립터
  • event: 관찰대상의 관찰 이벤트 유형

op에 전달 가능한 상수 EPOLL_CTL_ADD: 파일 디스크립터를 epoll 인스턴스에 등록한다. EPOLL_CTL_DEL: 파일 디스크립터를 epoll 인스턴스에서 삭제한다. EPOLL_CTL_MOD: 등록된 파일 디스크립터의 이벤트 발생상황을 변경한다.

epoll_event는 상태변화가 발생한 파일 디스크립터를 묶는 용도 외에도, 감지할 이벤트를 지정하는 역할도 한다.

아래는 epoll_event의 멤버인 events에 저장 가능한 상수이다.

  • EPOLLIN: 수신할 데이터가 존재하는 상황
  • EPOLLOUT: 출력버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
  • EPOLLPRI: OOB데이터가 수신된 상황
  • EPOLLRDHUP: 연결이 종료되거나 Half-close가 진행된 상황, 이는 엣지 트리거 방식에서 유용하게 사용될 수 있다.
  • EPOLLERR: 에러가 발생한 상황
  • EPOLLET: 이벤트의 감지를 엣지 트리거 방식으로 동작시킨다.
  • EPOLLONESHOT: 이벤트가 한번 감지되면, 해당 파일 디스크립터에서는 더 이상 이벤트를 발생시키지 않는다. 따라서 epoll_ctl 함수의 두 번째 인자로 EPOLL_CTL_MOD를 전달해서 이벤트를 재설정해야 한다.
event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

epoll_wait

이 함수가 epoll관련 함수 중에서 가장 마지막에 호출된다.

#include <sys/epoll.h>

int epoll_wait(int dpfd, struct epoll_evnet * events, int maxevents, int timeout);
// 성공 시 이벤트가 발생한 파일 디스크립터의 수, 실패 시 -1 반환
  • epfd: 이벤트 발생의 관찰영역인 epoll 인스턴스의 파일 디스크립터
  • events: 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소 값
  • maxevents: 두 번째 인자로 전달된 주소 값의 버퍼에 등록 가능한 최대 이벤트 수
  • timeout: 1/1000초 단위의 대기시간, -1 전달 시, 이벤트가 발생할 때까지 무한 대기

예제 코드

int event_cnt;
struct epoll_event *ep_events;
. . . . .
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE); // EPOLL_SIZE는 매크로 상수 값
. . . . .
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
. . . . .

epoll 기반의 에코 서버

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];
    
    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;
    
    if(argc != 2)
    {
        printf("USAGE: %s <port>\n", argv[0]);
        exit(1);
    }
    
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
    
    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    
    epfd = epoll_create(EPOLL_SIZE);
    ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    
    event.events = EPOLLIN;
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
    
    while(1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if(event_cnt == -1)
        {
            puts("epoll_wait() error");
            break;
        }
        
        for(i = 0 ; i < event_cnt, i++)
        {
            if(ep_events[i].data.fd == serv_sock)
            {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                event.events = EPOLLIN;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client: %d \n", clnt_sock);
            }
            else
            {
                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if(str_len == 0) // close request!
                {
                    epoll_ctl(dpfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                    close(ep_events[i].data.fd);
                    printf("closed client: %d n", ep_events[i].data.fd);
                }
                else
                {
                    write(ep_events[i].data.fd, buf, str_len); // echo
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}

void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

fctl 사용 논블로킹 epoll

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
 
const unsigned short PORT = 32452;

void set_nonblocking( int sock );
static int doEcho(int clientSock);

int main(void) 
{ 
  int serviceSock = socket(PF_INET, SOCK_STREAM, 0);
  if(serviceSock < 0) 
  {
      perror("fail create socket");
      printf("errorno : %d\n", errno);
      return 1;
  }
 
  struct sockaddr_in servicAddr;
  memset(&servicAddr, 0x0, sizeof(servicAddr));
  servicAddr.sin_family = AF_INET;
  servicAddr.sin_port   = htons(PORT);
  servicAddr.sin_addr.s_addr   = INADDR_ANY;
  
  if(bind(serviceSock, (struct sockaddr *)&servicAddr, sizeof(servicAddr)) < 0) 
  {
     perror("fail bind");
     printf("errorno : %d\n", errno);
     return 1;
  }
 
  if(listen(serviceSock, 10) < 0) 
  {
     perror("fail listen");
     printf("errorno : %d\n", errno);
     return 1;
  }
 
  // 이벤트 감시 대상으로서 접속 대기 소켓을 등록 
  int eventChkFd = epoll_create(10);
  if(eventChkFd < 0) 
  {
     perror("fail epoll_create");
     printf("errorno : %d\n", errno);
     return 1;
  }
   
  struct epoll_event serverEv;
  memset(&serverEv, 0, sizeof(serverEv));
  serverEv.data.fd = serviceSock;
  serverEv.events = EPOLLIN;

  if(epoll_ctl(eventChkFd, EPOLL_CTL_ADD, serviceSock, &serverEv) < 0) 
  {
     perror("fail epoll_ctl");
     printf("errorno : %d\n", errno);
     return 1;
  }
 
  
  struct epoll_event events[10];
  while(true) 
  { 
     int eventCnt = epoll_wait(eventChkFd, events, 10, -1);
     if(eventCnt < 0) 
     {
        perror("fail epoll_wait");
        printf("errorno : %d\n", errno);
        return 1;
     }

     for(int i = 0; i < eventCnt; i++) 
     { 
        if(events[i].data.fd == serviceSock) 
        {
           /* 신규 접속의 경우는 감시 대상으로 등록 */
           struct sockaddr srcaddr;
           int addrLen;

           int clientSock = accept4(serviceSock, &srcaddr, &addrLen, SOCK_NONBLOCK);
           if(clientSock < 0) 
           {
              perror("fail accept");
              printf("errorno : %d\n", errno);
              continue;
           }
           
           // 소켓을 논블럭킹 모드로 설정 
           set_nonblocking( clientSock );
          
           
           struct epoll_event ev;
           memset(&ev, 0x0, sizeof(ev));
           ev.events = EPOLLIN | EPOLLET;
           ev.data.fd = clientSock;
           
           if(epoll_ctl(eventChkFd, EPOLL_CTL_ADD, clientSock, &ev) < 0) 
           {
               perror("fail epoll_ctli client");
               printf("errorno : %d\n", errno);
               return 1;
           }
        } 
        else 
        {
           if(doEcho(events[i].data.fd) < 0) 
           {
              close(events[i].data.fd);
           }
        }
    } // for
  } // while

  return 0;
} // main


void set_nonblocking( int sock ) 
{
    int flags = fcntl( sock, F_GETFL );
    fcntl( sock, F_SETFL, flags | O_NONBLOCK );
}

int doEcho(int clientSock) 
{
 
  char buf[256] = {0,};
  
  while(true) 
  {
    int receiveMsgSize = recv(clientSock, buf, sizeof(buf), 0);
    
    if(receiveMsgSize == 0) 
    {
        return -1;
    } 
    else if(receiveMsgSize < 0) 
    {
      if(errno == EAGAIN) 
      {
        break;
      } 
      
      return -1;
    }
 
    if(send(clientSock, buf, receiveMsgSize, 0) < 0) 
    {
        return -1;
    }
  }

  return 0;
}