본문 바로가기
프로그래밍/C

C] epoll 사용법

by eteo 2024. 6. 24.
반응형

 

 

 

epoll이란?

epoll은 리눅스 커널 2.5.44부터 도입된 Multiplexing IO 함수로 다수의 파일 디스크립터를 모니터링 할 때 효율적이다. epoll은 epoll_event 구조체와 다음의 세 가지 주요 함수로 구성된다.

  • epoll_create : epoll 파일 디스크립터 저장소 생성
  • epoll_ctl : 저장소에 파일 디스크립터 등록 및 삭제
  • epoll_wait : 파일 디스크립터의 변화를 대기

 

 

 

select와의 차이점

select의 경우 매번 호출할 때마다 파일 디스크립터 집합을 사용자 공간에서 커널 공간으로 복사해야 하지만 epoll는 한 번 설정한 파일 디스크립터 집합을 커널 공간에서 유지하여 불필요한 복사를 방지한다. 따라서 많은 수의 파일 디스크립터를 다룰 때 epoll이 더욱 효율적이며 사용자 입장에서 다음의 차이가 있다.

  • 상태변화의 확인을 위해 전체 파일 디스크립터를 대상으로 하는 반복문이 필요 없다.
  • select 함수에 대응되는 epoll_wait 함수 호출 시 관찰 대상의 정보를 매번 전달할 필요가 없다.

 

 

 

 

epoll_event 구조체

struct epoll_event {
    uint32_t events;   /* Epoll events */
    epoll_data_t data; /* User data variable */
};

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;
  • events : 감지할 이벤트 (e.g, EPOLLIN, EPOLLOUT, EPOLLERR)
  • data : 사용자 데이터, 보통 파일 디스크립터를 저장

epoll_event 배열을 넉넉한 길이로 선언해서 epoll_wait 함수호출 시 인자로 전달하면 이벤트가 발생한 경우 배열의 인덱스 0부터 차곡차곡 이벤트 정보가 저장된다.

한편 epoll_event는 파일 디스크립터를 epoll 인스턴스에 추가할 때 이벤트의 유형을 등록하는 용도로도 사용된다.

 

 

 

 

 

events에 저장 가능한 상수

OR 연산자를 이용해 둘 이상을 함께 등록할 수 있다.

  • EPOLLIN : 수신할 데이터가 존재하는 상황
  • EPOLLOUT : 출력버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
  • EPOLLPRI : OOB 데이터가 수신된 상황
  • EPOLLRDHUP : 연결이 종료되거나 Half-close가 진행된 상황
  • EPOLLERR : 에러가 발생한 상황
  • EPOLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작시킨다.
  • EPOLLONESHOT : 이 플래그가 설정된 파일 디스크립터에 이벤트가 발생하면, 커널은 해당 파일 디스크립터를 epoll 인스턴스에서 자동으로 비활성화한다. 이후 같은 파일 디스크립터의 이벤트를 다시 감지하려면 명시적으로 epoll_ctl 함수를 호출하며 EPOLL_CTL_MOD를 인자로 넘겨 재등록해야 한다.

 

 

 

epoll_create

epoll 인스턴스를 생성한다.

int epoll_create(int size);
  • size : epoll 인스턴스의 크기 결정 시 참고로만 사용되는 파라미터였으나 현대 리눅스에서는 무시된다. 
  • 리턴 값 : 성공 시 epoll 파일 디스크립터를 반환하고 실패시 -1을 반환한다.

 

epoll_ctl

epoll 인스턴스에 파일 디스크립터를 추가, 수정 또는 삭제한다.

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd : epoll 파일 디스크립터
  • op : 수행할 작업, EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTR_DEL
  • fd : 대상 파일 디스크립터
  • event : 모니터링할 이벤트의 종류를 정의하는 epoll_event 구조체
  • 리턴 값 : 성공 시 0을 반환하고 실패시 -1을 반환한다.

 

epoll_wait

epoll 인스턴스를 사용하여 이벤트가 발생할 때까지 대기한다.

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

 

  • epfd : epoll 파일 디스크립터
  • events : 이벤트가 발생한 파일 디스크립터 정보를 저장할 배열
  • maxevents : 이벤트 배열의 최대 크기
  • timeout : 밀리초 단위의 대기시간, -1은 무한대기한다.
  • 리턴 값 : 발생한 이벤트 수를 반환하고 실패 시 -1을 반환한다.

 

 

 

 

epoll의 단계별 사용방식

1. epoll 인스턴스 생성 (epoll_create)

- 모니터링할 파일 디스크립터들을 관리할 epoll 인스턴스를 생성한다.

2. 파일 디스크립터 추가 (epoll_ctl)

- 생성된 epoll 인스턴스에 모니터링할 파일 디스크립터를 추가한다.

3. 이벤트 대기 (epoll_wait)

- 이벤트가 발생할 때까지 대기한다.

4. 이벤트 처리

- epoll_wait이 반환한 이벤트 수만큼 반복하여 각 파일 디스크립터의 이벤트를 처리한다.

 

 

 

 

 

 

 

 

 

 

 

epoll 사용 에코 서버 예제

echo_epollserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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");
	
    // epoll 인스턴스 생성
	epfd=epoll_create(EPOLL_SIZE);
    
    // epoll_event 구조체 배열 동적 할당
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
	
    // 서버 소켓의 읽기 이벤트 감지하도록 epoll 인스턴스에 추가 
	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 인스턴스에 추가
				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);
                    close(ep_events[i].data.fd);
                    printf("closed client: %d \n", ep_events[i].data.fd);
                }
                else if(str_len > 0)
                {
                	// 클라이언트에 데이터 에코
                    write(ep_events[i].data.fd, buf, str_len);    // echo!
                }
				else
                {
                	error_handling("read() error");
                }
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

 

 

echo_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;

	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(sock==-1)
		error_handling("socket() error");
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while(1) 
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
			break;

		write(sock, message, strlen(message));
		str_len=read(sock, message, BUF_SIZE-1);
		message[str_len]=0;
		printf("Message from server: %s", message);
	}
	
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

$ gcc echo_epollserv.c -o serv
$ gcc echo_client.c -o client
$ ./serv 9190
새 터미널
$ ./client 127.0.0.1 9190

 

 

 

 

출처 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍

 

 

✓ epoll의 엣지 트리거 방식 사용에 대해서...

select와 위의 epoll 예제는 모두 레벨 트리거 방식이다. 레벨 트리거는 운영체제 커널의 입력 버퍼에 데이터가 남아 있는 동안 계속해서 이벤트가 발생하는 방식으로, select는 레벨 트리거만 사용할 수 있지만, epoll의 경우 events 옵션에 EPOLLET 플래그를 지정하여 엣지 트리거 방식으로 사용할 수도 있다.

다만, 엣지 트리거 사용 시 주의할 점은 반드시 논블로킹 소켓으로 설정해 read, write에서 블로킹이 되지 않게 설정해야 하며, 이벤트가 발생하면 입력 버퍼에 남아 있는 데이터를 EAGAIN이 반환될 때까지 반복해서 모두 읽어야 한다. 그렇지 않으면 남은 데이터를 놓칠 수 있다. 이런 제약과 구현의 복잡함 때문에 일반적인 애플리케이션에서는 레벨 트리거 방식으로 충분하다. 반면, 대규모 연결을 처리하는 고성능 서버에서는 불필요한 wake-up 횟수를 줄여 성능을 최적화하기 위해 엣지 트리거 방식을 사용하기도 한다.

반응형