TCP-IP 소켓 프로그래밍

epoll을 이해해보자

코딩봇치 2023. 9. 12. 20:15

epoll???


epoll은 select의 단점을 보완하기 위해 만들어진 I/O통지 모델이다!
그럼 select의 단점부터 알아보자

select의 단점

  • select 후 모든 디스크립터 대상으로 반복문을 실행한다.
  • select함수는 호출 할 때마다 인자로 관찰대상을 전달해야한다.

우리는 두 번째 단점에 집중 해 볼 것이다.
select함수는 커널단의 힘을 더해 완성되는 기능이아닌 순수함수로 동작하기 때문에 select 함수 호출에 전달된 정보는 운영체제에 등록되지 않는다. 그래서 select를 실행 할 때 마다 항상 운영체제에게 관찰대상에 대한 정보를 전달 해주어야한다.

이를 보완하기 위해 만들어진 것이 epoll이다.
운영체제에게 관찰대상에 대한 정보를 딱 한 번만 알려주고, 바뀔 때만 알려주자!
epoll은 처음 관찰 대상 등록 후 변경 될 때만 운영체제에게 보고한다.

epoll의 단점

그럼 select는 아예 안쓰이는 것일까? 그것은 또 아니다. epoll도 단점이 존재한다.

  • epoll 방식은 리눅스에서만 지원된다.
    여러 운영체제에서 운영을 해야한다면 epoll이 아닌 select를 사용해야할지도 모른다.

epoll 동작 함수


epoll을 동작 시키기 위한 함수에 대해 알아보자.

  • epoll_create: epoll 파일 디스크립터 저장소 생성
  • epoll_ctl: 저장소에 파일 디스크립터 등록 및 삭제
  • epoll_wait: select함수의 역할을 하는 함수, 파일 디스크립터(소켓)의 변화를 대기

epoll_create

#inlcude <sys/epoll.h>

int epoll_create(int size);

-> 성공 시 epoll 파일 디스크립터, 실패 시 -1을 반환함.

epoll_create호출 시 파일 디스크립터를 저장하는 'epoll 인스턴스'가 생성된다.
size를 통해 epoll인스턴스의 크기를 정할 수 있는데, size 값으로 생성되는 것이 아닌 저 수를 참고하여 조정된 크기로 생성이된다. (최근 리눅스 버전에선 아예 자동화 되어 그냥 쓸모없는 값이 돼버렸다)

소멸 시 close 함수 호출이 필요하다.

epoll_ctl

#inlcude <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, strct epoll_event *event);
  • epfd: 관찰 대상을 등록할 epoll 인스턴스의 파일 디스크립터
  • op: 관찰 대상의 추가, 삭제 여부 설정
  • fd: 등록할 관찰대상의 파일 디스크립터
  • event: 관찰대상의 관찰 이벤트 유형

epoll 인스턴스에 파일 디스크립터를 등록하는 함수이다.

op인자

op인자를 통해 추가,삭제를 지정할 수 있다.

  • EPOLL_CTL_ADD: 파일 디스크립터 추가
  • EPOLL_CTL_DEL: 파일 디스크립터 삭제
  • EPOLL_CTL_MOD: 등록된 파일 디스크립터 이벤트 발생상황 변경

event 인자

event 인자를 통해 어떤 이벤트를 보고할 건지 입력하는 함수이다.

  • EPOLLIN: 수신할 데이터가 존재하는 상황
  • EPOLLOUT: 출력버퍼가 비워져서 당자 데이터를 전송할 수 있는 상황
  • EPOLLPRI: OBB 데이터가 수신된 상황
  • EPOLLRDHUP: 연결 종료, Half-close가 진행된 상황
  • EPOLLERR: 에러가 발생한 상황
  • EPOLLET: 이벤트의 감지를 엣지 트리거 방식으로 동작시킴
  • EPOLLONESHOT: 이벤트가 한번 감지되면, 해당 파일 디스크립터ㅓ에선 이벤트를 발생시키지 않는다. EPOLL_CTL_MOD로 재설정 해줘야함.

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

사용

struct epoll_event event
...
event.events = EPOLLIN;
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
...

epoll_wait

#include <sys/epoll.h>

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

-> 성공 시 이벤트가 발생한 파일 디스크립터의 수, 실패 시 -1을 반환함.

  • epfd: 이벤트 관찰영역인 epoll 인스턴스의 파일 디스크립터
  • events: 이벤트가 발생한 파일 디스크립터가 채워지는 버퍼 주소
  • maxevents 두 번째 인자로 전달딘 주소 값의 버퍼에 등록 가능한 최대 개수
  • timeout: 타임 아웃 시가 -1시 무한 대기

두 번째 인자로 전달되는 주솟값은 동적 할당 해야한다.

int event_cnt;
struct epoll_event* ep_events;
....
ep_events = malloc(sizeof(strct epoll_evnet)*EPOLL_SIZE); //EPOLL_SIZE는 매크로 상수 값이다.
....
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

함수 호출 후에 이벤트 발생 파일 디스크립터 수가 바환되고, 두 번째 인자로 전달한 주소 값에 이벤트가 발생한 타입 디스크립터의 정보가 별도로 나오기 때문에 select처럼 전체 파일 디스크립터를 대상으로 하는 반복문이 불필요하다!

레벨 트리거와 엣지 트리거

앞서 본 event인자에서 엣지 트리거라는 어휘를 봤을 것이다. 엣지트리거가 무엇인지, 그에 반하는 레벨 트리거는 무엇인지 각각의 특징을 알아보자.

엣지트리거와 레벨트리거는 이벤트 등록 방식을 말하는 것이다.

레벨 트리거

레벨트리거는 값이 존재하면 계속 이벤트를 등록한다.


이벤트: 입력버퍼에 5000바이트 남았어요.
.....
이벤트: 입력버퍼에 2000바이트 남았어요.
.....
이벤트: 입력버퍼에 2000바이트 남았어요.
.....
이벤트: 입력버퍼에 400바이트남았어요.
.....
.....

입력버퍼에 데이터가 없어져야 이벤트 등록을 그만한다.

엣지 트리거

엣지 트리거는 값이 생기는 그 순간 한 번만 이벤트를 등록한다. 엣지 트리거를 사용하기 위해선 소켓을 논블로킹으로 바꿔주어야한다.

이벤트: 입력 버퍼에 5000바이트 남았어요
.....
.....
.....

한번에 수신가능한 데이터가 2000이라면 2000을 처리하고, 남은 3000은 처리하지 않는다.(이벤트가 발생하지 않기 때문)

뭐가 더 좋아?

얼핏 보면 남아있는 데이터를 모두 처리해주는 레벨 트리거가 좋다고 생각할 수 있다. 하지만 엣지트리거에게도 장점이 존재한다.

"데이터의 수신과 데이터 처리 시점을 분리할 수 있다."

문장만 봐선 이해할 수 없다.
이러한 시나리오가 있다고 가정해보자.

  1. 서버는 두 클라이언트 A,B에게 데이터를 수신받는다.
  2. 데이터를 A,B순으로 조합한다.
  3. 클라이언트 C에게 전달한다.

하지만 이 상황 때 변수가 생기기도한다.

 

  1. A는 데이터를 전송했는데 B가 연결도 안 한 경우
  2. A,B가 순서가 바껴 오는 경우
  3. C가 접속을 안 한 경우

이 때 레벨 트리거라면 계속해서 이벤트를 발생시킬 것이다.

하지만 엣지 트리거는 그렇지않다. 이벤트가 등록된 상황에도 이 것을 처리하는 시점을 서버가 정하는 것은 엄청난 유연성을 부여한다.

마무리


epoll 방식을 알아봤다. 윈도우 사용자인 나에겐 굉장히 생소하고 이해되지 않는 것들이 많아 굉장히 애를 먹었다. 하지만 장점과 단점, select와의 다른점을 명확히 알고 있어야 유동적으로 쓸 수 있을 것이다!

참고