Epoll의 이해와 활용
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;
}