아래의 내용은
http://chonga.pe.kr/blog/index.php?pl=919 사이트에서 가지고 온 내용입니다.
다른 용도가 아니라.. 개인적으로 좋은 내용이라 가지고 있기 위해서 가지고 온 것인데.. 불펌이라면 불펌일 수도 있는데.. 순수 개인 용도라.. 이해를 해 주시길..^^
=============================================================================
제목: linux epoll을 이용한 echo server
- 작성자: 어린미르 (
http://chonga.pe.kr/blog)
- 문서위치:
http://chonga.pe.kr/blog/index.php?pl=919- 작성일: 20070405
- 문서 변경 이력 :
20070405 초판 릴리즈
20070406 EPOLLONESHOT 설명 추가
선언: 본 문서는 저자의 개인적인 의견이 들어가 있을 수 있습니다. 이 곳에 게재된 내용은 상업적 판단을 위한 자료로 이용될 수 없으며 그로 인한 책임은 저자에게 없습니다.
1 개요
Linux kernel 2.6에서 네트워크 성능 향상을 위해 정식으로 릴리즈된 i/o 이벤트 통지 기능인 epoll을 이용하여 echo server의 예를 통해 간단한 개요를 살펴보도록 하자.
2 epoll이란?
Linux kernel 2.4 이전부터 select, poll의 한계를 극복하기 위해서 실험적인 i/o 모델들이 테스트되기 시작했다. /dev/poll 혹은 realtime signal(rtsignal), event poll 등이 그 대표적인 예이다. 그 와중에 event poll이라는 이름으로 진화되기 시작한 것이 지금의 epoll이라고 할 수 있다.
3 epoll의 설명
말 그대로 i/o 이벤트의 통지기능을 하고 있는데, 크게 2가지 모드를 지원한다. 하나는 Level Triggered(이후 LT)이고 또 하나는 Edge Triggered(이후 ET)라고 할 수 있다. LT는 상태의 값을 검사하는 방법이고, ET는 상태의 변화를 감지하는 방법이다.
epoll은 우선 기존의 select, poll과 마찬가지로 socket(file descriptor)의 목록을 관리하고 그 것을 어떻게 검사하는 지에 대해서는 LT와 ET의 방법이 있다고 보면 크게 다르지 않다. select와 poll의 방법은 LT의 방법이라고 보면 된다. 따라서 기존에 만들어진 select, poll의 소스코드를 epoll로 그대로 마이그레이션하는 경우에는 LT 모드로 작업을 하면 쉽게 수정을 마칠 수 있다.
3.1 epoll의 함수
epoll을 위해 간단히 3가지 함수가 제공되는데, epoll 객체를 생성하는 epoll_create와 epoll에 socket 목록을 제어하는 epoll_ctl, 그리고 socket 목록의 이벤트를 감지하는 epoll_wait다.
그리고 struct epoll_event가 있다. 더 자세한 내용은 man-page로부터 살펴보시기를 바란다.
3.1.1 struct 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 */
};
3.1.2 관련 함수
int epoll_create(int size)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
3.2 epoll의 Level Triggered 모드와 Edge Triggered 모드
epoll은 기존 socket 목록을 갖고 있는 상황에서 그 목록 중에 "읽기/쓰기/예외 상황" 등의 이벤트들이 발생을 감지해서 각각에 맞는 처리를 하게 되는 것은 공통이지만, LT와 ET의 모드에 따라 이벤트를 감지하는 방식이 달라진다고 볼 수 있다. 다시 LT를 설명하자면 epoll에 등록된 socket 목록들 가운데, 이벤트가 발생된 부분이 체크가 되어 있다고 가정하면 이벤트가 이번 턴에 발생(epoll_wait가 깨어남)하더라도 처리가 되지 않았다면 다음 턴에 다시 감지된다고 이해하면 쉽겠다. 이를 기준으로 ET를 다시 설명하면 이번 턴에 읽기 이벤트가 발생했다고 하면 이번 턴에 읽기 동작을 모두 완료하지 않는다면 다시 epoll_wait로 이벤트를 감지하는 함수가 호출되더라도 이전 동작에 의한 이벤트 감지는 불가능해진다. 따라서 LT는 상태감지, ET는 변경감지라고 할 수 있겠다. 물론 일반적으로 변경만을 감지하는 ET의 경우가 성능이 더 뛰어나다. 중요한 부분으로 ET의 경우에는 반드시 non blocking socket을 사용해야한다.
마지막으로 ET의 경우에는 추가적으로 한가지 모드를 더 지원하는데, EPOLLONESHOT이라는 모드다. 이 것은 epoll이 관리하는 socket 목록에 한 번 등록이 된 이후에, 이벤트 감지에 의해 검출이 되는 경우, socket 목록에서 이벤트를 받지 않도록 설정이 변경되는 것으로 one shot이라는 표현을 쓰고 있다. 이 경우는 이벤트를 검출한 이후에, 다른 쓰레드에 해당 socket에 대해서 read 작업을 맡기고 끝난 경우에 다시 epoll의 socket 목록에서 이벤트를 받을 수 있도록 변경하는 구조의 서버 프로그램에서 유용한 기능이라고 할 수 있겠다. 이 기능은 client 로서 동작하는 경우에 non-blocking socket 설정하고 epoll의 목록에 넣은 후에 connect를 한 후에 읽기 이벤트(접속됨)를 1회 검사하는 기능으로 사용되어도 될 듯 하다.
4 epoll의 실전 예
echo server의 구현을 통해 epoll의 실전 예를 살펴보도록 하자. 우선 LT, ET, 그리고 ET의 ONESHOT기능을 모두 이용하여 다음과 같은 예제 코드를 간단히 만들어 보았다. LT의 경우는 blocking socket으로 구현했고, ET의 경우는 non-blocking socket으로 구현되었다.
4.1 makefile의 예
make / LT 모드의 echo server 빌드
make echoserver2-et / ET 모드의 echo server 빌드
make echoserver2-etoneshot / ET 모드의 ONESHOT 기능의 echo server 빌드
CC:=gcc
CFLAGS:=
LIBS:=
all:
$(CC) $(CFLAGS) -g -o epoll-echoserver2 epoll-echoserver2.c $(LIBS)
echoserver2-et:
$(CC) $(CFLAGS) -DUSE_ET -g -o epoll-echoserver2 epoll-echoserver2.c $(LIBS)
echoserver2-etoneshot:
$(CC) $(CFLAGS) -DUSE_ET -DUSE_ETONESHOT -g -o epoll-echoserver2 epoll-echoserver2.c $(LIBS)
4.2 echoserver의 예 : epoll-echoserver2.c
소스의 경우 select, poll과 크게 다르지 않다고 볼 수 있다. accept socket을 만들고 epoll에 읽기 이벤트를 설정한 후에 epoll의 socket 목록에 최초에 추가시키고 epoll_wait의 loop로 들어와서 accept socket인지 일반 socket인지에 따라 do_accept()와 do_use_fd()의 함수로 분기 호출했다. 내용 중에서 눈여겨 볼 부분은 struct epoll_event인데, events(읽기,쓰기, ET모드 설정 등)와 data이다. data의 경우에는 int타입이나 void* 타입을 마음대로 설정해서 사용할 수가 있다. 여기서는 int 타입으로 socket인 file descriptor를 넣어서 처리한다. 실제로 개발자가 원하는 사용자 객체의 타입을 데이터의 포인터로 사용자 관리 타입을 만들고 struct epoll_event의 data에 넣어줄 수도 있다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#define EPOLL_SIZE 60
#define EPOLL_EVENT_SIZE 100
int
setnonblock (int sock)
{
int flags = fcntl (sock, F_GETFL);
flags |= O_NONBLOCK;
if (fcntl (sock, F_SETFL, flags) < 0)
{
perror("fcntl, executing nonblock error");
return -1;
}
return 0;
}
int
settcpnodelay (int sock)
{
int flag = 1;
int result = setsockopt (sock, /* socket affected */
IPPROTO_TCP, /* set option at TCP level */
TCP_NODELAY, /* name of option */
(char *) &flag, /* the cast is historical cruft */
sizeof (int)); /* length of option value */
return result;
}
int
setreuseaddr(int sock)
{
int flag = 1;
int result = setsockopt (sock, /* socket affected */
SOL_SOCKET, /* set option at TCP level */
SO_REUSEADDR, /* name of option */
(char *) &flag, /* the cast is historical cruft */
sizeof (int)); /* length of option value */
return result;
}
int
get_portnumber_from_arg(int argc, char **argv)
{
// get port number
if (argc == 2)
{
return atoi(argv[1]);
}
else
{
return -1;
}
}
int
init_epoll (int eventsize)
{
int efd;
// init epoll
if ((efd = epoll_create (eventsize)) < 0)
{
perror ("epoll_create (1) error");
return -1;
}
return efd;
}
int
init_acceptsock (unsigned short port)
{
struct sockaddr_in addr, clientaddr;
int clilen = sizeof (clientaddr);
int sfd = socket (AF_INET, SOCK_STREAM, 0);
if (sfd == -1)
{
perror ("socket error :");
close (sfd);
return -1;
}
setreuseaddr(sfd);
addr.sin_family = AF_INET;
addr.sin_port = htons (port);
addr.sin_addr.s_addr = htonl (INADDR_ANY);
if (bind (sfd, (struct sockaddr *) &addr, sizeof (addr)) == -1)
{
close (sfd);
return -2;
}
listen (sfd, 5);
return sfd;
}
int
do_use_fd (int efd, struct epoll_event ev)
{
int readn;
int sendflags = 0;
char buf_in[1024] = { '\0' };
int cfd = ev.data.fd;
//printf("DoUseFD\n");
#ifdef USE_ET
int sizen = 0;
char readbuf[256] = { '\0' };
while(1) {
readn = read (cfd, readbuf, 255);
if (readn <0 ) {
if (EAGAIN == errno ) {
break;
}
do_del_fd(efd, cfd);
close (cfd);
printf ("Close fd %d by %d\n", cfd,readn);
perror("Closed");
return -1;
}
sizen += readn;
if (sizen >= 1024) {
do_del_fd(efd, cfd);
close (cfd);
printf ("Close buffer full fd %d by %d\n", cfd,readn);
perror("Closed");
return -1;
}
memcpy(buf_in,readbuf,readn);
}
printf ("read data %d, %s", sizen, buf_in);
send (cfd, buf_in, strlen (buf_in), sendflags);
#ifdef USE_ETONESHOT
// re-set fd to epoll
do_modify_fd(efd, cfd);
#endif // USE_ETONESHOT
#else //#ifdef USE_ET
//memset (buf_in, 0x00, 256);
readn = read (cfd, buf_in, 255);
// if it occured ploblems with reading, delete from epoll event pool and close socket
if (readn <= 0)
{
do_del_fd(efd,cfd);
close (cfd);
printf ("Close fd ", cfd);
}
else
{
printf ("read data %s", buf_in);
send (cfd, buf_in, strlen (buf_in), sendflags);
}
#endif //#ifdef USE_ET
return 1;
}
int
do_accept (int efd, int sfd)
{
int cfd;
int clilen;
struct sockaddr_in addr, clientaddr;
printf("Accepted\n");
cfd = accept (sfd, (struct sockaddr *) &clientaddr, &clilen);
if (cfd < 0)
{
perror ("Accept error");
return -1;
}
#ifdef USE_ET
setnonblock (cfd);
#endif // USE_ET
settcpnodelay (cfd);
do_add_fd(efd, cfd);
return cfd;
}
int
do_add_fd(int efd, int cfd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
#ifdef USE_ET
ev.events |= EPOLLET;
#ifdef USE_ETONESHOT
ev.events |= EPOLLONESHOT;
#endif // USE_ETONESHOT
#endif // USE_ET
ev.data.fd = cfd;
return epoll_ctl (efd, EPOLL_CTL_ADD, cfd, &ev);
}
// fixme
int
do_modify_fd(int efd, int cfd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
#ifdef USE_ET
ev.events |= EPOLLET;
#ifdef USE_ETONESHOT
ev.events |= EPOLLONESHOT;
#endif // USE_ETONESHOT
#endif // USE_ET
ev.data.fd = cfd;
return epoll_ctl (efd, EPOLL_CTL_MOD, cfd, &ev);
}
int
do_del_fd(int efd, int cfd)
{
struct epoll_event ev;
return epoll_ctl (efd, EPOLL_CTL_DEL, cfd, &ev);
}
// main
int
main (int argc, char **argv)
{
const int poolsize= EPOLL_SIZE;
const int epollsize= EPOLL_EVENT_SIZE;
struct epoll_event *events;
struct epoll_event ev;
int sfd, efd;
int i;
int max_got_events;
int result;
int port;
port = get_portnumber_from_arg(argc,argv);
if (port < 0)
{
puts("I need 1 argument(as a listen port)");
return 1;
}
efd = init_epoll (epollsize);
if (efd < 0)
{
perror ("init_epoll error");
return 1;
}
// init pool
events = (struct epoll_event *) malloc (sizeof (*events) * poolsize);
if (NULL == events)
{
perror ("epoll_create (0) error");
return -1;
}
sfd = init_acceptsock (port);
if (sfd < 0)
{
perror ("init_acceptsock error");
return 1;
}
printf("Running Server port %d\n",port);
result = do_add_fd(efd,sfd);
if (result < 0)
{
perror ("epoll_ctl error");
return 1;
}
while (1)
{
max_got_events = epoll_wait (efd, events, poolsize, -1);
for (i = 0; i < max_got_events; i++)
{
if (events[i].data.fd == sfd)
{
do_accept (efd, sfd);
}
else
{
do_use_fd (efd, events[i]);
}
}
}
return 1;
}
5 참고 문헌
- man page : man epoll
- google 에서 epoll 검색
- wikipedia 에서 epoll 검색