기본 콘텐츠로 건너뛰기

[C언어] epoll 설명

출처 - 
http://biscuit.cafe24.com/moniwiki/wiki.php/epoll


1 준비
2 socket 프로그래밍 기본
3 비동기 입출력 (Asyncronous I/O) & 입출력 다중화 (I/O Multiplexing)
4 select
5 select 와 poll 그리고 epoll. 그 차이
6 epoll 프로그래밍 흐름
7 epoll 함수들
8 epoll References

빈폴도 아니고, 이폴이란 대체 무엇일까?


당신은 서버한대로 몇 명의 동시접속자를 수용할 수 있습니까? 최근에 인터넷에 떠돌아다니는 [http]c10k_problem은 대당 10K, 즉 1만명의 동시접속(concurrent users)을 받아보자는 문제다. 서버 프로그래밍을 해 본 사람이라면 이게 그리 만만한 문제가 아니라는 것을 직감할 듯 --;


요즘의 Massive 온라인게임은 '분산처리'가 기본이라 한 대에서 많은 이용자를 커버하기보다는 여러대가 하나의 세트로써 구성하는 것이 인기가 있고 다수의 커넥션보다는 소수 커넥션에서의 대용량 전송이 더 중요한 요소이기도 하다.


c10k problem에 나또한 관심을 가지게 되었고, epoll 이 최근 급부상하는 솔루션으로 인기가 있다기에 한 번 파보자 하고 결심하고 이 글을 시작했다. 마침 wiki에도 관심이 있던 차라, wiki 공부도 할 겸해서 epoll 을 연구하는 과정을 이 wiki에 담아 보고자 한다.


1 준비 #


* 누구를 위한 epoll 인가?


epoll은 '한 대의 서버에서 아주많은 동시접속자를 처리하기 위한 수단'이다. 이미 당신이 그 수단을 알고 있다면 - epoll 이건 아니건 - 이 글은 별로 도움이 안될듯하다. 동시접속자가 천명을 넘지않는다면 구닥다리 방법을 이용하는 것과 큰 차이가 없으리라 본다.


또한, epoll은 Linux 프로그래머의 도구이다. M$ window$ 환경의 개발자라면 이미 iocp 라는 훌륭한 도구가 있고 ?FreeBSD라면 kqueue라는 도구가 있다. 각각의 OS에 맞는 도구를 선택하자.


* 사전준비

epoll(4) is a new API introduced in Linux kernel 2.5.44.  Its interface  should be finalized in Linux kernel 2.5.66.

epoll은 Linux 커널 2.5.44 에서 소개된 새로운 API이며, 2.5.66 에서 완성되었다. 구버전 커널이라면 패치를 할 수도 있겠으나 그냥 Linux 2.6.x 를 구해다 쓰자. ;)

[biscuit@fedora-dumoyi epoll]$ uname -a
Linux fedora-dumoyi 2.6.5-1.358 #1 Sat May 8 09:04:50 EDT 2004 i686 i686 i386 GNU/Linux

[biscuit@fedora-dumoyi epoll]$ nm -D /lib/libc.so.6 | grep epoll
000bd740 T epoll_create
000bd780 T epoll_ctl
000bd7d0 T epoll_wait

nm 도구를 이용하여, /lib/libc.so.6 을 덤프해본 결과, 잘 있다 ^^.


같은 2.6.x 라고 하더라도 커널 설정에서 빠질수도있는데, 이 경우에는 물론 커널 컴파일을 다시 해주어야 한다 --; General setup > Configure standard kernel features > Enable eventpoll support 를 Yes로 하자. 커널설치 참고 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/mz_gentoo


Linux Kernel 2.5.22 이상이 필요하다.
glibc 2.3.2 이상이 필요하다. ?RedHat 9.x 는 지원하고 있다.
gcc 3.3.x 이상이 필요하다. 추가적으로 이 문서에서는 C++과 STL을 이용하고 있으므로 예제를 모두 컴파일 해보기 위해서는 g++ 과 libstdc++ 이 설치되어 있어야 한다.


2 socket 프로그래밍 기본 #


* 이 곳을 가볍게 읽고 온다 : socket프로그래밍기본


3 비동기 입출력 (Asyncronous I/O) & 입출력 다중화 (I/O Multiplexing) #


* 이 곳을 가볍게 읽고 온다 : 비동기입출력(?AsyncIo) * 이 곳을 가볍게 읽고 온다 : 입출력 다중화(?IoMultiplex)


4 select #


* 이 곳을 가볍게 읽고 온다 : Select 를 이용한 서버 만들기 (?SelectModel)


5 select 와 poll 그리고 epoll. 그 차이 #


select는 fd_set이라는 구조체를 통해 fd들을 등록하게 되는데, 불행하게도 이 fd_set는 bitmask라서 1024개까지만 등록할 수 있다 (커널을 뜯어고치면 늘릴 수 있다는 말도 들어본거 같기는 하다만...). 게다가 이벤트 발생을 감지하기 위해 내부적으로 순차검사를 시행하므로 동시접속이 늘어날수록 불리해진다.


poll 함수는, 관심있어하는 fd 들의 구조체배열을 파라미터로 받는데, 모든 fd들에 대해 순차검사를 시행하므로 역시 마찬가지이다.


select나 poll이나, 또 epoll이나 관심있는 fd를 죄다 등록해두고 그중 한개 이상에서 사건이 발생하는 것을 감지한다는데는 차이가 없다. 그러나 어떤 fd에서 사건이 발생했을 경우, select는 어느 녀석에서 발생한 사건인지를 찾기 위해 전체 fd 에 대해서 FD_ISSET 루프를 돌려야 하지만, epoll_wait는 사건이 발생한 fd들만의 구조체 배열을 셋팅해주므로, 보다 합리적이다.


6 epoll 프로그래밍 흐름 #


* epoll fd 생성 : epoll_create() * 연결 받기 (listen, accept)
  1. listen 용 fd 생성 및 준비 : socket(), bind(), setsockopt()
  2. epoll에 등록 : epoll_ctl(..., EPOLL_CTL_ADD, ... )
  3. listen()
  4. epoll_wait를 통해, fd_listen 에 사건발생(즉, 누군가 접속시도함)을 감지하여 접속받음 : accept()
  5. accept로부터 넘어온 fd에 통신준비 : fcntl()
  6. epoll에 새로운 fd 등록 : epoll_ctl(..., EPOLL_CTL_ADD, ... )
* 연결 하기 (connect)
  1. 사전준비(상대방 ip주소, port번호등) 그리고 연결 : connect()
  2. 연결성공하면 connect로부터 넘어온 fd에 통신준비 : fcntl()
  3. epoll에 새로운 fd 등록 : epoll_ctl(..., EPOLL_CTL_ADD, ... )
* 연결 끊기 (connect)
  1. 연결을 끊고 싶거나, 연결이 끊김 사건 발생 또는 에러발생 : read(), epoll_wait() 에서 감지
  2. epoll에서 해당 fd 삭제 : epoll_ctrl(..., EPOLL_CTL_DEL, ...)
  3. 연결 닫음 : close()
* epoll 종료 : close(), epoll 도 FILE이기 때문에 그냥 닫으면 된다 ;)

* 읽기 (read)
  1. 읽을 데이타 도착했음을 감지 : epoll_wait()
  2. 데이타 읽기 : read()
  3. 읽는 도중 발생하는 에러처리, 필요시 연결끊기
* 쓰기 (write)
  1. 보낼 데이터가 발생한 경우 Send Buffer에 일단 저장
  2. 보내도 된다는 신호를 감지 : epoll_wait()
  3. 데이터 전송 : write()
    1. 전송 도중 발생하는 에러처리, 필요시 연결끊기 혹은 WOULDBLOCK 처리
  4. 참고 1, WOULDBLOCK 인 경우는 에러지만 에러처리하지 않고, 단순히 리턴하고 기다리다가, EPOLLOUT 신호가 다시 발생했을 경우에 데이터를 전송하도록 한다.
    참고 2, Send Buffering (]?SendBuffer])

7 epoll 함수들 #


* nm 으로 덤프해본 결과에서 눈치챘겠지만, epoll 관련 함수는 딸랑 3개다! 이 3개의 함수와 struct epoll_event 구조체만 알면 epoll 정복은 눈앞에 있다고 해도 과언이 아니다. ;)


epoll_create
#include <sys/epoll.h>

int epoll_create(int size);

epoll_create는 size 만큼의 커널폴링공간을 만드는 함수이다. 리턴값은 그냥 정수값인데 fd_epoll이라고 부르기로 하자. 이 fd_epoll 를 이용해서 앞으로 다른 조작들을 하게 된다.

int fd_epoll;
bool is_epoll_init = false;

int EpollInit(int size)
{       
        if((fd_epoll = epoll_create(size)) > 0) is_epoll_init = true;
        return fd_epoll;
}

size 값은 정수인데 무작정 큰 수를 쓸 수는 없다. 예상되는 최대 동시접속 수로 하면 될텐데, 운영체제가 이 숫자를 허용하는지 먼저 확인해주어야한다. 서버의 한계(ServerLimits)를 숙지하고, 적당한 값을 써 주거나 서버한계를 늘려! 주어야 한다.


위 코딩은, fd_epoll 과 is_epoll_int 를 전역변수로 빼 놨는데, 나중에 class 로 만들기 위해서이다. 이후에 class ?TEpoll 을 만들것이고, new ?TEpoll(size)로 접근하게 할 계획.


epoll_ctl
#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl은 epoll이 관심을 가져주길 바라는 fd와 그 fd에서 발생하는 관심있는 사건의 종류를 등록하는 인터페이스. 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 events */
    epoll_data_t data;  /* User data variable */
};

공용체 하나와 32비트 정수를 가지는 평범한 구조체인데, 사건은 (epoll_event).events를 이용한다. 보통 서버에서는 입출력 시점에 관심이 많기 때문에, 아래 예제에서는 IN/OUT/ERR 세가지 이벤트를 설정하도록 했다.
int EpollAdd(const int fd)
{       
        struct epoll_event ev;
        
        ev.events  = EPOLLIN | EPOLLOUT | EPOLLERR;
        ev.data.fd = fd;
        
        return epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd, &ev);
}


epoll_wait
이제 epoll의 핵심, epoll_wait를 살펴보자.
#include <sys/epoll.h>

int  epoll_wait(int  epfd,  struct epoll_event * events, int maxevents, int timeout);

epoll_wait함수는 관심있는 fd 들에 무슨일이 일어났는지 조사한다. 다만 그 결과는 앞서 살펴본바와 같이 select나 poll과는 차이가 있다. 사건들의 리스트를 (epoll_event).events[] 의 배열로 전달한다. 또, 실제 동시접속수와는 상관없이 maxevents 파라미터로 최대 몇개까지의 event만 처리할 것임을 지정해 주도록 하고 있다.


만약 현재 접속수가 1만이라면 최악의 경우 1만개의 연결에서 사건이 발생할 가능성도 있기 때문에 1만개의 events[] 배열을 위해 메모리를 확보해 놓아야하지만, 이 maxevents 파라미터를 통해, 한번에 처리하길 희망하는 숫자를 제한할 수 있다.


timeout은, epoll_wait의 동작특성을 지정해주는 중요한 요소인데, 밀리세컨드 단위로 지정해주도록 되어 있다. 이 시간만큼 사건발생을 기다리라는 의미인데, 기다리는 도중에 사건이 발생하면 즉시 리턴된다. 이 값에 (-1)을 지정해주면 영원히 사건을 기다리고(blocking), 0을 지정해주면, 사건이 있건없건 조사만 하고 즉시 리턴한다 (즉 기다리지 않는다).


간단한 채팅서버의 경우를 살펴보자. 서버가 어떠한 일을 해야하는 시점은 이용자 누군가가 데이터를 보내왔을 때인데, 아무도 아무말도 하지 않는다면 서버는 굳이 프로세싱을 할 이유가 없다. 이럴때 timeout을 (-1)로 지정해두고 이용자들의 입력이 없는 동안 운영체제에 프로세싱 타임을 넘기도록 한다.


온라인게임(특히 MMORPG)의 경우에는, 이용자의 입력이 전혀 없는 도중이라고 하더라도, 몬스터에 관련된 처리, 적절한 저장, 다른 서버와의 통신들을 해야 하므로 적절한 timeout (필자의 경우에는 1/100 sec, 즉 10ms를 선호한다)을 지정해 주도록 한다.


뭔가의 프로세싱을 주로 하면서 잠깐잠깐 통신이벤트를 처리하고자 하는 경우, 즉 프로세스의 CPU 점유를 높게해서 무언가를 하고 싶은 경우에는, timeout 0을 설정하여 CPU를 독점하도록 설계할 수도 있다.


별도 thread를 구성하여 이 thread 가 입출력을 전담하도록 프로그램을 작성하고자 하는 경우에는, 당연히 timeout을 (-1)로 설정하여 남는 시간을 다른 thread, 혹은 운영체제에 돌려 주도록 한다.


아래 예제는, epoll_wait을 전형적으로 구현해 본것이다.
#define MAX_EVENTS  100 // 최대 100개를 한번에 처리할 것이다.

struct epoll_event events[MAX_EVENTS];
int nfds, n;

for(;;){
    // 발생한 사건의 갯수를 얻어낸다. 0인 경우는 아무일도 발생하지 않은 것
    nfds = epoll_wait(fd_epoll, events, MAX_EVENTS, 10);

    if(nfds < 0) {
        // critical error
        fprintf(stderr, "epoll_wait() error : %s\n", strerror(errno));
        exit(-1);
    }

    // 아무일도 일어나지 않았다.
    if(nfds == 0){
        // idle 
        continue;
    }

    for(n=0; n < nfds; ++n) OnEvent(&events[n]);
}

epoll_wait를 호출하고, 그 결과로 이벤트가 발생한 fd의 갯수가 돌아오며 어떤 fd들인지와 어떤 이벤트들인지는 epoll_event 구조체 배열에 담겨진다. 그 갯수만큼의 이벤트 처리를 해 주도록 했다.


?OnEvent()는 이벤트 처리를 담당할 실제 함수. Client class를 구성하고, 그 객체와 바로 연결시키도록 작성하면 무난할 듯(접속 연결별로 class ?TClient의 객체를 생성하고 map< fd, ?TClient *> 로 관리를 한다면 아래와 같이 작성).
#include <map>

using namespace std;

class TClient 
{
    ...
};

map<int, TClient *> mapClient;
TClient *tmpClient;

...
...

nfds = epoll_wait(...);

...

for(n=0; n < nfds; ++n){ 
    tmpClient = mapClient[events[n]->data.fd];

    if(tempClient) tmpClient->OnEvent(&events[n]);
    else ... // 에러처리
}


?OnEvent()의 구현
int OnEvent(const struct epoll_event *event)
{
    int nread;
    char buf[1024];     

    if( event->events & EPOLLIN ){
        // 나중에 OnRead(); 로 바꿀것이다.
        nread = read(event->data.fd, buf, 1024);
        
        if( nread < 1){
            fprintf(stdout, "nread returns : %d\n", nread);
         } else {
            fprintf(stdout, "data : %s\n", buf);
            buf[0] = 0;
         }
    }
    if( event->events & EPOLLOUT){
        // 나중에 OnWrite(); 로 바꿀것이다.
    }
    if( event->events & EPOLLERR){
        // 나중에 OnError(); 로 바꿀것이다.
    }

    return 1;
}

구현은 특별한 내용은 없고, 다만 각 event들에 대해서 해당 비트가 셋 되어 있는지 확인하도록 하고 있고, EPOLLIN 이벤트, 즉 데이터가 수신되었을때 읽어서 화면에 표시하도록 했다.



댓글

이 블로그의 인기 게시물

UNIX C errno 정리( 에러 번호 )

#define EPERM   1   /* Operation not permitted      */ #define ENOENT  2   /* No such file or directory        */ #define ESRCH   3   /* No such process          */ #define EINTR   4   /* interrupted system call      */ #define EIO 5   /* I/O error                */ #define ENXIO   6   /* No such device or address        */ #define E2BIG   7   /* Arg list too long            */ #define ENOEXEC 8   /* Exec format error            */ #define EBADF   9   /* Bad file descriptor          */ #define ECHILD  10  /* No child processes           */ #define EAGAIN  11  /* Resource temporarily unavailable */ #define ENOMEM  12  /* Not enough space         */ #define EACCES  13  /* Permission denied            */ #define EFAULT  14  /* Bad address              */ #define ENOTBLK 15  /* Block device required        */ #define EBUSY   16  /* Resource busy            */ #define EEXIST  17  /* File exists              */ #define EXDEV   18  /* Improper link            */ #define ENODEV  19  /* No such

시리얼(Serial) 이란?

출처 - http://www.ni.com/white-paper/2895/ko/#toc4 시리얼은 거의 모든 PC에서 표준으로 사용되는 디바이스 통신 프로토콜입니다. 시리얼의 개념을 USB의 개념과 잘 구분하십시오. 대부분의 컴퓨터에는 2개의 RS232 기반 시리얼 포트가 있습니다. 시리얼은 또한 여러가지 디바이스에서 계측을 위한 일반 통신 프로토콜이며, 여러 GPIB 호환 디바이스에는 RS232 포트가 장착되어 있습니다. 뿐만 아니라, 원격 샘플링 디바이스로 데이터 수집을 하는 경우에도 시리얼 통신을 사용할 수 있습니다. 시리얼 통신의 개념은 간단합니다. 시리얼 포트는 정보의 바이트를 한번에 한 비트씩 순차적으로 송수신합니다. 한번에 전체 바이트를 동시에 전달하는 병렬 통신과 비교하면 시리얼 통신은 속도가 느리지만 훨씬 간단하며 장거리에도 사용할 수 있습니다. 예를 들어, 병렬 통신용 IEEE 488 스펙을 보면 기기간 케이블링은 총 20 m 미만이어야 하며, 두 개의 디바이스간은 2 m 미만이어야 합니다. 반면 시리얼 통신은 최대 1.2 Km의 통신거리를 보장합니다. 통상 엔지니어들은 ASCII 데이터를 전송할 때 시리얼 통신을 사용합니다. 이 때 송신용 (Tx), 수신용 (Rx), 그라운드용 (GND)의 세 가지의 전송 라인을 사용하여 통신합니다. 시리얼은 비동기식이므로 포트는 한 라인에서 데이터를 전송하고 다른 라인에서 데이터를 수신합니다. 핸드쉐이킹용 라인도 사용 가능하지만 필수 요구사항은 아닙니다. 시리얼 통신의 가장 중요한 특징에는 보드 속도 (baud rate), 데이터 비트, 정지 비트, 패리티가 있습니다. 두 개의 포트가 통신하기 위해서는 이러한 파라미터가 반드시 적절하게 맞춰져야 합니다. 보드 속도는 통신의 속도를 측정하는 수치이며 초당 비트 전송 숫자로 표시됩니다. 예를 들어 300 보드 속도는 초당 300 비트를 의미합니다. 엔지니어들이 흔히 말하는 클럭 주기는 보드 속도를 의미합니다. 따라서 프로토콜에 4800