select 기반의 IO 멀티 플렉싱이 느린 이유
- 소켓 관리하는 fd_set을 직접 관리해야 함
- select로 fd_set을 전달할 때 원본을 직접 전달하지 않고 복사본을 전달해야 하는 번거로움
- select가 한번 일어날 때 마다 fd_set을 모두 점검하면서 변화가 일어났는지 체크해야 함
- select는 운영체제에 의해서 완성되는 기능이 아니고 함수에 의해 완성되기 때문에 번거롭고 성능이 떨어질 수 있음
- 이 문제를 해결하기 위해선 운영체제에 관찰 대상(소켓)의 정보를 한번만 알려주고 변화가 있을 때 변화가 있는 소켓의 정보들만 받으면 됨
- 위처럼 운영체제 레벨에서 지원하는 멀티 플렉싱 모델이 epoll(리눅스), IOCP(윈도우)
select를 사용할 상황
- 서버의 접속자가 많지 않음
- 다양한 운영체제에서 사용할 수 있어야 함
-> epoll이나 iocp는 운영체제에 종속적임
epoll 방식의 장점
- select처럼 모든 소켓에 대해 반복문을 수행하면서 점검할 필요가 없음
-> 변화가 일어난 소켓의 정보만 전달해서 주기 때문
- select함수에 대응하는 epoll_wait 시 소켓의 정보를 매번 전달할 필요가 없음
epoll에 필요한 함수와 구조체
- epoll_event, epoll_data 구조체 : 소켓 디스크립터 등록, 이벤트 발생의 확인에 사용되는 구조체
typedef union epoll_data
{
void *ptr;
int fd; // 이벤트가 일어난(or 일어날) 파일 디스크립터
__unit32_t u32;
__unit64_t u64;
} epoll_data_t;
struct epoll_event
{
__unit32_t events; // 관찰할 이벤트의 종류
epoll_data_t data;
}
- epoll_create : epoll 파일 디스크립터 저장소 생성
#include <sys/epoll.h>
int epoll_create(int size);
-> 성공 시 epoll 파일 디스크립터, 실패 시 -1 반환
-> epoll의 시작을 운영체제에 알려주어 OS가 fd를 관리할 저장소를 만들게 함
- epoll_ctl : 저장소에 파일 디스크립터를 등록, 삭제
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
-> 성공 시 0, 실패 시 -1 반환
-> epfd : epoll_create로 생성한 epoll 파일 디스크립터
-> op : 관찰 대상의 추가, 삭제, 변경 여부
// epoll.h
#define EPOLL_CTL_ADD 1 /* Add a file decriptor to the interface. */
#define EPOLL_CTL_DEL 2 /* Remove a file decriptor from the interface. */
#define EPOLL_CTL_MOD 3 /* Change file decriptor epoll_event structure. */
-> fd : 등록할 파일디스크립터
-> event : 관찰 대상의 이벤트 유형
- EPOLLIN : 수신할 데이터가 존재하는 이벤트
- EPOLLOUT : 즉시 데이터를 전송할 수 있도록 출력 버퍼가 비워진 이벤트
- EPOLLPRI : OOB(Out-Of-Band) 데이터가 수신된 이벤트
- EPOLLRDHUP : 연결이 종료된 이벤트 (half-close 포함)
- EPOLLET : Edge-trigger 방식으로 이벤트 감지. 이 경우 | 연산자를 이용해 이벤트 종류도 함께 명시
- EPOLLONESHOT : 최초의 이벤트만 감지하고 이후에는 return하지 않는 방식. | 연산자를 이용해 이벤트 종류를 함께 명시
- EPOLLERR : 에러가 발생한 이벤트
enum EPOLL_EVENTS
{
EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
EPOLLONESHOT = (1 << 30),
#define EPOLLONESHOT EPOLLONESHOT
EPOLLET = (1 << 31)
#define EPOLLET EPOLLET
};
- epoll_wait : select 함수처럼 파일의 디스크립터 변화를 대기
-> 성공 시 이벤트가 발생한 파일 디스크립터의 수, 실패 시 -1 반환
-> epfd : epoll 파일 디스크립터
-> events : 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소 값
-> maxevent : 최대 이벤트 수
-> timeout : 대기 시간, -1 전달 시 이벤트 발생 시까지 무한 대기
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
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);
...;
}
epoll 기반의 에코 서버
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;
...;
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
...;
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
printf("bind() error");
if(listen(serv_sock, 5)==-1)
printf("listen() error");
epfd=epoll_create(EPOLL_SIZE); // epoll 저장소 생성
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(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
// 소켓을 epoll 관리 대상에서 삭제
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); // epoll 인스턴스 소멸
return 0;
}
'Programming > Network' 카테고리의 다른 글
열혈 TCP/IP 18-1. 쓰레드의 이론적 이해 (0) | 2021.03.28 |
---|---|
열혈 TCP/IP 17-2. 레벨 트리거와 엣지 트리거 (0) | 2021.03.28 |
열혈 TCP/IP 16-2. 파일 디스크립터의 복사와 half-close (0) | 2021.03.28 |
열혈 TCP/IP 16-1. 입력 / 출력 스트림의 분리 (0) | 2021.03.28 |
열혈 TCP/IP 15-3. 소켓 기반에서의 표준 입출력 함수 사용 (0) | 2021.03.28 |