Multi Plexing 에 대하여

2025. 7. 7. 21:09·Server

[ 멀티플렉싱(multiplexing)은 왜 등장했을까? ]


서버를 개발하다 보면 여러 클라이언트가 동시에 접속해 서비스를 제공받도록 하는 '다중 접속 서버'를 만들 필요가 생깁니다. 이러한 다중 접속 서버를 구현하는 방법은 크게 세 가지로 나눌 수 있습니다.

  • 멀티프로세싱(multiprocessing): 각각의 클라이언트 요청을 처리할 때마다 '새로운 프로세스를 생성'해서 대응하는 방식입니다. 프로세스 생성과 유지 비용이 커서 자원을 많이 소모합니다.
  • 멀티스레딩(multithreading): 한 프로세스 내에서 여러 개의 '스레드를 생성'해서 동시에 요청을 처리하는 방식입니다. 프로세스보다는 가볍지만, 스레드 간의 자원 경쟁과 컨텍스트 스위칭으로 인해 성능에 한계가 있습니다.
  • 멀티플렉싱(multiplexing): 하나의 프로세스 또는 스레드에서 여러 개의 입출력(I/O)을 '묶어서 관리'하는 방식입니다. 이벤트 기반으로 동작하며, 적은 리소스로 다수의 연결을 효율적으로 처리할 수 있습니다.

멀티플렉싱 방식은 기존의 멀티프로세싱과 멀티스레딩의 문제점인 높은 리소스 소모와 비효율성을 해결하고, 효율적으로 다중 클라이언트 요청을 처리할 수 있는 최적의 방법으로 자리 잡았습니다.

 

 

[ 멀티플렉싱 동작방식 ]


https://jongmin92.github.io/2019/03/03/Java/java-nio/#google_vignette

 

멀티플렉싱(multiplexing)은 하나의 스레드가 여러 클라이언트의 연결을 동시에 감시하고, 이벤트가 발생한 연결에만 반응하여 처리하는 방식입니다.

멀티플렉싱이라는 단어는 Multi(많은)와 plex(엮다, 결합하다)의 합성어로, 여러 개를 하나로 엮어서 효율적으로 관리하는 것을 의미합니다.

 

즉, 여러 개의 입출력(I/O, 예: 소켓, 파일 등)을 하나의 스레드가 모아서 감시하고, 이벤트가 발생한 연결만 처리합니다. 이 방식은 하나의 스레드가 다수의 I/O 객체를 관리할 수 있게 합니다.

커널(kernel)에서는 하나의 스레드가 여러 개의 소켓 또는 파일을 동시에 관리할 수 있도록 select, poll, epoll, kqueue, io_uring 등의 시스템 콜(system call)을 제공하고 있습니다.

기존에는 하나의 프로세스나 스레드가 하나의 클라이언트 입출력만 처리할 수 있었습니다. 그 이유는 입출력 함수가 블록되면 입출력 데이터가 준비될 때까지 무한정 기다려야 했기 때문입니다. 그래서 여러 클라이언트의 입출력을 동시에 처리할 수 없었습니다.

하지만 멀티플렉싱(I/O multiplexing) 기법을 사용하면, 입출력 함수 자체는 여전히 블록되지만, 함수를 호출하기 전에 어떤 파일이나 소켓에서 입출력이 준비되었는지 미리 확인할 수 있어서 효율적으로 입출력을 관리할 수 있습니다.

 

이제 하나의 스레드가 여러 요청의 처리를 가능해주는 시스템콜들에대해 알아보겠습니다.

 

[ Select ] 


Select 탄생 배경

초창기 UNIX 네트워크 프로그래밍은 blocking I/O 방식이었습니다. 서버가 read()를 호출하면 데이터가 도착할 때까지 스레드가 CPU를 점유하거나 블로킹 상태가 되었습니다.

연결이 수십, 수백 개 이상 늘어나면 각 연결마다 스레드를 만들거나 계속 블로킹 상태로 기다려야 했기 때문에 메모리 소모가 크고, 컨텍스트 스위칭 비용이 급격히 증가했습니다.

즉, select 시스템콜 이전에는 소켓 하나당 하나의 프로세스(또는 스레드)가 블로킹으로 I/O를 담당했으며, 대규모 동시 처리를 효율적으로 수행할 수 없었습니다.

select는 이러한 많은 블로킹 소켓을 효율적으로 감시하기 위해 1983년 BSD 4.2에서 처음 등장했습니다. 이후 POSIX 표준(유닉스 기반 기기 표준)에도 포함되었습니다.

 

Select 동작

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

 

select는 상태를 가지지 않는 완전한 함수로, 호출할 때마다 감시할 소켓 목록(파일 디스크립터 목록)과 대기 시간을 넘겨줘야 합니다. 리눅스에서는 소켓 또한 파일이고 파일 디스크립터(특정 파일에 할당해주는 정수값)로 관리되기 때문에, 소켓의 상태를 확인하는 것도 파일 디스크립터를 통해 이루어집니다. 커널은 파라미터를 통해 전달받은 파일 디스크립터 정보를 기반으로 한 번 감시를 수행하고 결과를 반환합니다.

 

사용자는 감시할 파일 디스크립터 목록을 fd_set이라는 구조체로 넘깁니다. 이 구조체는 최대 1024개의 비트로 구성되어 있으며, 비트 하나가 파일 디스크립터 하나를 의미합니다. 비트가 1이면 감시 대상이고, 0이면 감시하지 않습니다. 예를 들어 비트 배열이 0101101이라면, 이는 0번, 2번, 3번, 5번 파일 디스크립터를 감시하겠다는 의미입니다. (오른쪽부터 0번)

readfds 는 읽기 가능 여부 이벤트를 감시할 fd 집합이며 writefds 는 쓰기 가능 여부 이벤트를 감시할 fd 집합입니다.

 

 

timeout은 대기할 시간을 설정하는 구조체이며, NULL을 넘기면 무한히 대기합니다.

 

select가 호출되면 커널은 주어진 fd_set을 기반으로 이벤트를 검사합니다. 이벤트가 발생한 파일 디스크립터는 fd_set에서 1로 유지되고, 이벤트가 없는 디스크립터는 0으로 초기화됩니다. 예를 들어 호출 전 0101101이었지만, 이벤트가 발생한 fd가 3번과 5번뿐이라면, 호출 후 fd_set은 0101000으로 바뀝니다.

함수의 반환값은 이벤트가 발생한 파일 디스크립터의 수입니다. 반환값이 0이면 타임아웃이 발생한 것이고, -1이면 오류가 발생한 것입니다.

 

select를 사용하여 한 스레드에서 여러 클라이어트의 요청을 처리하려면 어떻게 구현해야 할까요?

  • select 호출 전에 fd_set 구조체를 만든다.
  • select 호출 후 반환값이 0 이상이면 읽기/쓰기 이벤트가 발생한 소켓이 있다는 뜻이다..
  • fd_set을 순회하면서 이벤트가 발생한 소켓을 처리한다.
  • 다시 select를 호출하여 위 과정을 무한 반복한다.

 

Select 장점

  • 단일 프로세스(스레드)에서 여러 파일의 입출력 처리가 가능해 동시에 여러 연결을 처리할 수 있습니다.

Select 단점

  • 매 select 호출마다 fd_set을 새로 만들어 전달해야 합니다.
  • select함수 반환값으로는 반환된 이벤트 수만 알 수 있어 어떤 fd에서 이벤트가 발생했는지 확인하려면 매번 파라미터로 넘겼던 전체 fd_set을 검사해야 합니다.
    • 이는 O(n)의 시간복잡도를 가집니다.
  • 감시 가능한 파일 디스크립터가 최대 1024개로 제한됩니다.

 

[ Poll ] 


Poll 탄생 배경

  • 1988~1989년, AT&T UNIX System V Release 3 (SVR3)에서 등장
  • fd_set 비트마스크 한계(특히 1024 제한) 때문에 개발
  • select의 fd_set 비트 배열 구조 한계(1024 제한, 불편한 API, 메모리 낭비) 때문에 fd를 배열로 표현해서 훨씬 유연하고 더 많은 fd를 감시할 수 있도록 만든 이 poll의 존재 이유이다.

Poll 동작

poll은 select처럼 '순수 함수'처럼 동작합니다

poll 시스템 콜은 select와 유사하지만 고정 크기 비트마스크 대신 가변 길이 배열을 사용하고, 준비된 이벤트를 반환하는 방식이 다를 뿐 기본 원리는 같습니다.

poll 역시 매번 모든 fd를 확인하므로 선형 시간이 걸리고, 많은 수의 fd 중 극소수만 이벤트가 발생하는 상황에서는 대다수의 확인 작업이 불필요한 낭비가 됩니다

매번 모든 fd를 순회해야 하는 구조라서 대량 연결에서 결국 한계가 있었습니다.

 

 

[ epoll ] 


epoll 탄생 배경

  • 하나의 서버가 수천~수만 개의 연결을 동시에 처리해야 하는 상황이 발생했습니다.
  • select와 poll로는 수천, 수만 개의 파일 디스크립터를 실시간으로 감시하기에 CPU 자원이 부족해졌습니다.
  • 2002년 리눅스 커널에 처음 도입되었습니다.
  • select와 poll의 비효율성(O(n) 선형 검색, 복사 오버헤드)을 해결해 대규모 커넥션 처리가 가능하도록 만들었습니다.

epoll 동작

epoll은 단순한 함수 호출 방식이 아니라, epoll 파일 디스크립터 객체를 생성해 상태를 유지하면서 동작합니다.

select는 매번 모든 연결에 대해 새 메시지가 도착했는지 일일이 확인하지만,

epoll은 새 메시지가 도착한 연결만 알려주는 구조로 불필요한 확인 작업을 없앴습니다. 한 번 등록해 두면 이후 이벤트 발생 시에만 통지받는 구조로 효율적입니다.

 

epoll 객체는 관심 리스트(interest list)와 준비 리스트(ready list)를 가집니다.

  • 관심 리스트 (interest list) : 감시 중인 파일 디스크립터 목록
  • 준비 리스트 (ready list) : 이벤트가 발생한 파일 디스크립터 목록

epoll은 준비 리스트만 처리하면 되며, 관심 리스트를 선형 검색하지 않고 커널 내부 콜백 기반으로 준비 리스트를 채워 감시 fd 개수(n)에 관계없이 이벤트 통지 처리 시간이 O(1) 입니다. fd 추가/삭제 시 자료구조 관리에 O(log n) 정도의 비용이 듭니다

 

epoll 관련 시스템 콜

 

1. epoll_create1

int epfd = epoll_create1(0);
  • epoll 객체 생성, epoll 전용 fd를 반환

 

2. epoll_ctl 

epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
  • 감시할 fd 등록/제거, 관심 이벤트(EPOLLIN, EPOLLOUT 등) 설정
  • struct epoll_event에 비트마스크와 사용자 데이터 포인터 설정 가능
  • EPOLLET 플래그로 Edge-Triggered 모드 사용 가능 (상태 변화 시 1회만 알림)
  • 기본은 Level-Triggered 모드 (데이터를 모두 처리할 때까지 반복 알림)

 

3. epoll_wait

int n = epoll_wait(epfd, evlist, MAXEV, timeout);
  • 이벤트 대기 및 처리, 준비된 이벤트를 배열에 담아 반환
  • 준비된 이벤트가 발생하면 최대 MAXEV 개까지 evlist 배열에 채워서 반환합니다
  • 반환값 n은 발생한 이벤트 개수이며, 0이면 타임아웃입니다.
  • epoll_wait 진입하자마자 ready list 확인하고 데이터가 있으면 바로 evlist에 복사하고 리턴합니다.
  • ready list에 없으면 스레드는 wait queue에 등록하고 S(Task interruptible sleep) 상태로 잠듭니다. 이벤트가 발생하면 wake-up 호출이 일어나서 S 상태인 epoll_wait 스레드가 R(runnable)로 바뀌고 CPU 스케줄러가 다시 CPU를 할당해줍니다. epoll_wait은 깨어난 뒤 레디리스트를 다시 확인하고 작업을 계속합니다.
  • 사용자 코드에서는 반환된 배열을 확인하여 어떤 fd에 어떤 이벤트가 발생했는지 확인하고 처리할 수 있습니다. 즉 epoll 로 어떤 fd 가 이벤트가 있는지 확인하고, 이벤트 처리(read 등)은 직접 사용자가 구현합니다.

 

4. 정리

close(epfd);
  • epoll 사용 후 fd를 닫아 자원을 해제하며 감시 중이던 fd도 자동 해제됩니다.

 

epoll 장점

  • fd 수(n)가 커져도 이벤트 대기 및 처리의 비용이 크게 늘지 않으므로, 고밀도 동시 접속을 다루는 서버에 적합합니다.
  • epoll 자체도 파일 디스크립터로 동작하므로, epoll 간에 계층적으로 감시하거나 (epoll을 또 다른 epoll에 등록) poll/select와 혼용하는 고급 사용도 가능합니다.
  • Edge-Triggered 모드를 활용하면 이벤트를 비동기적으로 덜 빈번하게 받을 수 있습니다.

epoll 단점

  • epoll은 Linux 전용으로, 타 OS에서는 사용할 수 없습니다.

 

(+) IOCP, kqueue

epoll 은 리눅스에서만 동작합니다.

비슷한 역할을 하는 시스템 콜로 Windows에는 IOCP, FreeBSD에서는 Kqueue(mac)가 있습니다.

이 OS별 차이를 감추기 위해 추상화 레이어(예: libevent, libuv)가 존재합니다.

  • C 코드 한 벌만 작성하고 컴파일할 때 운영체제에 맞게 분기 처리 해서 맞춰주는 구조
  • 전처리기 지시문활용하여 컴파일 시점에 운영체제에 맞는 코드만 살아남는다.
  • 예를들어 libevent, libuv를 리눅스에서 컴파일하면 epoll 관련 C 코드만 들어간 바이너리가 나오고, 윈도우에서 컴파일하면 IOCP 관련 코드가 들어간다.

 

io uring


io_uring 탄생배경

epoll은 네트워크 서버에서는 충분히 성능을 발휘했지만, 고성능 파일 I/O 처리에는 한계가 있었습니다.

소켓은 버퍼가 찼을 때 이벤트로 깨워주는 방식이지만, 파일은 항상 읽을 데이터가 있어 이벤트가 상시 발생하여 busy loop처럼 동작해 비효율이 발생합니다. 파일은 epoll로 감시를 요청하면 항상 이벤트가 발생한 상태로 즉시 반환되어, 이벤트 기반이 아닌 블로킹과 다를 바 없는 동작이 됩니다.

 

이러한 문제를 해결하기 위해 Facebook 엔지니어가 리눅스용 새로운 비동기 API를 제안했고, 2019년 리눅스 커널 5.1에 io_uring이 처음 추가되었습니다.

io_uring은 기존 select/poll/epoll의 파일/소켓 비동기 I/O 문제를 해결하고, 시스템콜 호출과 메모리 복사를 최소화해 진정한 고성능 비동기 I/O를 제공하는 최신 리눅스 API입니다.

 

io_uring 컨셉

기존 select/poll/epoll은 “준비 여부(readiness)”를 확인하는 모델이었다면, io_uring은 “완료 통지(completion)” 모델입니다.

즉 select/poll/epoll 를 통해 어떤 소켓/파일에 이벤트가 있는지 얻을 수 있었고 그 후 파일의 이벤트 처리는 따로 해주는 방식이었으면, io_uring 은 특정 작업을 요청하고, 완료되면 통지받는 방식으로 동작합니다

 

io_uring의 핵심은 커널과 사용자 공간이 공유하는 두 개의 링 버퍼(queue)를 사용해 상호작용한다는 점입니다.

  • SQ (Submission Queue): 사용자 공간에서 커널로 I/O 요청을 제출
  • CQ (Completion Queue): 커널이 사용자 공간으로 완료된 I/O 결과를 반환

이 두 개의 링 버퍼는 커널과 사용자 공간이 공유 메모리 방식으로 직접 접근 가능하여, 시스템콜 호출과 데이터를 복사하는 비용을 크게 줄입니다.

 

io_uring 기본 동작

  • 사용자는 SQE(Submission Queue Entry)를 SQ(Submission Queue)에 추가해 파일 읽기, 쓰기 요청 등의 작업을 등록합니다.
  • io_uring_enter() 시스템콜을 호출해 커널에 처리를 요청합니다.
  • 커널은 요청을 처리한 뒤 CQE(Completion Queue Entry)를 CQ(Completion Queue)에 기록합니다.
  • 사용자는 CQ에서 처리 결과를 읽어 확인합니다.
  • SQE 1개당 CQE 1개가 매칭됩니다.

시스템콜

  • io_uring_setup(): io_uring 인스턴스를 생성해 ring 버퍼를 설정합니다.
  • io_uring_register(): 성능 최적화를 위해 파일 디스크립터나 버퍼를 미리 등록해두는 용도로 사용됩니다.
  • io_uring_enter(): 커맨드를 제출하거나 완료된 이벤트를 수집할 때 사용하는 시스템콜로, "제출 큐에 작업을 넣었으니 처리해달라" 혹은 "완료 이벤트를 최대 M개까지 가져오겠다(없으면 최대 T까지 기다리겠다)"라는 동작을 커널에 전달합니다. 한 번의 호출로 제출과 완료 수집을 동시에 처리할 수 있습니다.

Polling 모드

  • polling 모드를 사용하면 커널 스레드가 하나 떠서 SQ를 계속 감시합니다. 따라서 사용자가 io_uring_enter() 시스템콜을 호출할 필요가 없습니다.
  • 데이터가 준비되면 즉시 처리되며, 시스템콜 비용이 발생하지 않습니다.
  • 단점으로 커널 스레드가 계속 감시를 하며 일하므로 CPU 자원을 많이 사용하게 됩니다.

장점

  • 시스템콜을 최소화 합니다: 묶어서 제출(batch submit), polling 모드 지원
    • 현대 CPU 보안 이슈(Spectre, Meltdown)로 시스템콜 비용이 커진 상황에서 특히 유리합니다.
      • 컨텍스트 스위치 시 보안 검증 추가되어 더 오버헤드가 생겼습니다.
      • 현대에는 시스템콜을 회피하는 것이 고성능 서버에서 필수가 됐습니다.
  • Zero-Copy 가능: 공유 버퍼를 통한 직접 접근으로 메모리 복사까지 줄일 수 있습니다.
    • 일반적으로 소켓에서 데이터 읽으면 커널에서 유저 공간으로 메모리 복사가 한 번 발생한다.
    • io_uring은 조건이 갖춰진다면 shared buffer를 통해 아예 복사(copy)도 생략할 수 있다.
    • read() / write() 할 때 커널 버퍼로 한 번 더 복사하는 전통적 모델을 깨버리는 새로운 방식이다.
  • epoll의 한계를 극복:
    • epoll은 파일 I/O(regular file)의 경우 항상 “ready” 상태로 나타나 상시 이벤트로 취급되기 때문에 비동기 처리가 어렵습니다.
    • 반면 io_uring은 파일 I/O조차 비동기적으로 처리 가능하여 상시 이벤트 문제를 해결하고, 디스크 I/O와 네트워크 I/O를 동일 인터페이스로 통합 관리할 수 있습니다.

단점

  • 새로운 개념, 미성숙, 구현 복잡

 

 

참고

  • https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part1
  • https://www.youtube.com/watch?v=PsF9EeYndd4&t=5327s
  • https://unixism.net/loti/what_is_io_uring.html#what-is-io-uring
  • https://swsmile.info/post/linux-io-polling-epoll/

 

'Server' 카테고리의 다른 글

프로세스 종료와 시그널  (5) 2025.08.02
kotlin coroutine 에 대하여  (3) 2025.07.26
Java의 BLOCKED 상태와 I/O 대기로 인한 일시정지 상태  (0) 2025.07.12
java 다이렉트 버퍼에 대하여  (2) 2025.07.11
'Server' 카테고리의 다른 글
  • 프로세스 종료와 시그널
  • kotlin coroutine 에 대하여
  • Java의 BLOCKED 상태와 I/O 대기로 인한 일시정지 상태
  • java 다이렉트 버퍼에 대하여
WildDevmon
WildDevmon
『앗! 야생의 개발몬(이)가 나타났다!』
  • WildDevmon
    야생의 개발몬
    WildDevmon
  • 전체
    오늘
    어제
    • 분류 전체보기 (35)
      • Server (5)
      • 알고리즘 문제풀이 (27)
      • 회고 (3)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    코틀린 코루틴
    소가 길을 건너간 이유 5
    자바 스레드 상태
    자바 물리 메모리
    백준2262
    백준14465
    백준 30
    우아한 종료
    소수&팰린드롬
    파이썬
    컴공 취준
    백준 램프
    백준15954
    graceful shutdown
    백준 5347
    백준 1114 파이썬
    백준 1027
    백준1238
    백준1188
    자바 프로세스 종료
    네카라쿠배 취업
    백준 1114
    preStop
    백준1747
    통나무 자르기
    백준 1034
    다이렉트 버퍼
    백준 1004
    자바 최대공약수
    백준 파티
    백준 18405
    커널 스레드 상태
    자바 최소공배수
    백준 인형들
    백준6980
    합승 택시 요금
    경력 서류
    백준 경쟁적 전염
    백준 10610
    네이티브 메모리
    direct buffer
    백준 17281
    백준 어린 왕자
    ai개발자
    2020 카카오 인턴
    백준 고층 건물
    1114백준
    선발 명단
    terminationGracePeriodSeconds
    카카오 필요스펙
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
WildDevmon
Multi Plexing 에 대하여
상단으로

티스토리툴바