대규모 연결 환경에서의 I/O 처리 문제
5000개의 클라이언트 연결을 관리하는 채팅 서버에서 select 사용 시 CPU 사용률이 80%까지 상승하는 경우, 그 원인은:
- 매 호출마다 전체 파일 디스크립터(fd)를 커널에 복사
- 커널이 모든 fd를 순차적으로 점검(O(n) 복잡도)
poll은 fd 제한이 없지만 동일한 비효율 발생. epoll은 활성 연결만 효율적으로 처리하는 것이 핵심 목표입니다.
아키텍처 비교: 폴링 vs 콜백
| 방식 | 작동 예시 | 효율성 |
|---|---|---|
| select | 모든 고객 집 방문 확인 | O(전체 연결 수) |
| poll | 용량 확장된 방문 확인 | O(전체 연결 수) |
| epoll | 수거함에 도착한 패킷만 확인 | O(활성 연결 수) |
epoll은 등록-콜백 메커니즘으로 동작: fd를 한 번 등록하면 데이터 도착 시 커널이 직접 통보합니다.
epoll의 핵심 데이터 구조
struct eventpoll {
struct rb_root tree_root; // 등록된 fd 저장 (레드-블랙 트리)
struct list_head ready_list; // 데이터 도착 fd 연결 리스트
wait_queue_head_t wait_q; // 대기 프로세스 큐
};
동작 흐름:
- 등록(epoll_ctl): fd를 트리에 삽입, 소켓에 콜백 함수 연결
- 데이터 수신: 네트워크 스택이
ep_poll_callback실행 → ready_list에 fd 추가 - 이벤트 처리(epoll_wait): ready_list에서 활성 fd만 추출(O(활성 연결))
성능 비교표
| 항목 | select | poll | epoll |
|---|---|---|---|
| fd 한계 | 1024 | 무제한 | 무제한 |
| 시간 복잡도 | O(n) | O(n) | O(활성 연결) |
| 커널 복사 | 매번 전체 복사 | 매번 전체 복사 | 1회 등록 |
에지 트리거(ET) vs 레벨 트리거(LT)
| LT (기본) | ET |
|---|---|
| 버퍼에 데이터 존재 시 지속 알림 | 데이터 도착 시점에 1회 알림 |
| 부분 읽기 허용 | 반드시 EAGAIN까지 전체 읽기 |
| 안정성 높음 | 고성능 요구 환경 적합 |
epoll 서버 구현 예제
// 핵심 프레임워크
int epoll_fd = epoll_create(0);
struct epoll_event reg_event;
reg_event.events = EPOLLIN; // LT 모드
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, ®_event);
struct epoll_event active_events[MAX_EV];
int cnt = epoll_wait(epoll_fd, active_events, MAX_EV, -1);
// ET 모드 데이터 처리
while((len = read(fd, buf, BUF_SIZE)) {
if (len == -1 && errno == EAGAIN) break;
// 데이터 처리
}
ET 모드 주의사항
- 반드시 넌블로킹 모드 설정:
fcntl(fd, F_SETFL, O_NONBLOCK); - 데이터 완전 읽기: EAGAIN 에러 반환 시까지 읽기 반복
핵심 동작 요약
- 빠른 처리가 가능한 이유: 등록된 fd에 대한 이벤트 기반 알림
- 레드-블랙 트리: 효율적인 fd 관리(O(log n))
- ET 모드: 높은 처리량 보장 but 구현 복잡도 증가