본문 바로가기
프로그래밍/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 소켓 프로그래밍