4 분 소요

1. Epoll이란?

  • Epoll
    • 리눅스에서 select의 단점을 보완하여 사용할 수 있는 함수
    • 전체 fd에 대해 반복문을 사용하지 않음
    • fd를 사용자가 아닌 커널이 관리
      • 따라서 CPU는 계속해서 파일 디스크립터의 상태 변화를 감시할 필요가 없음
      • fd에 대한 정보를 담은 저장소를 os에서 담당
      • FD_SET을 순회하는 문제가 없어짐
  • select (remind)
    • 다중 입출력으로 프로그램에서 여러 개의 파일을 작업하고자 할 때 사용하는 함수
    • FD_SET 구조체를 사용하여 유저에게 파일 디스크립터의 상황을 알려줌
    • 문제점
      • fd 개수가 1024개로 제한
      • 모든 fd를 순회하면서 체크하기 때문에 실제 delta만 체크하는 것이 아님
      • FD_SET을 계속해서 select를 통해 os에 전달하는 것도 큰 부하

2. Epoll 함수

2.1. epoll_fd를 만들어주는 함수

#incude <sys/epoll.h>
int epoll_create(int size); //size는 epoll_fd의 크기정보를 전달
                            //return 실패 시 -1, 성공 시 epoll_fd의 값

2.2. 관찰 대상이 되는 fd들을 등록/수정/삭제 하는데 사용되는 함수

int epoll_ctl(int epoll_fd,             //epoll_fd
              int operate_enum,         //어떤 변경을 할지 결정하는 enum값
              int enroll_fd,            //등록할 fd
              struct epoll_event* event //관찰 대상의 관찰 이벤트 유형
              ); 
  • operate_enum
    • EPOLL_CTL_ADD : fd를 epfd의 관심 목록에 추가, 이미 목록에 존재한다면 EEXIST에러를 발생, event 집합은 *event에 저장
    • EPOLL_CTL_DEL : *event에 지정된 정보를 이용해 fd 설정 변경, 관심 목록에 없는 fd라면 ENOENT 에러를 발생
    • EPOLL_CTL_MOD : epfd 에서 fd를 제공, epfd 관심 목록에 없는 fd를 제거하려면 ENOENT 에러를 발생한다. fd를 닫으면 epoll관심 목록에서 자동 제거

2.3. epoll_event 구조체

struct epoll_event
{
  __uint32_t events;
  epoll_data_t data;
}

typedef epoll_data
{
  void* ptr;
  int fd;
  __uint32_t u32;
  __uint64_t u64;
}epoll_data_t;

enum Events
{
  EPOLLIN,      //수신할 데이터
  EPOLLOUT,     //송신 가능
  EPOLLPRI,     //중요한 데이터(OOB)가 발생
  EPOLLRDHUD,   //연결 종료 or Half-close 발생
  EPOLLERR,     //에러 발생
  EPOLLET,      //엣지 트리거 방식으로 설정
  EPOLLONESHOT, //한번만 이벤트 받음
}

2.4. fd들이 무슨 일이 일어 났는지 조사

int epoll_wait(int epoll_fd,              //epoll_fd
               struct epoll_event* event, //event 버퍼의 주소
               int maxevents,             //버퍼에 들어갈 수 있는 구조체 최대 개수
               int timeout                //select의 timeout과 동일 단위는 1/1000
              );
              //성공시 이벤트 발생한 파일 디스크립터 개수 반환, 실패시 -1 반환
  • 두번째 인자로 epoll_event 구조체의 배열을 넘김
  • 함수가 반환 성공 시 배열에 이벤트가 발생한 fd와 이벤트의 종류가 묶여서 들어오기 때문에 모든 fd들을 순회할 필요가 없음

3. Epoll 사용

3.1. Edge Trigger(ET)

  • 특정 상태가 변화하는 시점에서만 감지
  • ET로 작동하게 하려면 Non-blocking 소켓으로 생성해 줘야 하며 epoll에 관심 FD로 등록 할 때 EPOLLET로 설정해야만 함
  • 단점:
    • 서버의 트리거 모드가 ET인 경우, 한번에 전송 가능한 패킷 버퍼 사이즈보다 보내야 하는 데이터의 사이즈가 커서 여러번에 걸쳐 write를 하게 되면 ET의 특성상 정상적으로 데이터를 전부 읽어드릴 수 없는 경우가 생길 수 있음

3.2. Level Trigger(LT)

  • 특정 상태가 유지되는 동안 감지
  • LT는 기본으로 설정되어 있으며 select나 poll은 LT만 지원
  • 단점:
    • 서버의 트리거 모드가 LT인 경우, 입력 버퍼에 데이터가 수신된 상황에서 이를 빠르게 읽어들이지 않으면 epoll_wait() 함수를 호출할 때 마다 이벤트가 발생
    • 이로인해 발생하는 이벤트의 수가 계속 누적되는데, 이를 현실적으로 감당하는건 불가능
    • 따라서, 정상적인 접속-접속종료 테스트에 대한 처리가 불가능해질 수 있음

3.3. 예제

#include <cstdio>
#include <iostream>
#include <string.h>
#include <fcntl.h>
 
using namespace std;
 
#include <sys/unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
 
#define LISTEN_BACKLOG 15

int main()
{
  int error_check;

  /* 
      socket : 소켓 생성
  */
  int server_fd = socket(PF_INET, SOCK_STREAM, 0);
  if (server_fd < 0) {
    printf("socket() error\n");
    return 0;
  }

  // server fd Non-Blocking Socket으로 설정  
  int flags = fcntl(server_fd, F_GETFL);
  // flag 추가
  flags |= O_NONBLOCK;
  // fd에 대해서 F_SETFL cmd를 사용하는데 flags를 arg로 설정 
  if (fcntl(server_fd, F_SETFL, flags) < 0) {
    printf("server_fd fcntl() error\n");
    return 0;
  }

  // 소켓 옵션 설정
  int option = true;
  error_check = setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
  if (error_check < 0) {
    printf("setsockopt() error[%d]\n", error_check);
    close(server_fd);
    return 0;
  }

  // 소켓 속성 설정
  struct sockaddr_in mSockAddr;
  memset(&mSockAddr, 0, sizeof(mSockAddr));
  mSockAddr.sin_family = AF_INET;
  mSockAddr.sin_port = htons(1818);
  mSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 사용가능한 랜카드 IP 사용

  /* 
      bind : 소켓 속성과 소켓 fd 연결
  */
  error_check  = bind(server_fd, (struct sockaddr*)&mSockAddr, sizeof(mSockAddr));
  if (error_check < 0) {
      printf("bind() error[%d]\n", error_check);
    close(server_fd);
    return 0;
  }

  /*
      listen : 응답 대기
  */
  if (listen(server_fd, LISTEN_BACKLOG) < 0) {
    printf("listen() error\n");
    close(server_fd);
    return 0;
  }

  /* 
      Epoll fd 생성
  */
  int epoll_fd = epoll_create(1024);    // size 만큼의 커널 폴링 공간을 만드는 함수
  if (epoll_fd < 0) {
    printf("epoll_create() error\n");
    close(server_fd);
    return 0;
  }

  // server fd, epoll에 등록
  struct epoll_event events;
  events.events = EPOLLIN | EPOLLET;
  events.data.fd = server_fd;
  
  /*
      server events set
  */
  // epoll_ctl : epoll이 관심을 가져주기 바라는 FD와 발생하는 event를 등록하는 인터페이스
  // EPOLL_CTL_ADD : 관심있는 파일디스크립트 추가
  // EPOLL_CTL_MOD : 기존 파일 디스크립터를 수정
  // EPOLL_CTL_DEL : 기존 파일 디스크립터를 관심 목록에서 삭제
  if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &events) < 0) {
    printf("epoll_ctl() error\n");
    close(server_fd);
    close(epoll_fd);
    return 0;
  }

  /* 
      epoll wait
  */
  // 관심있는 fd들에 무슨일이 일어났는지 조사
  // 사건들의 리스트를 epoll_events[]의 배열로 전달
  // 리턴값은 사건들의 개수, 사건의 최대 개수는 MAX_EVENTS
  int MAX_EVENTS = 1024;
  struct epoll_event epoll_events[MAX_EVENTS];
  int event_count;
  int timeout = -1;

  while (true)
  {
    event_count = epoll_wait(epoll_fd, epoll_events, MAX_EVENTS, timeout); 
    if (event_count < 0) {
      printf("epoll_wait() error [%d]\n", event_count);
      return 0;
    }

    for (int i = 0; i < event_count; i++)
    {
      if (epoll_events[i].data.fd == server_fd)
      {
        int client_fd;
        int client_len;
        struct sockaddr_in client_addr;
        
        printf("User Accept\n");
        client_len = sizeof(client_addr);

        /* 
            Accept
        */
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, (socklen_t*)&client_len);

        // client fd Non-Blocking Socket으로 설정
        int flags = fcntl(client_fd, F_GETFL);
        flags |= O_NONBLOCK;
        if (fcntl(client_fd, F_SETFL, flags) < 0) {
          printf("client_fd[%d] fcntl() error\n", client_fd);
          return 0;
        }

        if (client_fd < 0) {
          printf("accept() error [%d]\n", client_fd);
          continue;
        }

        // 클라이언트 fd, epoll 에 등록
        struct epoll_event events;
        events.events = EPOLLIN | EPOLLET;
        events.data.fd = client_fd;

        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &events) < 0) {
          printf("client epoll_ctl() error\n");
          close(client_fd);
          continue;
        }
      }
      else
      {
        // epoll에 등록 된 클라이언트들의 send data 처리
        int str_len;
        int client_fd = epoll_events[i].data.fd;
        char data[4096];
        str_len = read(client_fd, &data, sizeof(data));

        if (str_len == 0) {
          // 클라이언트 접속 종료 요청
          printf("Client Disconnect [%d]\n", client_fd);
          close(client_fd);
          epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
        }
        else {
          // 접속 종료 요청이 아닌 경우 요청의 내용에 따라 처리.
          printf("Recv Data from [%d]\n", client_fd);
        }
      }
    }
  }
}

참고

ozt88
rammuking


This is personal diary for study documents.
Please comment if I'm wrong or missing something else 😄. 

Top

댓글남기기