ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCP/IP 소켓 프로그래밍 - 17장 : select 보다 나은 epoll
    네트워크 2024. 6. 26. 22:16
     

    17장

    17-1 epoll의 이해와 활용

     

    select 기반의 멀티플렉싱 서버( 12장 )에는 코드 상의 문제점이 아래와 같이 드러난다.

    • select 함수 호출 이후, 모든 파일 디스크립터를 대상으로 하는 반복문
    • select 함수 호출할 때마다 인자로 매번 전달해야 하는 관찰대상에 대한 정보들

     

    반복문 보다도 관찰대상에 대한 정보들을 인자로 전달하는 부분이 성능 저하의 주요인이 될 수 있다.

    이유는, 관찰대상에 대한 정보를 운영체제에게 전달해주기 때문이다.

     

    select함수는 소켓의 변화를 관찰하는 함수지만,

    소켓은 운영체제에 의해 관리되는 대상임을 잊지 말자.

     

    운영체제에게 관찰대상에 대한 정보를 한번만 알려주고,

    변경사항이 있을 때만 변경사항만 알려주도록 select함수의 단점을 개선한 방식이

    리눅스에서는 epoll, 윈도우에서는 IOCP라고 한다.

     

     

    그러나 발전된 IO 멀티플렉싱 모델은 운영체제 별로 호환되지 않을 수 있다.

    select 함수는 대부분의 운영체제에서 지원하니 알아둘 필요가 있다.

    지금까지도 호환이 안되려나?

     

     

    epoll은 다음과 같은 상황이라면 굳이 쓸 필요가 없다.

    • 서버 접속자 수가 적다.
    • 다양한 운영체제에서 이식성이 좋아야 한다.

     

    epoll의 장점은 다음과 같다.

    • 상태 변화 확인을 위해서 전체 fd를 확인하는 반복문이 필요없다.
    • 관찰 대상 정보를 매번 전달할 필요가 없다.

     

    epoll은 다음의 세가지 함수가 필요하다.

    • epoll_create : epoll 파일 디스크립터 저장소 생성
    • epoll_ctl : 저장소에 파일 디스크립터 등록 및 삭제
    • epoll_wait : select 함수와 마찬가지로 파일 디스크립터의 변화 대기

     

    epoll 방식에서는 관찰 대상 (파일 디스크립터)의 저장을 운영체제가 담당한다.

    → select 방식은 파일 디스크립터를 fd_set형 변수에 담아 유저레벨에서 저장했다.

     

    이때, 저장소의 생성을 운영체제에게 요청하는 함수가 epoll_create이다.

     

    select 방식에서 FD_SET, FD_CLR 함수를 통해 관찰 대상 파일디스크립터 추가, 삭제를 진행했다면,

    epoll 방식에서는 epoll_ctl 함수를 통해 운영체제에게 원하는 작업(추가, 삭제)을 요청한다.

     

    select 방식에서는 전달한 fd_set형 변수의 변화를 통해 관찰대상의 상태변화를 확인한다면,epoll 방식에서는 구조체 epoll_event를 기반으로 상태변화가 발생한 파일 디스크립터가 따로 묶인다.

    ⇒ select는 fd_set형 변수에 변화가 생겼는지 반복문으로 찾아야 한다.

     

    epoll_wait 함수 호출 시 epoll_event 구조체 배열을 인자로 넘겨주면,

    이벤트가 발생한 파일디스크립터 정보가 이 배열에 묶이므로

    전체 파일 디스크립터를 살필 필요가 없다.

     

     

    epoll_create함수는 아래와 같다.

    #include <sys/epoll.h>
    
    int epoll_create(int size);
    //성공 시 epoll 파일 디스크립터, 실패 시 -1 리턴

     

    epoll 인스턴스는 epoll_create 함수호출로 생성된 파일 디스크립터 저장소이다.

     

    매개변수 size는 인스턴스 크기결정의 참고로만 사용된다.

     

    위 함수가 반환하는 파일 디스크립터는 epoll 인스턴스를 구분하기 위해 쓰인다.

    소멸을 원할 때는 close 함수를 호출하면 된다.

     

    epoll_ctl 함수는 아래와 같다.

    #include <sys/epoll.h>
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    //성공 시 0, 실패 시 -1 리턴
    
    //epfd : epoll 인스턴스의 파일디스크립터
    //op : 추가, 삭제, 변경여부
    //fd : 관찰 대상 파일 디스크립터
    //event : 관찰 이벤트 유형

    epoll_event 구조체는 다음과 같은 용도로 쓰인다.

    • 이벤트가 발생한 파일 디스크립터를 묶는 용도
    • 파일 디스크립터를 epoll 인스턴스에 등록할 때, 이벤트 유형을 등록하는 용도

    이벤트 유형은 아래와 같이 존재한다.

    • EPOLLIN : 수신할 데이터가 존재
    • EPOLLOUT : 당장 데이터 송신이 가능한 상황
    • EPOLLPRI : OOB 데이터 수신한 상황
    • EPOLLRDHUP : 연결 종료 또는 Half-close 진행된 상황. 엣지 트리거 방식에서 유용
    • EPOLLERR : 에러 발생
    • EPOLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작
    • EPOLLONESHOT : 이벤트가 감지되면 해당 파일 디스크립터는 이벤트 발생 x.

     

    epoll_wait함수는 아래와 같다.

    #include <sys/epoll.h>
    
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    
    //성공 시 이벤트 발생한 파일 디스크립터 수, 실패 시 -1 리턴
    //maxevents : events에 등록 가능한 최대 이벤트 수
    //timeout : 1/1000초 단위로, -1 전달 시 이벤트 발생까지 무한 대기

     

    이 함수에서 주목해야 하는 점은,

    두 번째 인자로 전달되는 주소 값의 버퍼를 동적으로 할당해야 한다는 것이다!

    int event_cnt;
    struct epoll_event * ep_events;
    ...
    ep_events = malloc(sizeof(struct epoll_event)* EPOLL_SIZE);
    ...
    event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1 );
    ...

     


     

    17-2 레벨 트리거와 엣지 트리거

     

    레벨 트리거 방식은

    입력 버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 등록된다.

    ( 참고로, select 방식은 레벨 트리거 방식이다. )

     

    엣지 트리거 방식은

    입력 버퍼로 데이터가 수신된 상황에서 딱 한번 이벤트가 등록된다.

     

     

    아래 그림은 처음 데이터를 수신했을 때, 두방식 모두 이벤트를 OS가 등록해주는 걸 나타냈다.

    위 방식은 레벨 트리거 방식이다. 입력 버퍼에 데이터가 남아있으므로 계속 이벤트 등록을 해준다.

     

    위 방식은 엣지 트리거 방식이다. 처음 수신할 때 제외하고는 이벤트 등록을 하지 않는다.

     

     

    예제대로 단순하게 엣지 트리거 방식으로 바꾼 경우, 처음 데이터를 받았을 때에 대해서만 이벤트를 등록해주기 때문에,

    그 뒤에 데이터를 바탕으로 클라이언트들에게 다시 echo 해주는 부분에 대해서는

    남아있는 버퍼의 데이터를 처리하는 것에 대한 event를 등록해주지 않아, (buf_size가 4였기 때문에 처리 안해주면 곤란..)

    원하는 결과를 얻지 못한다. (추가적인 장치가 필요하다!!!!) 

     



    엣지 트리거 기반 서버를 구현하기 위해서는 다음을 알아야 한다.

    • 변수 errno를 이용한 오류의 원인을 확인하는 방법
    • non-blocking IO를 위한 소켓의 특성을 변경하는 방법

    errno는 오류의 원인을 알 수 있게끔 설계되었다. (일반적으로 에러가 나면 -1 리턴하는 것 그 이상의 정보가 담겨있다. )

    함수 별로 오류 발생시 errno에 저장되는 값이 다르다.

     

    소켓을 non- blocking 모드로 바꾸기 위해서는 fcntl 함수를 사용해야 한다.

    아래는 fcntl 함수이다.

    #include <fcntl.h>
    
    int fcntl(int filedes, int cmd, ...);
    //filedes : 특성을 변경할 파일 디스크립터
    //cmd : 함수 호출 목적 정보 전달  

     

    cmd에 들어가는 값에 따라 파일 디스크립터의 특성 정보를 얻거나, 특성 정보를 변경할 수 있다.

    F_GETFL, F_SETFL을 사용하면 된다.

    int flag= fcntl(fd, F_GETFL,0);
    fcntl(fd, F_SETFL, flag|O_NONBLOCKING);

     

    첫 문장으로 기존 특성정보를 가져오고,

    두번째 문장으로 non-blocking 모드를 추가해 재설정해주고 있다.

    fcntl함수도 공부해보자.. 시스템 프로그래밍 공부해야겠지?

     

    다시 돌아가서, 엣지 트리거 방식에서 errno와 non-blocking 을 쓰는 이유를 살펴보면,

    엣지트리거 방식은 데이터 수신시 딱 한 번 이벤트를 등록하기 때문에,

    입력 버퍼에 저장된 데이터를 전부 읽어야 한다. 이때 쓰이는 것이 errno이다.

     

    read 함수가 -1 리턴하고, errno에 저장된 값이 상수 “EAGAIN”일 때,

    입력버퍼에 더이상 읽을 데이터가 없다는 뜻이고

    문제 없이 서버가 제역할을 할 수 있다.

     

    non-blocking은 ,

    엣지 트리거 방식의 특성상 무작정 blocking 방식의 read&write를 기다리는 것은

    시간 낭비가 될 수 있기 때문에 non-blocking 소켓 기반으로 read&write를 호출해야 한다.

     

     

    위는 엣지트리거 방식의 코드 예제 실행 결과이다.

    이따금 메시지를 처리하는 데, delay가 있기는 하지만 일단 다 보내주기는 한다.

    epoll_wait함수가 호출된 횟수는 처음 데이터를 수신한 횟수와 동일하다.

    (연결 요청 1회 + 메시지 전송 5회 )


     

    엣지 트리거 방식을 사용하면

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

    데이터의 수신과 처리되는 시점을 분리하고 서버가 이를 결정하는 것은

    서버 구현에 엄청난 유연성을 주는 것이다.

     

    클라이언트 1, 2, 3로 부터 데이터A, B, C를 서버가 받아와,

    ABC 순으로 또 다른 호스트에게 전달하고자 할 때,

    • 클라이언트 1, 3은 먼저 전송 중인데, 2는 아직 연결 조차 안한 경우
    • 클라이언트 1,2,3 이 순서를 바꿔서 데이터를 전송하는 경우
    • 서버가 데이터를 전송하고 있지만 아직 받기로 한 호스트가 연결되지 않은 경우

    등의 상황을 처리할 수 있다!!!

     

    레벨 트리거 방식으로는 입력 버퍼를 읽지 않으면 이벤트 발생 수가 계속 누적되기 때문에,

    이에 대한 처리가 현실적으로 불가능할 수도 있다.

     

    서버 구현 모델 측면에서 봤을 때, 엣지 트리거가 좋은 성능을 발휘할 확률이 높지만, 

    엣지 트리거를 적용했다고 해서 무조건 빨라지는 것은 아니다. 

Designed by Tistory.