본문 바로가기
컴퓨터과학/운영체제

[I/O 멀티플렉싱] select · poll · epoll

by 시뮬레이션 프로그래머 2025. 10. 10.

참고

 


select

  • FD 한도: 기본 FD_SETSIZE=1024 (빌드 시 키우면 늘릴 수 있지만 보통 1024로 가정)
  • FD 관심목록: 사용자 공간이 비트셋을 매 호출마다 재구성해 커널로 전달
  • 복잡도: 커널이 maxfd까지 선형 스캔 O(n), 사용자도 FD_ISSET로 선형 확인 O(n)

시나리오 (select)

  1. 사용자가 사용할 소켓 FD 집합(비트셋, 최대1024bit)을 준비하고 select를 호출한다. (비트셋이 커널로 복사됨)
  2. 커널은 블록되어 있다가 타임아웃/이벤트가 발생하면, 사용자가 넘긴 비트셋을 선형 스캔(O(N))하여 준비된 FD만 표시한다.
  3. 커널은 표시된 비트셋을 사용자에게 복사해서 돌려준다.
  4. 사용자는 자신이 넘겼던 비트셋을 다시 선형 스캔(O(N))하여 어떤 FD가 준비됐는지 확인한다.
  5. 준비된 FD에 대해 read/write/err 처리를 수행한다.
  • 여기서 사용자는 소켓을 생성하는 소켓 서버를 의미한다.
  • (참고) 기본 FD_SETSIZE=1024 한계가 있어 동시 FD가 많으면 select가 불리하다.
  • c10K 문제를 select 모델로 해결하기 위해서는 FD_SET 개수를 늘려줘야한다.
    c10K 문제: 단일 서버가 동시에 1만 개 이상의 클라이언트 연결을 처리할 때 발생하는 성능 병목 현상

poll

  • FD 한도: ulimit/메모리에 의해 결정 (이론상 2^64bit)
  • FD 관심목록: 배열을 매 호출마다 커널로 전달
  • 복잡도: 커널/사용자 모두 O(n) (전체 배열 스캔)
  • 간단 요약: poll 모델은 select와 비슷하고, FD 한도가 늘어났다. (메모리/하드웨어에 제약받음)

시나리오 (poll)

  1. 사용자는 감시할 소켓들로 pollfd 배열을 만들고(각 항목에 fd와 events 설정) poll() 호출 (배열이 커널로 복사)
  2. 커널은 블록되어 있다가 타임아웃/이벤트 발생 시, 배열을 선형 스캔(O(n)) 하며 각 fd의 현재 상태를 조사한다.
    → 준비된 항목에 revents 세팅
  3. 커널은 수정된 배열을 사용자에게 돌려준다.
  4. 사용자는 배열을 선형 스캔(O(n)) 하며 revents != 0 인 fd들을 찾아 어떤 이벤트가 준비되었는지 확인한다.
  5. 준비된 fd에 대해 read/write/err 처리를 수행한다.
  • (참고) poll은 커널이 관심목록을 보존하지 않기 때문에, 매 호출마다 전체 배열을 다시 넘겨야 합니다.
    즉, FD 수가 커지면 select와 마찬가지로 O(n) 비용이 커집니다.
  • (주의) select는 fd한도가 1024에 반해, poll은 2^64bit이므로 fd 최대한도 범위를 잘못잡으면 fd개수가 1024개도 안됨에도 불구하고 poll이 select보다 느릴 수 있습니다.

epoll

  • FD 한도: ulimit/메모리에 의해 결정 (이론상 2^64bit)
  • FD 관심목록: 커널이 보존(한 번 epoll_ctl(ADD/MOD/DEL)로 등록 후 지속).
  • 자료구조/복잡도
    • FD 관심목록: 커널 내부적으로 레드-블랙 트리(rbtree)로 관리 ⇒ epoll_ctl() = O(log n)
    • 준비 큐(ready list): 준비된 것만 담아 epoll_wait() = O(#ready) (FD 준비된 것만 반환)

시나리오 (epoll)

  1. 사용자가 epoll_create1() 로 epoll 인스턴스(epfd)를 만든다.
  2. 감시할 소켓이 생길 때마다 epoll_ctl(epfd, ADD, fd, EPOLLIN|EPOLLOUT|...)등록한다. (커널 FD관심목록 rbtree에 저장)
  3. 사용자는 epoll_wait(epfd, events, ...) 로 대기한다.
  4. 데이터가 들어오거나 송신 버퍼가 비는 등 상태 변화가 있으면, 커널이 해당 fd를 준비 큐(ready list) 에 올리고 epoll_wait를 깨운다.
  5. epoll_wait는 준비된 항목만 events[]로 반환한다. (스캔 O(#ready))
  6. 사용자는 events[]를 순회하며 fd별로 read/write/err 처리한다.
  7. 다 보냈다면 EPOLLOUT 비활성화 등으로 불필요한 깨움을 줄이기 위해 epoll_ctl(MOD/DEL) 로 마스크/등록 상태를 조정함.
    (트리거)
  • 레벨 트리거(LT): 준비 상태가 계속이면 매번 이벤트가 온다(버퍼에 데이터 남아 있으면 계속 알림).
        리눅스 epoll 트리거 기본 값: 레벨 트리거(LT)
  • 엣지 트리거(ET): 상태가 변하는 순간 1회 알림 → 논블로킹으로 버퍼를 끝까지 드레인해야 다음 알림이 온다
  • 트리거 비유
    - 레벨 트리거 비유: “문이 
    열려 있는 동안은 계속 종을 눌러 준다.”

    - 엣지 트리거 비유: “문이 닫힘→열림으로 바뀌는 순간 딱 한 번 종을 눌러 준다.”
    여기서 “문이 열림” = 읽을 데이터가 있다 / 쓸 공간이 있다는 뜻이고,
    “문이 닫힘” = 버퍼를 끝까지 비워(읽기) / 꽉 채워(쓰기) 상태 변화가 사라지게 만든다는 의미

epoll 요약

  • epoll은 커널이 FD 관심목록을 유지하고, epoll_wait에서 준비된 fd만 전달하므로 대규모 FD에서 유리.
  • 등록/수정은 O(log n), 이벤트 회수는 O(#ready) → c10k/c10m 문제에 적합하다.

(참고) Redis는  epoll 모델로 멀티플렉싱 I/O를 사용한다. 그럼 레디스는 어떤 트리거를 사용할까? 레디스는 싱글 스레드라 버퍼 동시성에 대한 오버헤드가 없으므로 LT를 쓰고 계속해서 버퍼를 사용하는게 유리해보인다.


결론

한 줄 비교

  • select/poll: 매 호출 전체 FD를 스캔(O(n)), select는 기본 1024 한계.
  • epoll: 커널이 목록을 보존하고, epoll_wait는 준비된 것만 반환(O(#ready)), epoll_ctl은 O(log n)

 

select/poll/epoll 공통점

  • 모두 readiness 기반: “지금 read/write 시도해도 안 막힘”을 알려줌(= 완료 통지가 아님).
  • 멀티플렉싱: 한 스레드가 한 번의 대기 호출로 여러 FD를 동시에 감시.
  • 논블로킹 I/O와 궁합: 이벤트 받으면 앱이 직접 read/write 호출(부분 읽기/쓰기, EAGAIN 대비).
  • 타임아웃 지원: 지정 시간 동안 이벤트 없으면 깨어남(주기 작업/타임아웃 처리용).
  • FD 전반 지원: 소켓/파이프/일부 파일 등 파일 디스크립터 대상.
  • 이벤트 유형 공통: 읽기(READ), 쓰기(WRITE), 에러/HUP 등 상태 변화를 통지.

 

I/O 멀티플렉싱이 아닌 다른 방식은?

  • 블로킹 + 단일 연결
    - 한 프로세스/프로그램이 accept→read/write를 블로킹으로 수행

    - 동시에 여러 클라이언트 처리 불가 → 직렬 처리
    - C10K에서 한계
  • 블로킹 + 멀티 프로세스
    - 연결마다 자식 프로세스를 하나 붙임

    - 격리 강함(안전) vs 컨텍스트 스위치/메모리 오버헤드↑
    - C10K에서 한계
  • 블로킹 + 멀티 스레드
    - 연결마다 스레드 하나. 블로킹 I/O 그대로 사용 가능.

    - 구현 쉬움 vs 스레드 수만큼 스택/스케줄링 비용↑, 락·동기화 부담
    - C10K에서 한계
  • 논블로킹 + 바쁜 대기(폴링)
    - non-blocking으로 바꿔 루프에서 주기적으로 fd를 훑음.

    - CPU 낭비 심함 → 실전 부적합
    C10K에서 한계
    • O(n) 스캔 지옥: 1만 FD를 “주기적으로 전부 훑는” 순간, 지연을 낮게 유지하려면 스캔 주기를 짧게 해야 하고
      → 초당 수백만~수천만 번의 FD 체크 = CPU 100% 소모
    • 지연/전력/발열: 바쁜 대기는 항상 CPU를 태움. 다른 작업도 못 돌리고, 전력/발열 폭증
      (참고) OS에서는 일부 기능을 일부러 바쁜 대기(스핀락)을 하는 경우가 있음.
      →  (컨택스트 스위칭 시간이 바쁜대기보다 긴 경우)
    • 스케일 불가: 연결이 늘수록 비용이 선형으로 증가. C10K(1만), C100K(10만)로 갈수록 선형 비용이 감당 불가