한 줄 정의

BSD/macOS 커널이 제공하는 이벤트 통지 메커니즘. 소켓뿐 아니라 파일 변경, 프로세스 종료, 시그널, 타이머 등 다양한 커널 이벤트를 하나의 인터페이스로 감시할 수 있다.

쉽게 말하면

epoll이 “네트워크 전용 알림 시스템”이라면, kqueue는 “건물 전체 통합 관제 시스템”입니다. 네트워크 이벤트는 물론이고 파일 변경, 프로세스 종료, 타이머 만료까지 하나의 대기열에서 모두 받아볼 수 있습니다.

왜 이걸 알아야 하는가?

  • macOS/BSD 환경의 서버 개발: macOS에서 개발하고 Linux에 배포하는 경우, 로컬에서는 kqueue가 동작하고 프로덕션에서는 epoll이 동작합니다. Netty, libuv 같은 프레임워크가 이 차이를 추상화하지만, 디버깅이나 성능 분석 시 어떤 메커니즘이 동작하는지 알아야 합니다.
  • epoll과의 비교를 통한 이해 심화: 같은 문제(select/poll의 O(n) 한계)를 Linux와 BSD가 서로 다른 방식으로 해결했습니다. 둘을 비교하면 I/O 다중화의 본질적인 설계 원칙을 더 깊이 이해할 수 있습니다.

왜 이렇게 설계했는가?

  • 해결하는 문제: epoll과 동일하게 select/poll의 O(n) 한계를 해결합니다. 추가로 BSD 진영은 소켓 I/O뿐 아니라 모든 종류의 커널 이벤트를 하나의 통합 인터페이스로 감시하고 싶다는 설계 목표가 있었습니다.
  • epoll보다 먼저 등장: kqueue는 2000년(FreeBSD 4.1)에, epoll은 2002년(Linux 2.5.44)에 등장했습니다. 두 프로젝트는 독립적으로 개발되었으며, kqueue가 범용 이벤트 시스템을 지향한 반면 epoll은 네트워크 I/O에 집중했습니다.

어떻게 동작하는가?

두 가지 시스템 콜

kqueue는 epoll의 세 개(create/ctl/wait)와 달리 두 개만으로 동작합니다.

1. kqueue() — kqueue 인스턴스 생성

int kq = kqueue();

2. kevent() — 이벤트 등록과 대기를 하나의 호출로 처리

struct kevent changes[2];   // 등록할 이벤트
struct kevent events[10];   // 수신할 이벤트
 
// 소켓 읽기 감시 등록
EV_SET(&changes[0], sockfd, EVFILT_READ, EV_ADD, 0, 0, NULL);
// 파일 변경 감시 등록
EV_SET(&changes[1], filefd, EVFILT_VNODE, EV_ADD, NOTE_WRITE, 0, NULL);
 
// 등록 + 대기를 한 번에
int n = kevent(kq, changes, 2, events, 10, &timeout);
for (int i = 0; i < n; i++) {
    handle(events[i]);
}

epoll은 등록(epoll_ctl)과 대기(epoll_wait)가 분리되어 있지만, kqueue는 kevent() 한 번의 호출로 이벤트 등록과 수신을 동시에 처리할 수 있습니다.

struct kevent 구조

struct kevent {
    uintptr_t ident;    // 식별자 (fd, PID, 시그널 번호 등)
    int16_t   filter;   // 이벤트 종류 (EVFILT_READ, EVFILT_VNODE 등)
    uint16_t  flags;    // 동작 플래그 (EV_ADD, EV_DELETE, EV_ONESHOT 등)
    uint32_t  fflags;   // 필터별 세부 플래그
    intptr_t  data;     // 필터별 데이터 (읽기 가능 바이트 수 등)
    void      *udata;   // 사용자 정의 데이터
};

ident + filter 조합이 이벤트를 고유하게 식별합니다. 같은 fd에 대해 READ와 WRITE를 별도로 등록할 수 있습니다.

필터 (Filter) 시스템

kqueue의 가장 큰 특징은 다양한 커널 이벤트를 필터로 구분하여 하나의 인터페이스로 처리하는 것입니다.

필터감시 대상용도
EVFILT_READfd소켓/파이프에 읽을 데이터 도착
EVFILT_WRITEfd소켓/파이프에 쓰기 가능
EVFILT_VNODEfd파일/디렉토리 변경 (수정, 삭제, 이름변경)
EVFILT_PROCPID프로세스 이벤트 (종료, fork, exec)
EVFILT_SIGNAL시그널 번호시그널 수신
EVFILT_TIMER임의 ID타이머 만료

epoll은 기본적으로 fd 기반 I/O 이벤트만 처리하므로, 프로세스 감시는 signalfd, 타이머는 timerfd, 파일 변경은 inotify를 별도로 만들어서 epoll에 등록해야 합니다. kqueue는 이 모든 것이 하나의 필터 시스템에 통합되어 있습니다.

내부 동작

epoll과 마찬가지로 커널에 상태를 유지하는 stateful 방식입니다.

  1. kqueue()로 커널에 이벤트 큐 인스턴스를 생성합니다.
  2. kevent()의 changelist로 감시 대상을 등록하면, 커널이 해당 객체(소켓, 파일, 프로세스)에 콜백을 등록합니다.
  3. 이벤트가 발생하면 커널이 콜백을 통해 kqueue의 ready list에 이벤트를 추가합니다.
  4. kevent()의 eventlist로 ready list에 쌓인 이벤트만 반환합니다.

시각화

epoll vs kqueue API 매핑

epoll                          kqueue
─────────────────────         ─────────────────────
epoll_create()          →     kqueue()
epoll_ctl(ADD/MOD/DEL)  →     kevent(changelist)
epoll_wait()            →     kevent(eventlist)

epoll: 3개 시스템 콜           kqueue: 2개 시스템 콜
      (등록과 대기 분리)              (등록과 대기 통합 가능)

kqueue 동작 흐름

sequenceDiagram
    participant App as 애플리케이션
    participant K as 커널 (kqueue)
    participant Net as 네트워크
    participant FS as 파일시스템

    App->>K: kqueue() → 인스턴스 생성
    App->>K: kevent(changelist=[소켓 READ, 파일 VNODE])
    Note over K: 소켓과 파일 모두 하나의 kqueue에 등록

    Net-->>K: 소켓에 데이터 도착 → ready list에 추가
    FS-->>K: 파일 수정 감지 → ready list에 추가
    K-->>App: kevent() 리턴: [{소켓 READ}, {파일 VNODE}]

정리

기준epoll (Linux)kqueue (BSD/macOS)
등장2002 (Linux 2.5.44)2000 (FreeBSD 4.1)
시스템 콜 수3개 (create, ctl, wait)2개 (kqueue, kevent)
등록/대기분리 (ctl ≠ wait)통합 가능 (kevent 하나로)
감시 대상fd 기반 I/Ofd, 프로세스, 시그널, 타이머, 파일 변경
파일 변경 감시inotify 별도 필요EVFILT_VNODE 내장
타이머timerfd 별도 필요EVFILT_TIMER 내장
ET/LT 모드둘 다 지원기본 ET와 유사, EV_CLEAR로 제어
배치 변경ctl 호출마다 1개씩changelist로 여러 개 한 번에
성능O(1)O(1)

실무에서 만난 사례

  • Nginx: Linux에서는 epoll, FreeBSD/macOS에서는 kqueue를 사용합니다. ngx_kqueue_module.cngx_epoll_module.c가 각각 플랫폼별 구현이며, 상위 레이어에서는 동일한 이벤트 인터페이스로 추상화됩니다.
  • libuv (Node.js): Node.js의 이벤트 루프 라이브러리인 libuv가 Linux에서는 epoll, macOS에서는 kqueue, Windows에서는 IOCP를 내부적으로 선택합니다. 개발자는 플랫폼 차이를 신경 쓰지 않아도 됩니다.
  • macOS에서의 개발: 로컬 개발 시 java.nio.channels.Selector는 macOS에서 kqueue 기반 KQueueSelectorImpl을 사용합니다. Linux 프로덕션에서는 EPollSelectorImpl로 바뀌므로, 플랫폼 간 미묘한 동작 차이가 있을 수 있습니다.

관련 개념

출처

  • FreeBSD man pages: kqueue(2), kevent(2)
  • Jonathan Lemon, “Kqueue: A generic and scalable event notification facility” (USENIX 2001)
  • Apple Developer Documentation: kqueue