-
Notifications
You must be signed in to change notification settings - Fork 7
epoll(7)
epoll - I/O 이벤트 알림 기능
#include <sys/epoll.h>
epoll API는 poll(2)과 비슷한 작업을 한다. 즉 여러 파일 디스크립터들을 I/O가 가능한지 감시한다. epoll API는 에지 트리거 인터페이스나 레벨 트리거 인터페이스로 사용할 수 있으며 감시하는 파일 디스크립터 수가 많아져도 잘 동작한다.
epoll API의 핵심에는 epoll 인스턴스라는 커널 내 자료 구조가 있는데, 사용자 공간에서 보면 두 가지 목록을 담은 컨테이너라고 할 수 있다.
-
관심 목록 (epoll 집합이라고도 함): 프로세스에서 감시 대상으로 등록한 파일 디스크립터들의 집합.
-
준비 목록: I/O "준비" 상태인 파일 디스크립터들의 집합. 준비 목록은 관심 목록의 파일 디스크립터들의 부분집합(더 정확히는 참조의 집합)이며, 그 파일 디스크립터들에서 일어난 I/O 활동에 의해서 커널이 동적으로 집합을 채운다.
epoll 인스턴스 생성과 관리를 위한 다음 시스템 호출들을 제공한다.
-
epoll_create(2)에서는 새 epoll 인스턴스를 만들고 그 인스턴스를 가리키는 파일 디스크립터를 반환한다. (더 최근의 epoll_create1(2)은 epoll_create(2)의 기능을 확장한 것이다.)
-
다음으로 epoll_ctl(2)을 통해 관심 있는 파일 디스크립터를 등록한다. 그러면 epoll 인스턴스의 관심 목록에 항목이 추가된다.
-
epoll_wait(2)에서는 I/O 이벤트를 기다린다. 현재 가용 이벤트가 없으면 호출 스레드를 블록 시킨다. (epoll 인스턴스의 준비 목록에서 항목을 가져오는 걸로 생각할 수 있다.)
epoll 이벤트 배포 인터페이스는 에지 트리거(ET)로 동작할 수도 있고 레벨 트리거(LT)로 동작할 수도 있다. 두 메커니즘의 차이를 설명하기 위해 다음 사건들이 일어난다고 하자.
-
파이프의 읽기 쪽을 나타내는 파일 디스크립터(
rfd
)를 epoll 인스턴스에 등록한다. -
파이프 쓰기 쪽에서 2 kB 데이터를 써넣는다.
-
epoll_wait(2) 호출이 이뤄지고, 준비 상태인 파일 디스크립터로
rfd
를 반환한다. -
파이프 읽기 쪽에서
rfd
로부터 1 kB 데이터를 읽는다. -
epoll_wait(2) 호출이 이뤄진다.
파일 디스크립터 rfd
를 epoll 인터페이스에 추가할 때 EPOLLET
(에지 트리거) 플래그를 사용했다면 파일 입력 버퍼에 데이터가 아직 있는데도 5번 단계의 epoll_wait(2) 호출에서 아마 멈출 것이다. 그동안 원격 상대는 이미 보낸 데이터에 대한 응답을 기다리고 있을 수 있다. 이렇게 되는 건 에지 트리거 모드에서는 감시 대상 파일 디스크립터에서 변화가 생길 때에만 이벤트를 내놓기 때문이다. 그래서 5번 단계에서 호출자가 이미 입력 버퍼 내에 있는 어떤 데이터를 기다리게 될 수가 있다. 위 예에서 rfd
에 이벤트가 생성되는 건 2번에서 이뤄진 쓰기 때문이고 그 이벤트를 3번에서 소모한다. 4번에서 이뤄진 읽기 연산이 버퍼 데이터 전체를 소모하지 않으므로 5번 단계에서 이뤄지는 epoll_wait(2) 호출이 무한정 블록 할 수도 있게 된다.
EPOLLET
플래그를 쓰는 응용에서는 블록 하는 읽기나 쓰기 때문에 여러 파일 디스크립터를 처리하는 태스크가 굶게 되는 걸 피하기 위해 논블로킹 파일 디스크립터를 사용하는 게 좋다. epoll을 에지 트리거(EPOLLET
) 인터페이스로 쓰는 권장 방식은 다음과 같다.
-
논블로킹 파일 디스크립터를 쓴다.
-
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)에서 깨어난다. 이는 몇몇 시나리오에서의 "개떼처럼" 깨어나기를 막는 최적화 효과를 준다.
/sys/power/autosleep
를 통해 시스템이 autosleep 모드에 들어가 있는데 이벤트가 발생해서 장치가 깨는 경우에 장치 드라이버는 이벤트가 큐에 들어갈 때까지만 장치를 깨워 두게 된다. 이벤트가 처리된 다음까지 장치를 깨워 두려면 epoll_ctl(2)의 EPOLLWAKEUP
플래그를 쓸 필요가 있다.
struct epoll_event
의 events
필드에 EPOLLWAKEUP
플래그를 설정하면 이벤트가 큐에 들어가는 순간부터 그 이벤트를 반환하는 epoll_wait(2) 호출에 이어 그 다음 epoll_wait(2) 호출까지 시스템이 깨어 있게 된다. 그 시간 너머까지 시스템을 깨워 둬야 하는 경우에는 두 번째 epoll_wait(2) 호출 전에 따로 wake_lock
을 잡으면 된다.
다음 인터페이스를 이용해 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_MOD
로 epoll_ctl(2)을 호출하며 EPOLLIN
과 EPOLLOUT
사이를 계속 오가는 걸 피할 수 있다.
-
epoll 집합에 등록된 파일 디스크립터들을 구별하는 데 쓰는 키는 무엇인가?
파일 디스크립터 번호와 열린 파일 기술 항목(열린 파일의 커널 내 표현, "열린 파일 핸들"이라고도 함)의 조합이 키다.
-
어떤 epoll 인스턴스에 같은 파일 디스크립터를 두 번 등록하면 어떻게 되는가?
아마
EEXIST
가 나올 것이다. 하지만 복제 (dup(2), dup2(2), fcntl(2)F_DUPFD
) 파일 디스크립터를 같은 epoll 인스턴스에 추가하는 건 가능하다. 복제 파일 디스크립터들을 다른events
마스크로 등록하는 기법이 이벤트 필터링에 유용할 수 있다. -
epoll 인스턴스 두 개가 같은 파일 디스크립터에 대기할 수 있는가? 만약 그렇다면 이벤트가 두 epoll 파일 디스크립터 모두로 보고되는가?
가능하며, 이벤트가 둘 모두로 보고될 것이다. 하지만 올바로 처리하려면 조심스런 프로그래밍이 필요할 것이다.
-
epoll 파일 디스크립터 자치를 poll/epoll/select 할 수 있는가?
가능하다. epoll 파일 디스크립터에 대기 이벤트가 있으면 읽기 가능한 것으로 표시된다.
-
epoll 파일 디스크립터를 그 자체의 파일 디스크립터 집합에 넣으려고 하면 어떻게 되는가?
epoll_ctl(2) 호출이 실패(
EINVAL
)한다. 하지만 epoll 파일 디스크립터를 다른 epoll 파일 디스크립터 세트에 추가할 수는 있다. -
epoll 파일 디스크립터를 유닉스 도메인 소켓을 통해 다른 프로세스로 보낼 수 있는가?
가능하지만 의미가 없다. 수신 프로세스에서 관심 목록에 파일 디스크립터들의 사본이 없을 것이기 때문이다.
-
파일 디스크립터가 닫히면 epoll 관심 목록에서 제거되는가?
그렇기는 한데 조심할 점이 있다. 파일 디스크립터는 열린 파일 기술 항목(open(2) 참고)에 대한 참조이다 dup(2), dup2(2), fcntl(2)
F_DUPFD
, fork(2)를 통해 파일 디스크립터가 복제될 때마다 동일한 열린 파일 기술 항목을 가리키는 새 파일 디스크립터가 생성되는 것이다. 그리고 열린 파일 기술 항목은 자신을 가리키는 파일 디스크립터들이 모두 닫힐 때까지 계속 존재한다.관심 목록에서 파일 디스크립터가 제거되는 건 기반 열린 파일 기술 항목을 가리키는 파일 디스크립터들이 모두 닫힌 후이다. 따라서 관심 목록에 속한 파일 디스크립터가 닫힌 후에도 동일한 기반 열린 파일 기술 항목을 가리키는 다른 파일 디스크립터가 열려 있다면 그 닫힌 파일 디스크립터에 대한 이벤트 보고가 있을 수 있다. 이를 막으려면 파일 디스크립터를 복제하기 전에 (epoll_ctl(2)
EPOLL_CTL_DEL
로) 파일 디스크립터를 관심 목록에서 명시적으로 제거해야 한다. 아니면 응용에서 모든 파일 디스크립터들이 닫도록 신경 써야 한다. (하지만 안 보이는 곳에서 라이브러리 함수가 dup(2)이나 fork(2)를 써서 파일 디스크립터를 복제했다면 어려울 수도 있다.) -
epoll_wait(2) 호출 간에 이벤트가 여러 개 발생하면 하나로 합쳐지는가, 아니면 따로 보고되는가?
합쳐진다.
-
파일 디스크립터에 대한 동작이 이미 수집됐지만 아직 보고되지는 않은 이벤트에 영향을 끼치는가?
기존 파일 디스크립터에 취할 수 있는 동작은 두 가지가 있다. 제거 동작은 이 경우에 의미가 없을 것이다. 변경 동작은 가능한 I/O를 재확인하게 한다.
-
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에 대한 다른 이벤트를 발견하면 그 파일 디스크립터가 이미 제거되었음을 알게 될 것이고 혼동이 없을 것이다.
리눅스 커널 2.5.44에서 epoll API가 도입되었다. glibc 버전 2.3.2에서 지원이 추가되었다.
epoll API는 리눅스 전용이다. 몇몇 다른 시스템에서도 비슷한 메커니즘을 제공하는데, 예를 들어 FreeBSD에는 kqueue
가 있고 솔라리스에는 /dev/poll
이 있다.
프로세스의 /proc/[pid]/fdinfo
디렉터리 안에 있는 epoll 파일 디스크립터 항목을 통해 epoll 파일 디스크립터를 통해 감시 중인 파일 디스크립터들의 집합을 볼 수 있다. 자세한 내용은 proc(5) 참고.
kcmp(2)의 KCMP_EPOLL_TFD
동작을 사용해 epoll 인스턴스 내에 어떤 파일 디스크립터가 있는지를 검사할 수 있다.
epoll_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2), poll(2), select(2)
2019-03-06