NAME

epoll - I/O 이벤트 알림 기능

SYNOPSIS

#include <sys/epoll.h>

DESCRIPTION

epoll API는 poll(2)과 비슷한 작업을 한다. 즉 여러 파일 디스크립터를 감시해서 I/O가 가능한지 살펴본다. epoll API는 에지 트리거 인터페이스나 레벨 트리거 인터페이스로 사용할 수 있으며 감시하는 파일 디스크립터 수가 많아져도 잘 동작한다.

epoll API의 핵심에는 epoll 인스턴스라는 커널 자료 구조가 있는데, 사용자 공간에서 보자면 두 가지 목록을 담은 컨테이너라고 할 수 있다.

epoll 인스턴스 생성과 관리를 위한 다음 시스템 호출들이 제공된다.

레벨 트리거와 에지 트리거

epoll 이벤트 배포 인터페이스는 에지 트리거(ET: edge-triggered)로 동작할 수도 있고 레벨 트리거(LT: level-triggered)로 동작할 수도 있다. 두 메커니즘의 차이를 설명하기 위해 다음 사건들이 일어난다고 하자.

  1. 파이프의 읽기 쪽을 나타내는 파일 디스크립터(rfd)를 epoll 인스턴스에 등록한다.

  2. 파이프 쓰기 쪽에서 2 kB 데이터를 써넣는다.

  3. epoll_wait(2) 호출이 이뤄지고, 준비 상태인 파일 디스크립터로 rfd를 반환한다.

  4. 파이프 읽기 쪽에서 rfd로부터 1 kB 데이터를 읽는다.

  5. epoll_wait(2) 호출이 이뤄진다.

파일 디스크립터 rfdepoll 인터페이스에 추가할 때 EPOLLET(에지 트리거) 플래그를 사용했다면 파일 입력 버퍼에 데이터가 아직 있는데도 5번 단계의 epoll_wait(2) 호출에서 아마 멈출 것이다. 그동안 원격의 상대가 이미 보낸 데이터에 대한 응답을 기다리고 있을 수도 있다. 이렇게 되는 건 에지 트리거 모드에서는 감시 대상 파일 디스크립터에 변화가 생길 때만 이벤트를 내놓기 때문이다. 그래서 5번 단계에서 호출자가 이미 입력 버퍼 내에 있는 어떤 데이터를 기다리게 될 수가 있다. 위 예에서 rfd에 이벤트가 생성되는 건 2번에서 이뤄진 쓰기 때문이고 그 이벤트를 3번에서 소모한다. 4번에서 이뤄진 읽기 연산이 버퍼 데이터 전체를 소모하지 않으므로 5번 단계에서 이뤄지는 epoll_wait(2) 호출이 무한정 블록할 수도 있게 된다.

EPOLLET 플래그를 쓰는 응용에서는 블록하는 읽기나 쓰기 때문에 여러 파일 디스크립터를 처리하는 태스크가 굶게 되는 걸 피하기 위해 논블로킹 파일 디스크립터를 사용하는 게 좋다. epoll을 에지 트리거(EPOLLET) 인터페이스로 쓰는 권장 방식은 다음과 같다.

  1. 논블로킹 파일 디스크립터를 사용한다.

  2. read(2)write(2)EAGAIN을 반환한 후에 이벤트를 기다린다.

반면 레벨 트리거 인터페이스(EPOLLET를 지정할지 않았을 때의 기본 방식)로 쓸 때 epoll은 그냥 더 빠른 poll(2)이며, 같은 동작 방식을 공유하기에 poll(2)을 쓰는 곳 어디에든 쓸 수 있다.

에지 트리거 epoll을 쓰더라도 데이터를 여러 덩어리 수신하면 이벤트가 여러 개 생성될 수 있다. 이때 호출자가 EPOLLONESHOT 플래그를 지정해서 epoll_wait(2)으로 이벤트를 하나 수신한 다음에는 epoll에서 연계 파일 디스크립터를 비활성화하게 할 수도 있다. EPOLLONESHOT 플래그를 지정하는 경우 epoll_ctl(2) EPOLL_CTL_MOD로 파일 디스크립터를 재활성화하는 건 호출자의 몫이다.

여러 스레드가 (자식 프로세스가 fork(2)를 거치며 epoll 파일 디스크립터를 물려받은 경우라면, 여러 프로세스가) 같은 epoll 파일 디스크립터를 기다리며 epoll_wait(2)에서 블록돼 있는데 에지 트리거(EPOLLET) 알림 표시가 된 관심 목록의 한 파일 디스크립터가 준비 상태가 되면 그 스레드(프로세스)들 중 하나만 epoll_wait(2)에서 깨어난다. 이는 어떤 시나리오에서 "개떼처럼" 깨어나기를 막는 최적화 효과를 준다.

autosleep과의 상호작용

/sys/power/autosleep을 통해 시스템이 autosleep 모드에 들어가 있는데 이벤트가 발생해서 장치가 깨는 경우에 장치 드라이버는 이벤트가 큐에 들어갈 때까지만 장치를 깨워 두게 된다. 이벤트가 처리된 다음까지 장치를 깨워 두려면 epoll_ctl(2)EPOLLWAKEUP 플래그를 쓸 필요가 있다.

struct epoll_eventevents 필드에 EPOLLWAKEUP 플래그를 설정하면 이벤트가 큐에 들어가는 순간부터 그 이벤트를 반환하는 epoll_wait(2) 호출에 이어 그 다음 epoll_wait(2) 호출까지 시스템이 깨어 있게 된다. 그 시간 너머까지 시스템을 깨워 둬야 하는 경우에는 두 번째 epoll_wait(2) 호출 전에 따로 wake_lock을 잡으면 된다.

/proc 인터페이스

다음 인터페이스를 이용해 epoll에서 소모하는 커널 메모리 양을 제한할 수 있다.

/proc/sys/fs/epoll/max_user_watches (리눅스 2.6.28부터)
시스템의 모든 epoll 인스턴스들에 사용자가 등록할 수 있는 파일 디스크립터 총개수의 한계를 지정한다. 실제 사용자 ID별로 제한한다. 등록된 파일 디스크립터마다 32비트 커널에선 약 90바이트, 64비트 커널에선 약 160바이트가 든다. 현재 max_user_watches의 기본값은 사용 가능한 로우 메모리의 1/25(4%)를 등록당 드는 바이트 수로 나눈 것이다.

사용 예시

레벨 트리거 인터페이스로 쓸 때는 epoll 사용 방식이 poll(2)과 같지만 에지 트리거로 쓸 때는 응용의 이벤트 루프에서 멈추는 걸 막기 위해 더 명확한 처리가 필요하다. 이 예에서 연결을 받아들이는 소켓은 논블로킹 소켓에 listen(2)을 호출한 것이다. do_use_fd() 함수에서는 새로 준비된 파일 디스크립터를 EAGAIN이 반환될 때까지 read(2)write(2)로 사용한다. 이벤트 주도 상태 머신 응용에서는 EAGAIN 수신 후에 현재 상태를 기록해 둬서 다음 do_use_fd() 호출 때 중지 지점부터 read(2)write(2)를 계속할 수 있도록 해야 할 것이다.

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* 리슨 소켓 'listen_sock' 준비 코드
   (socket(), bind(), listen()) 생략. */

epollfd = epoll_create1(0);
if (epollfd == -1) {
    perror("epoll_create1");
    exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
    }

    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                               (struct sockaddr *) &addr, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                        &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
            do_use_fd(events[n].data.fd);
        }
    }
}

에지 트리거 인터페이스로 쓸 때 더 나은 성능을 위해 (EPOLLIN|EPOLLOUT)을 지정해서 epoll 인터페이스에 파일 디스크립터를 한 번만 추가(EPOLL_CTL_ADD)하는 게 가능하다. 그러면 EPOLL_CTL_MODepoll_ctl(2)을 호출하며 EPOLLINEPOLLOUT 사이를 계속 오가는 걸 피할 수 있다.

질문과 답변

  1. epoll 집합에 등록된 파일 디스크립터들을 구별하는 데 쓰는 키는 무엇인가?

    파일 디스크립터 번호와 열린 파일 기술 항목(열린 파일의 커널 내 표현, "열린 파일 핸들"이라고도 함)의 조합이 키다.

  2. 어떤 epoll 인스턴스에 같은 파일 디스크립터를 두 번 등록하면 어떻게 되는가?

    아마 EEXIST가 나올 것이다. 하지만 복제된 (dup(2), dup2(2), fcntl(2) F_DUPFD) 파일 디스크립터를 같은 epoll 인스턴스에 추가하는 건 가능하다. 복제 파일 디스크립터들을 다른 events 마스크로 등록하는 기법이 이벤트 필터링에 유용할 수 있다.

  3. epoll 인스턴스 두 개가 같은 파일 디스크립터에 대기할 수 있는가? 만약 그렇다면 이벤트가 두 epoll 파일 디스크립터 모두로 보고되는가?

    가능하며, 이벤트가 둘 모두로 보고될 것이다. 하지만 올바로 처리하려면 조심스런 프로그래밍이 필요할 것이다.

  4. epoll 파일 디스크립터 자체를 poll/epoll/select 할 수 있는가?

    가능하다. epoll 파일 디스크립터에 대기 이벤트가 있으면 읽기 가능한 것으로 표시된다.

  5. epoll 파일 디스크립터를 그 자체의 파일 디스크립터 집합에 넣으려고 하면 어떻게 되는가?

    epoll_ctl(2) 호출이 실패(EINVAL)한다. 하지만 epoll 파일 디스크립터를 다른 epoll 파일 디스크립터 세트에 추가할 수는 있다.

  6. epoll 파일 디스크립터를 유닉스 도메인 소켓을 통해 다른 프로세스로 보낼 수 있는가?

    가능하지만 의미가 없다. 수신 프로세스에서 관심 목록에 파일 디스크립터들의 사본이 없을 것이기 때문이다.

  7. 파일 디스크립터가 닫히면 epoll 관심 목록에서 제거되는가?

    그렇기는 한데 조심할 점이 있다. 파일 디스크립터는 열린 파일 기술 항목(open(2) 참고)에 대한 참조다. dup(2), dup2(2), fcntl(2) F_DUPFD, fork(2)를 통해 파일 디스크립터가 복제될 때마다 동일한 열린 파일 기술 항목을 가리키는 새 파일 디스크립터가 생겨나는 것이다. 그리고 열린 파일 기술 항목은 자신을 가리키는 파일 디스크립터들이 모두 닫힐 때까지 계속 존재한다.

    관심 목록에서 파일 디스크립터가 제거되는 건 기반 열린 파일 기술 항목을 가리키는 파일 디스크립터들이 모두 닫힌 후다. 따라서 관심 목록에 속한 파일 디스크립터가 닫힌 후에도 동일한 기반 열린 파일 기술 항목을 가리키는 다른 파일 디스크립터가 열려 있다면 그 닫힌 파일 디스크립터에 대한 이벤트 보고가 있을 수 있다. 이를 막으려면 파일 디스크립터를 복제하기 전에 (epoll_ctl(2) EPOLL_CTL_DEL로) 파일 디스크립터를 관심 목록에서 명시적으로 제거해야 한다. 아니면 응용에서 모든 파일 디스크립터들이 닫히도록 해야 한다. (하지만 안 보이는 곳에서 라이브러리 함수가 dup(2)이나 fork(2)를 써서 파일 디스크립터를 복제했다면 어려울 수도 있다.)

  8. epoll_wait(2) 호출 사이에 이벤트가 여러 개 발생하면 하나로 합쳐지는가, 아니면 따로 보고되는가?

    합쳐진다.

  9. 파일 디스크립터에 대한 어떤 동작이 이미 수집됐지만 보고되지는 않은 이벤트에 영향을 끼치는가?

    기존 파일 디스크립터에 취할 수 있는 동작은 두 가지가 있다. 제거 동작은 이 경우에 의미가 없을 것이다. 변경 동작은 가능한 I/O를 재확인하게 한다.

  10. EPOLLET 플래그(에지 트리거 동작)를 쓸 때 EAGAIN이 나올 때까지 계속해서 파일 디스크립터에 읽기/쓰기를 해야만 하는가?

    epoll_wait(2)으로 이벤트를 받았다는 건 요청한 I/O 동작에 대해 그 파일 디스크립터가 준비 상태라는 뜻일 것이다. 다음 번 (논블로킹) 읽기/쓰기가 EAGAIN을 내놓을 때까지 준비 상태라는 것으로 봐야 한다. 그 파일 디스크립터를 언제 어떻게 쓸지는 전적으로 프로그래머에게 달려 있다.

    패킷/토큰 지향 파일(가령 데이터그램 소켓, 정규 모드 터미널)에서 읽기/쓰기 I/O 공간의 끝을 알아내는 유일한 방법은 EAGAIN이 나올 때까지 읽기/쓰기를 계속하는 것이다.

    스트림 지향 파일(가령 파이프, FIFO, 스트림 소켓)에서는 대상 파일 디스크립터에 읽기/쓰기 된 데이터의 양을 확인하는 것으로 읽기/쓰기 I/O 공간이 고갈되었는지 알아낼 수도 있다. 예를 들어 read(2)를 호출해서 어떤 양의 데이터를 읽으라고 했는데 read(2)가 그보다 적은 바이트 수를 반환한다면 그 파일 디스크립터의 읽기 I/O 공간이 고갈됐다고 확신할 수 있다. write(2)로 쓰기를 할 때도 마찬가지다. (감시하는 파일 디스크립터가 항상 스트림 지향 파일을 가리킨다고 보장할 수 없다면 이 기법을 쓰지 말아야 한다.)

가능한 문제들과 회피 방법

굶주림 (에지 트리거)

아주 큰 I/O 공간이 있다면 그걸 비우려고 하는 동안 다른 파일들이 처리되지 않아서 기아를 유발하게 될 수 있다. (이건 epoll에 한정된 문제는 아니다.)

해법은 준비 상태인 디스크립터 목록을 유지하면서 그 파일 디스크립터의 연계 자료 구조에 준비 상태라고 표시해 두는 것이다. 그러면 처리해야 할 파일들을 응용에서 기억하면서 준비 상태인 파일들을 돌아가며 처리할 수 있다. 이렇게 하면 이미 준비 상태인 파일 디스크립터에 대해 이후 수신한 이벤트를 무시할 수 있기도 하다.

이벤트 캐시를 쓴다면...

이벤트 캐시를 쓴다면, 즉 epoll_wait(2)에서 반환된 파일 디스크립터들을 모두 저장한 다음 처리하려 한다면 동적으로 (즉 선행 이벤트 처리에 의해) 파일 디스크립터가 닫힌 걸 표시할 방법이 있어야 한다. 가령 epoll_wait(2)으로 100개 이벤트를 받았는데 47번 이벤트에서 어떤 조건 때문에 파일 디스크립터 13을 닫게 된다고 하자. 그냥 파일 디스크립터를 제거하고 close(2) 한다면 이벤트 캐시에서 그 파일 디스크립터에 대한 이벤트가 있다고 판단할 수도 있을 것이고 그래서 혼동이 생길 수 있다.

이에 대한 한 해법은 47번 이벤트 처리 동안 epoll_ctl(EPOLL_CTL_DEL)을 호출해서 파일 디스크립터 13을 삭제하고 close(2) 한 다음에 연계 자료 구조에 삭제 표시를 해서 제거 목록에 연결해 두는 것이다. 배치 처리 중 파일 디스크립터 13에 대한 다른 이벤트를 발견하면 그 파일 디스크립터가 이미 제거되었음을 알게 될 것이고 혼동이 없을 것이다.

VERSIONS

리눅스 커널 2.5.44에서 epoll API가 도입되었다. glibc 버전 2.3.2에서 지원이 추가되었다.

CONFORMING TO

epoll API는 리눅스 전용이다. 몇몇 다른 시스템에서도 비슷한 메커니즘을 제공하는데, 예를 들어 FreeBSD에는 kqueue가 있고 솔라리스에는 /dev/poll이 있다.

NOTES

프로세스의 /proc/[pid]/fdinfo 디렉터리 안에 있는 epoll 파일 디스크립터 항목을 통해 epoll 파일 디스크립터를 통해 감시 중인 파일 디스크립터들의 집합을 볼 수 있다. 자세한 내용은 proc(5) 참고.

kcmp(2)KCMP_EPOLL_TFD 동작을 사용해 어떤 파일 디스크립터가 epoll 인스턴스 내에 있는지 검사할 수 있다.

SEE ALSO

epoll_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2), poll(2), select(2)


2021-03-22