개발/CS

리눅스 select, poll, epoll

하프킴 2021. 5. 16. 20:36
728x90

알아보기 전에 꼭 알야아 할 키워드들이 있다.

File Desciptor : 리눅스 혹은 유닉스 계열의 시스템에서 프로세스가 파일을 다룰 때 사용하는 개념. 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값. 프로세스에서 열린 파일의 목록을 관리하는 테이블의 인덱스.

리눅스(유닉스) 에서는 모든것을 파일로 취급한다.(파일, 소켓 등) 각각의 프로세스는 File desciptors의 테이블을 가지고 있다.

 

IO multiplexing : 하나의 통신 채널을 통해서 둘 이상의 데이터를 전송하는 기술, 물리적 장치의 효율성을 높이기 위해, 최소한의 물리적 요소만을 이용하여, 최대한의 데이터를 전달하기 위해 사용되는 기술.

 

멀티플렉싱이 필요한 이유는, 각 파일을 처리할 때 각각의 io통로를 통로를 만들어 각각의 프로세스와 스레드를 만들게 되면 아래와 같은 단점이 있다.

  • 프로세스간의 통신을 위해 IPC가 필요하다.
  • 프로세스 동기화, 스레드를 동기화 해야한다.
  • 컨텍스트 스위칭 등의 오버헤드가 있을 수 있다.

 

그래서 이와 같은 단점을 보완하기 위해서, 하나의 채널을 통해 둘 이상의 데이터를 송수신 하여, 프로세스의 갯수를 최소한으로 유지하면서 여러개의 파일을 처리하는 방법인 IO 멀티플렉싱이 등장하였다.

 

위와 같이 멀티플렉싱을 통해 여러개의 파일을 다루기 위해 fd를 배열을 통해 관리한다. 

데이터 변경을 감시할 fd를 배열에 넣고, 배열에 포함된 fd에 변경(읽기, 쓰기, 에러)등이 발생하면, fd에 대응되는 배열에 flag를 표시하는 방법으로 동작한다.

즉 개발자는 fd 배열을 통해 여러개의 파일을 감시하고 처리할 수 있게 된다.

fd 배열의 fd의 변화 여부가 위와 같이 기록된다.

 

System Call : 응용프로그램에서 운영체제에게 시스템 자원을 요청하는 하나의 수단.

  • 처리방식 : 시스템콜을 요청하면 제어가 커널로 넘어가여, 내부적으로 각각의 시스템 콜을구분하기 위해 기능별로 고유한 번호를 할당해 놓는다. 그리고 그 번호에 맞는 서비스 루틴을 호출하고, 처리하고 나면 커널 모드에서 사용자 모드로 넘어옴.
  • 프로세스제어, 파일조작, 장치관리, 시스템 정보 및 자원관리, 통신 관련등이 있다.

 

polling : 하나의 장치 또는 프로그램에서 충돌을 피하거나 동기화 처리 목적으로 다른 장치나 프로그램의 상태를 주기적으로 검사해서 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식. 이 방식은 버스와 같이 여러개의 장치가 동일 회선을 사용하는 상황에서 주로 사용함 .

 

 

이제 본격적으로 select, poll, epoll에 대해서 알아보자.

 

 

*select란

싱글스레드로 여러개의 파일을 작업하고자 할 때 사용할 수 있는 메커니즘.

 

입출력을 관리하고자 하는 파일의 그룹을  fd_set이라는 파일 비트 배열에 집어넣고, 이 배열의 값이 변했는지 확인하는 방식으로 동작한다.

 

<sys/select.h>에 아래와 같이 구현되어 있다

     int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds,
         fd_set *restrict errorfds, struct timeval *restrict timeout);
         

성공시 준비된 fd 갯수를 리턴함. 타임아웃시 0, 오류 시 -1을 리턴함.

fd가 입출력을 수행할 준비가 되거나 정해진 시간이 경과할 때 까지 block된다.

 

매개변수는 아래와 같다.

  • ndfs : 감시할 fds의 갯수
  • readfds : 읽을 데이터가 있는지 검사하기 위한 파일 목록
  • writefds : 쓰여진 데이터가 있는지 검사하기 위한 파일 목록
  • exceptfds : 파일에 예외 사항들이 있는지 검사하기 위한 파일 목록
  • timeout : 데이터변화를 감시하기 위해서 사용하는 time-out. null이면 계속 대기함.

fd_set 구조체는 1024 크기를 가지는 비트 배열을 포함하는데, 파일 지정 번호는 각 비트 배열 첨자에 대응하는 구조를 가지고 있다. 예를 들어 파일 지정번호가 3이면 4번째 비트 배열에 대응된다.

 

만약 변경된 데이터가 있으면 해당 비트값이 1로 설정되고, 이 비트 값을 검사함으로써 어떤 파일 지정 번호에 변경된 데이터가 있는지 확인해서 읽기/쓰기를 하면 된다.

 

 

 

아래와 같은 매크로를 지원한다.

FD_ZERO(fd_set* set);        //fdset 을초기화
FD_SET(int fd, fd_set* set);  //fd를 set에 등록.
FD_CLR(int fd, fd_set* set);  //fd를 set에서 삭제. 더이상 검사하지 않음.
FD_ISSET(int fd, fd_set* set);//fd가 준비되었는지 확인

 

 

 

아래와 같이 예시의 일부를 보면 while루프 안에서, FD_ISSET매크로를 통해 fd배열에 변화가 있는지 검사하고 있다.

  while(1){
        FD_SET(fd,&readfds);

        ret = select(fd+1, &readfds, NULL, NULL, NULL);

        if(ret == -1){
                perror("select error ");
                exit(0);
        }

        if(FD_ISSET(fd, &readfds)){
                while(( n = read(fd, buf, 128)) > 0)
                        printf("%s",buf);
        }

        memset(buf, 0x00, 128);
        usleep(1000);
  }

 

 


그라고 fd_set을 만들어 그 set에 속한 fd중 하나라도 입력이 들어오면 block이 해제되고 원하는 루틴을 수행할 수 있다.

 

 

select의 단점

  • 감시하고자 하는 이벤트 설정을 변형하기 때문에 매번 이벤트 비트를 새로 설정해야 하는 불편함이 있다.
  • fd를 하나하나 체크해야헤서 On의 계산양이 필요함.  따라서 관리하는 fd의 수가 증가하면 성능이 떨어짐
  • 사용자가 selector.selectedKeys ()를 호출하면 운영 체제는 모든 소켓을 검색하여 시스템 커널에서 사용자의 메모리로 복사한다. 연결 수가 증가하면 순회 및 복제 시간이 선형 적으로 증가하고 메모리 소비가 증가한다.
  • fd 길이에 제한이 있음

 

*poll이란

 

함수를 통해 지정한 소켓의 변화를 확인할 때 쓰이는 함수

소켓셋의 저장된 소켓의 변화가 생길 때 까지 기다리고 있다가, 소켓이 어떤 동작을 하면 동작한 소켓을 제외한 나머지 소켓을 모두 제거하고, 해당 데이터 링크의 확립 방법 중 하나이다.

 

select와 동일하게 단일 프로세스에서 여러 파일의 입출력이 가능함.

select의 단점을 개선함. 관심있는 fd만 넘겨줄 수 있고, 감시하고 있는 이벤트도 보전이 된다.

하지만 감시하고 있는 모든 fd에 대해서 루프를 돌면서 체크를 해야하는 단점은 여전히 존재.

또한 하나의 fd 이벤트를 전송하기 위해 64비트를 전송해야 하는 단점도 있음.

timeval이라는 구조체를 사용해 타임아웃값을 세팅하지만, poll은 별다른 구조체 없이 타임아웃 기능을 지원함.

 

 

 

함수는 아래와 같이 정의 되어 있고, 매개변수에 pollfd라는 구조체가 있다.

int poll(struct poolfd *ufds, unsigned int nfds, int timeout);

 

pollfd의 구조체에 대해서 알아보면

3개의 멤버 변수가 있고, 이 구조체에 fd를 세팅하고, fd가 어떤 이벤트가 발생할 것인지 이벤트를 지정한다.

 

그럼 poll은 fd에 해당 events가 발생하는지를 검사하게 되고, 해당 events가 발생하면 revents를 채워서 돌려주게 된다. revents는 events가 발생했을 때 커널에서 이 events에 어떻게 반응했는지에 대한 반응값이다. 

 

struct pollfd
{
	int fd;         // 관심있어하는 파일지시자
	short events;   // 발생된 이벤트
	short revents;  // 돌려받은 이벤트
};

 

그리고 event, revents는 아래의 값들을 가질 수 있다. 

<sys/poll.h>에 디파인 되어 있다.

  POLLERR        An exceptional condition has occurred on the device or
                    socket.  This flag is output only, and ignored if present
                    in the input events bitmask.

     POLLHUP        The device or socket has been disconnected.  This flag is
                    output only, and ignored if present in the input events
                    bitmask.  Note that POLLHUP and POLLOUT are mutually
                    exclusive and should never be present in the revents bit-mask bitmask
                    mask at the same time.

     POLLIN         Data other than high priority data may be read without
                    blocking.  This is equivalent to ( POLLRDNORM | POLLRDBAND
                    ).

     POLLNVAL       The file descriptor is not open.  This flag is output
                    only, and ignored if present in the input events bitmask.

     POLLOUT        Normal data may be written without blocking.  This is
                    equivalent to POLLWRNORM.

     POLLPRI        High priority data may be read without blocking.

     POLLRDBAND     Priority data may be read without blocking.

     POLLRDNORM     Normal data may be read without blocking.

     POLLWRBAND     Priority data may be written without blocking.

     POLLWRNORM     Normal data may be written without blocking.

 

poll의 동작 방식은 아래의 시나리오와 같다.

 

  • pollfd에 입력된 fd의 event에 입력 event가 발생하면, 커널은 입력 event에 대한 결과를 되돌려준다.
  • 만약, 이 결과는 입력된 event가 제대로 처리되었다면 POLLIN을 리턴해준다. 하지만 에러가 발생하면 POLLERR을 되돌려준다.
  • 그러므로 revent를 검사함으로써, 해당 fd에 읽을 데이터가 있다는 것을 알 수 있게 된다.

 

 

* epoll

(ms에서는 iocp, freebsd계열에서는 kqueue가 있다)

select의 단점을 극복하기 위해 커널 레벨의 멀티플렉싱을 지원함. 

fd에 대한 루프를 돌지 않고, 커널에게 정보를 요청하는 함수를 호출할 때마다 전체 관찰 대상에 대한 정보를 넘기지도 않는다.

또한 epoll_wait함수를 호출하면 관찰 대상의 정보를 매번 전달할 필요가 없다.

이 부분을 직접 운영체제에서 담당하고, 관찰 대상의 저장소를 만들어달라고 OS에 요청하면 그 저장소에 해당하는 fd를 리턴해준다.

관찰 대상 범위가 달라지면 epoll_fd를 통해 확인을 한다. 따라서 전체 fd 리스트를 순회하면서 FD_ISSET을 하지 않아도 된다.

 

select의 단점을 개선하였지만, 프로세스가 커널에게 지속적으로 IO상황을 체크하여 동기화 하는 개념은 여전히 유효하다.

커널에 관찰대상에 대한 정보를 한번만 전달하고, 관찰댓아의 범위, 또는 내용에 변경이 있을 때만 변경 사항을 알려줌.

 

커널 공간이 fd를 관리하여 select보다 빠른 이벤트 처리가 가능함.

epoll_create를 통해 epoll 구조체를 생성하고 epoll_ctl을 통해서 디스크립터를 등록 수정 삭제하고, epoll_wait를 통해 파일 디스크립터의 변화를 탐지한다.

 

위의 동작을 위해서는 3가지 요청이 필요하다.

 

fd들의 io이벤트 저장을 위한 공간을 만드느 함수. 매개변수로 size만큼의 io이벤트를 저장할 공간을 만든다.

 

int epoll_create(int size); //size는 epoll_fd의 크기정보를 전달한다.
//반환 값 : 실패 시 -1, 일반적으로 epoll_fd의 값을 리턴

 

관찰 대상이 되는 fd를 등록, 삭제할 때 사용됨.

int epoll_ctl(int epoll_fd,             //epoll_fd
              int operate_enum,         //어떤 변경을 할지 결정하는 enum값
              int enroll_fd,            //등록할 fd
              struct epoll_event* event //관찰 대상의 관찰 이벤트 유형
              ); 
//반환 값 : 실패 시 -1, 성공시 0

 

  • 매개변수
    • epfd : epoll fd값
    • op : 관심가질 fd를 등록할지, 등록되어 있는 fd의 설정을 변경할지, 등록되어 있는 fdf를 관심 목록에서 제거할건지에 대한 옵션값.
      • EPOLL_CTL_ADD : fd를 목록에 추가. 이미 존재하면 EEXIST에러 발생
      • EPOLL_CTL_MOD : fd 설정 변경. 목록에 없으면 ENOENT 에러를 발생시킴
      • EPOLL_CTL_DEL : fd를 제거함.목록에 없으면 ENOENT 에러를 발생.
    • fd : edfd에 등록할 관심있는 fd값
    • event : edfd에 등록할 관심있는 fd가 어떤 이벤트가 발생할 때 관심을 가질지에 대한 구조체. 관찰 대상의 관찰 이벤트 유형. 아래와 같은 구조채로 선언되어 있음.
      • 상태변화가 발생한 fd의 정보가 이 배열에 별도로 묶이게 된다.
      • 따라서 상태 변화에 대해 일일이 반복문을 돌려 확인할 필요가 없다.
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_wait은 관심있는 fd의 변화를 감지한다. 이벤트의 배열을 전달하고, 리턴값은 발생한 사건들의 갯수를 리턴한다는 점에서 select, poll과 차이가 있다.

int  epoll_wait(int  epfd,  struct epoll_event * events, int maxevents, int timeout)
  • 매개변수
    • events: 이벤트가 발생된 fd들을 모아놓은 구조체 배열
    • maxevents : 실제 동시 접속수와 상관없이 최대 몇개까지의 event만 처리할 것임을 지정해주도록 함.
    • timeout : 밀리세컨드 단위로 지정할 수 있다. -1이면 영원히 기다리는 blocking상태가 되고, 0이면 즉시 조사만 하고 리턴한다.

 

  • 이벤트 탐지 방법
    • level : 특정 상태가 유지되는 동안 감지. 
    • edge trigger : 특정 상태가 변화하는 시점에서만 감지. 

 

 

더 알아보기

java에서는 epoll을 어떻게 활용할까?
java.nio.channels.SelectorProvider의 Linux의 epoll notification을 활용한다

 

SelectorProvider :

 

nio에서 Selector은 여러 SelectableChannel을 자신에게 등록하게 하고

등록된 SelectableChannel의 이벤트 요청들을 나눠서 적절한 서비스 사용자에게 보내 처리하는 멀티플렉스 IO를 가능하게 한다.

 

특히 새로운 epoll 기반의 SelectorProvider는 예전의 poll 기반의 SelectorProvider 구현보다 더 확장성이 좋다.

리눅스 커널 2.6 이면 디폴트로 epoll기반으로 설정된다. 그 미만의 버전에서는 poll 기반으로 설정됨.

 

selector* : nio에서 여러개의 채널에서 이벤트를 모니터링 할 수 있는 객체. 하나의 스레드에서 여러 채널에 대해 모니터링이 가능함.

 

SelectableChannel이 수천개 정도 있을 때 기존의 poll 기반의 SelectorProvider보다 확장성이 뛰어남.

 

javadoc에 아래와 같이 기술되어 있다.

 

A new java.nio.channels.SelectorProvider implementation that is based on the 
Linux epoll event notification facility is included.
The epoll facility is available in the Linux 2.6, and newer, kernels.
The new epoll-based SelectorProvider implementation is more scalable 
than the traditional poll-based SelectorProvider implementation 
when there are thousands of SelectableChannels registered with a Selector.
The new SelectorProvider implementation will be used by default
when the 2.6 kernel is detected. 
The poll-based SelectorProvider will be used when a pre-2.6 kernel is detected.

 

 

출처  : https://reakwon.tistory.com/117

https://docs.oracle.com/javase/8/docs/technotes/guides/io/enhancements.html

https://duksoo.tistory.com/entry/System-call-%EB%93%B1%EB%A1%9D-%EC%88%9C%EC%84%9C

https://niklasjang.github.io/backend/select-poll-epoll/

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/poll.2.html

https://www.joinc.co.kr/w/Site/system_programing/File/select

https://ozt88.tistory.com/21