이 글은 윤성우의 TCP/IP 소켓 프로그래밍 책을 참고하였습니다.

순서

I. select의 단점

II. epoll 개념

III. epoll 관련 함수들 분석
1. epoll_create
2. epoll_ctl
3. epoll_wait

IV. 서버 구현


I. select 함수의 단점

앞서 select를 설명하며 특이한 부분이 있었다.

바로 select를 호출해서 시그널을 볼 때 fd_set의 원본이 변경된다는 것이었다.
때문에 우리는 select를 호출할때는 항상 복사본을 사용했었다.

하지만 이것은 select기반의 IO 멀티플렉싱이 느린 이유가 된다.

매번 운영체제에게 관찰 대상을 인자로 전달해야하기 때문이다.

따라서 이런 개념이 필요하다

" 운영체제에게 관찰대상 정보를 딱 한번 알려주고, 변화가 있을 때만 변경 사항만 알려주도록 하자!!"
--> epoll

그렇다면 select는 쓸모없는가??
--> NO!

select는
1. Window, Linux 모두 동일하게 사용 가능하고 (다양한 운영체제에서 사용)
2. 서버의 접속자 수가 많지 않을 때

사용하기 좋다.


II. epoll 개념

위와같이 진행하기 위해서는 막 되는 것이 아니라, 운영체제가 OK~ 해야한다.

그렇기 때문에 운영체제에 따라서 지원여부, 방식도 차이가 있음을 기억하자.
(리눅스 : epoll, 윈도우 IOCP)

epoll의 장점은 다음과 같다
1. 전체 파일 디스크립터를 대상으로하는 반복문이 필요 없다.
2. select 함수에 대응하는 epoll_wait 함수호출 시, 관찰 대상의 정보를 매번 전달할 필요 없다.

epoll의 함수들은 다음과 같다.

#include <sys/epoll.h>

1. epoll_create : epoll fd 저장소 생성

2. epoll_ctl    : 저장소에 fd 등록 및 삭제

3. epoll_wait   : select함수와 마찬가지로 fd의 변화 대기

select에서는 fd_set형 변수를 직접 선언했다면, epoll은 운영체제가 알아서 담당해준다.

따라서 저장소 생성을 운영체제에 요청하는 함수가 1번이다.

그렇다면 epoll은 어떤 구조체나 자료형으로 이벤트들을 파악하는 것일까?
그 구조체는 다음과 같다.

struct epoll_event
{
    __unit32_t    events;
    epoll_data_t  data;
};

typedef union epoll_data
{
    void* ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

epoll_event 기반의 배열을 넉넉하게 선언하게되고,
상태변화가 발생한 fd의 정보가 이 배열에 별도로 묶이게 된다.

따라서 select처럼 반복해서 전체 fd를 대상으로 구현하는 것이 불필요해진다.

 


III. epoll 관련 함수들 분석

1. epoll_create

fd의 저장소(epoll 인스턴스)를 생성한다

#include <sys/epoll.h>

int epoll_create(int size);
    --> 성공 시 epoll 파일 디스크립터, 실패 시 -1 반환
    
    ㄴ. size : epoll 인스턴스의 크기정보
    
* 리눅스 커널 2.6.8 이후부터 전달인자 없음.

fd를 저장소에 모아두는데, 이 저장소를 "epoll 인스턴스"라고 한다.

int size는 인스턴스의 크기를 전달한다.

그러나 리눅스 커널 2.6.8 이후로는 전달 인자가 없어도 된다.


2. epoll_ctl

epoll 인스턴스에 관찰대상이 되는 fd를 등록, 삭제한다.

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
    --> 성공 시 0, 실패 시 -1 반환
    
    ㄴ. epfd  : 관찰대상을 등록할 epoll 인스턴스의 파일 디스크립터
                (cpoll_create의 반환값)
    ㄴ. op    : 관찰대상의 추가, 삭제 또는 변경 여부를 지정한다.
    ㄴ. fd    : 등록할 관찰대상의 파일 디스크립터
    ㄴ. event : 관찰대상의 관찰 이벤트 유형

 

인자1. int epfd

epoll_create를 통해 반환된 epoll 파일 디스크립터가 이에 해당된다.


인자2. int op

관찰대상의 추가, 삭제, 변경 여부를 지정한다. 이에 대한 상수정보는 다음과 같다.

EPOLL_CTL_ADD : fd를 epoll 인스턴스에 "등록"
EPOLL_CTL_DEL : fd를 epoll 인스턴스에서 "삭제"
EPOLL_CTL_MOD : 등록된 fd의 이벤트 발생상황 "변경"

 

인자3. int fd

등록할 파일 디스크립터

인자4. struct epoll_event* event

주의할 점은 여기서의 event는 "어떤 이벤트를 확인할 것인가"에 대한 이벤트 구조체 변수의 주소이다.

따라서 변화가 생긴 fd를 저장하는 event 구조체는 나중에 따로 선언해주어야 한다.

이 인자로 전달하기 전에 epoll_event 변수를 선언해서 두 멤버에 값을 넣어주어야 한다.

코드로 보자

#include <sys/epoll.h>

struct epoll_event myevent; // 변수선언

myevent.event = EPOLLIN; // event멤버에 원하는 것 등록
myevent.data.fd = fd; // 파일 디스크립터 설정

epoll_cnt(epfd, EPOLL_CTL_ADD, fd, &myevent); // 함수 선언모습


EPOLLIN    : 수신할 데이터가 존재하는 상황
EPOLLOUT   : 출력버퍼가 비워져서 당장 데이터 송신이 가능한 상황
EPOLLPRI   : OBB 데이터가 수신된 상황
EPOLLRDHUP : 연결이 종료되거나 Half-close가 진행된 상황 -> 엣지트리거에 유용
EPOLLERR   : 에러가 발생된 상황

위의 이벤트들은 event에 저장가능한 상수들이고, 비트 "or" 연산자를이용해 둘 이상을 함께 등록할 수 있다.

 


3. epoll_wait

파일 디스크립터의 변화를 대기하는 함수

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
    --> 성공 시 발생한 fd의 수, 실패 시 -1 반환
    
    ㄴ. epfd       : 이벤트 발생의 관찰영역인 epoll 인스턴스의 파일 디스크럽터(epfd)
    ㄴ. events     : 이벤트가 발생한 fd가 채워질 버퍼의 주소 값
    ㄴ. maxevents  : 두 번째 인자로 전달된 주소 값의 버퍼에 등록 가능한 최대 이벤트 수
    ㄴ. timeout    : ms 단위(1/1000초)의 대기시간, -1 전달 시 무한대기

인자1. int epfd

epoll_create 함수로 반환된 epoll 인스턴스의 파일 디스크립터 (epfd)

인자2. struct epoll_event* events

앞서 epoll_ctl을 위한 event 변수는 정보를 등록해서 보고자 하는걸 세팅했었다.

여기서의 event는 epoll_event의 주소값으로 이벤트가 발생했을 때 그 fd가 담길 버퍼이다. 잘 구별하자!!

중요한 점은 epoll_event* 자체를 변수로 선언하고 동적할당을 해주어야 한다. 

인자3. int maxevents

두 번째 인자인 events라는 버퍼에 최대 몇개의 이벤트를 담을건지 갯수를 전달한다.

인자4. int timeout

1ms단위의 타임아웃 시간을 전송한다. 특별한 점은 -1을 전달시 무한 대기한다.

코드로 보자

#include <sys/epoll.h>
#define EPOLL_SIZE 50

struct epoll_event* ep_events_buf; // 주소 변수 정의

ep_events_buf = malloc(sizeof(struct epoll_event)* EPOLL_SIZE); // 크기만큼 동적할당

event_cnt = epoll_wait(epfd, ep_events_buf, EPOLL_SIZE, -1); // 함수 선언 방법

 


IV. 서버 구현

기본 서버구현은 생략하고 epoll함수의 구현 위주로 구현한 코드이다.

<hide/>
#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

int main()
{
    // 기본 소켓 프로그래밍 변수들
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t addr_sz;
    int str_len, i;
    char buf[BUF_SIZE]
    
    // epoll위한 변수들
    struct epoll_event* ep_events_buf; // 변화를 담을 버퍼의 주소!
    struct epoll_event userevent; // 등록하기 위한 변수!
    int epfd, event_cnt;
    
    1. socket();
    2. bind();
    3. listen();
    
    // 여기서부터 epoll 시작
    
    epfd = epoll_create(EPOLL_SIZE); // epoll 인스턴스 생성
    ep_events_buf = malloc(sizeof(struct epoll_event)* EPOLL_SIZE); // 버퍼 동적할당
    
    userevent.events = EPOLLIN; // 수신관련한 이벤트를 보고싶다.
    userevent.data.fd = serv_sock; // 서버 fd를 등록할 것이다.
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &userevent); // ADD로 등록, 서버fd, 원하는 이벤트
    
    while(1)
    {
        // 무한 대기하며 epfd중에 변화가 있는 fd 확인. 확인 후 event 버퍼에 넣음.
        event_cnt = epoll_wait(epfd, ep_events_buf, EPOLL_SIZE, -1);
        
        if(event_cnt ==-1) 
        {
            puts("wait() error!\n");
            break;
        }
        
        // 확인된 변화의 갯수만큼 for문
        for(i=0; i<event_cnt; i++)
        {
            // 만약 이벤트가 생겼고 그 fd가 서버라면 -> 연결요청임!
            if(ep_events_buf[i].data.fd == serv_sock)
            {
                addr_sz = sizeof(clnt_addr);
                clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &addr_sz) // 이때 accept!!
                
                // 클라이언트의 fd를 등록해주는 과정
                userevent.events=EPOLLIN;
                userevent.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &userevent);
                
                printf("connected client : %d\n",clnt_sock);
            }
            // 서버가 아니라면 -> 클라이언트의 변화
            else
            {
                str_len = read(ep_events_buf[i].data.fd, buf, BUF_SIZE); // 디스크럽터 접근방법 집중!
                if( str_len ==0) // close socket
                {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events_buf[i], NULL);
                    close(ep_events_buf[i].data.fd);
                    printf("Close client : %d\n", ep_events[i].data.fd);
                }
                else 
                {
                    write(ep_events_buf[i].data.fd, buf, str_len);
                    //정상 수신되면 원하는 작업들~~
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}
                

+ Recent posts