I/O Multiplexing

김민서·2025년 11월 14일

C/C++

목록 보기
2/6

Blocking I/O

I/O 작업이 완료될 때까지 프로그램이 멈춰있는 방식이다.

char buffer[1024];
int n = read(socket_fd, buffer, 1024);  // 여기서 멈춤
printf("데이터 받음: %s\n", buffer);     // 데이터 올 때까지 실행 안 됨

read 함수가 호출되면 데이터가 도착할 때까지 프로그램이 대기한다. 그렇다는 건, 데이터 안 오면 하염없이 계속 기다린다는 것이다. Single Thread 환경에서는 한 Socket이 Blocking 걸리면 다른 Socket은 처리 못 한다.


Thread per Connection의 문제점

클라이언트마다 Thread를 할당하는 방식은 직관적이고 구현이 간단하지만, 실제 운영 환경에서는 다음과 같은 심각한 문제점들을 가지고 있다.

void handleClient(int client_fd) {
    while (true) {
        char buffer[1024];
        int n = read(client_fd, buffer, 1024);  // Blocking
        if (n > 0) {
            process(buffer);
            write(client_fd, response, size);
        }
    }
}

while (true) {
    int client_fd = accept(server_fd, ...);
    std::thread(handleClient, client_fd).detach();
}

문제점

  • Thread 자원 낭비
    • 각 Thread는 대부분의 시간을 I/O 대기 상태(Blocking)로 보낸다.
    • 실제 데이터 처리 시간은 전체 생명주기의 1% 미만인 경우가 많다.
  • Context Switching 오버헤드
    • Thread 개수가 증가할수록 Context Switching 빈도가 기하급수적으로 증가한다.
    • Context Switching 시 발생하는 비용:
      • 레지스터 상태 저장/복원
      • 캐시 무효화 (Cache Miss 증가)
      • TLB(Translation Lookaside Buffer) Flush
    • CPU 시간의 상당 부분이 실제 작업이 아닌 Switching에 소모된다.
  • 메모리 고갈
    • 각 Thread는 독립적인 스택 메모리를 필요로 한다.
    • 대규모 서비스에서는 메모리 부족으로 시스템 불안정
  • Thread 생성/삭제 비용
    • Thread 생성 시 OS 레벨의 오버헤드:
      • 커널 리소스 할당
      • 스택 메모리 할당 및 초기화
      • TLS(Thread Local Storage) 설정
      • 스케줄러에 등록
    • 연결이 잦은 환경에서 누적 비용이 상당함
  • 확장성 한계
    • C10K Problem: 10,000개 이상의 동시 연결 처리 어려움

I/O Multiplexing

I/O Multiplexing은 한 프로세스가 동시에 여러 파일 디스크립터를 관리하는 기법이다. 프로그램은 파일 디스크립터를 모니터링하여 어떤 종류의 I/O 이벤트(읽기, 쓰기, 예외 등)가 발생했는지 확인하고, 각각의 파일 디스크립터가 Ready 상태가 되었는지 판단한다. I/O Multiplexing을 구현하기 위한 방법으로는 select, poll, epoll 등이 있다.


I/O Multiplexing 동작 방식

  1. 하나의 Thread가 다수의 File Descriptor를 등록한다.
  2. OS Kernel이 등록된 File Descriptor들의 상태를 감시한다.
  3. I/O 작업이 가능한 상태가 되면 Application에 통지한다.
  4. Application은 준비된 File Descriptor에 대해서만 Non-blocking 방식으로 I/O를 수행한다.

I/O Multiplexing 구현 방법

1. select

  • Single 스레드에서 여러 파일을 처리할 때 사용된다.
  • 최대 1024개의 file descriptor를 저장하는 배열을 사용하며, sequential search 방식으로 대상 file descriptor를 탐색하기 때문에, file descriptor 개수가 많아질수록 성능이 저하된다.
  • 오래된 시스템에서도 호환되지만, 효율성이 낮아 현대적 요구에는 부적합하다.

2. poll

  • 관리 가능한 최대 FD 수가 1024로 제한적이었던 select와 달리 무한 개의 file descriptor를 검사할 수 있다.
  • sequential search 방식을 사용하는 건 여전하므로 select와 마찬가지로 file descriptor 개수가 많아질수록 성능이 떨어진다

3. epoll

  • 리눅스에서만 지원
  • select의 단점을 극복하기 위해 Kernel level의 멀티플렉싱을 지원한다.
  • File Descriptor의 상태를 kernel에서 관리하여 상태가 바뀐 것을 직접 통지한다.
    • 변화가 감지된 file descriptor의 개수가 아닌 목록 자체를 반환받기 때문에 대상 파일을 select, poll처럼 loop를 돌며 찾을 필요가 없어 효율적이다.
  • epoll_wait함수를 호출하면 관찰 대상의 정보를 매번 전달할 필요가 없다.
  • 두 가지 방식
    • Level-Triggered: 입력 Buffer에 데이터가 남아있는 동안 계속 이벤트를 발생시킨다. 데이터가 존재만 한다면 계속 알려준다.
    • Edge-Triggered: 입력 Buffer에 데이터가 들어오는 순간에만 이벤트를 발생시킨다.

4. IOCP (I/O Completion Port)

  • Windows 환경에서의 Non-Block Socket을 대량으로, 효율적으로 처리해 주는 API이다.
  • 특징
    • 고성능 서버 구축에 적합하다.
    • 다수의 작업자 스레드(worker thread)를 효율적으로 관리해 준다.
    • 입출력 작업 완료 시 알림을 통해 처리한다.

5. kqueue

  • FreeBSD와 macOS 등 UNIX 계열 운영체제에서 사용하는 이벤트 통지 메커니즘이다.
  • 특징
    • 소켓, 파일, 타이머 등 다양한 이벤트 모니터링이 가능하다.
    • 비동기 I/O 이벤트를 효율적으로 관리한다.

epoll vs IOCP vs kqueue

항목epollIOCPkqueue
지원 OSLinuxWindowsFreeBSD, MacOS 등
용도대규모 네트워크 I/O 처리비동기 I/O 처리이벤트 기반 I/O 처리
작동 방식이벤트 기반 비동기 I/O 처리완료된 작업에 대한 알림 큐 사용이벤트 등록 후 알림 수신
이벤트 관리epoll_wait()GetQueuedCompletionStatus()kevent()
핸들링 단위File DescriptorI/O HandleFile Descriptor, socket 등
성능대규모 클라이언트 연결에 최적화CPU 코어 기반 최적화다양한 이벤트 관리 가능, 효율적
특징Edge-triggered, Level-triggered 지원 간단한 API 제공 스케일러블입출력 작업 완료 후 작업 큐에서 알림 고성능 서버에 적합 스레드 풀 기반파일 I/O, 타이머 등 다양한 이벤트 지원 멀티플렉싱 가능 유연한 구조
장점높은 확장성 많은 연결 처리 가능스레드 풀 관리로 CPU 활용 최적화 대규모 작업 처리 적합여러 이벤트 소스 통합 관리 유연하고 강력한 기능
profile
시스템 개발 공부 중

0개의 댓글