2025. 8. 19. 17:30ㆍCS/Computer Network
epoll과 멀티플렉싱
1. 전통적인 웹소켓 통신
Blocking IO 기반의 멀티스레드 방식이다. 클라이언트가 accept()으로 요청하면 새로운 스레드를 생성하고, 생성된 스레드는 해당 클라이언트와 데이터 송수신을 전담한다. 이 과정에서 accept(), read(), write()와 같은 IO 함수들은 데이터가 준비될 때까지 해당 스레드를 Blocking 시킨다.
따라서 특정 클라이언트의 느린 IO가 존재한다면 그 클라이언트의 해당 워커 스레드는 Block 된다. 즉, 일시정지된다.
2. Non-blocking I/O
Non Blocking IO는 IO 작업을 요청했을 때 그 작업이 끝날 때까지 기다리지 않고 즉시 다음 코드를 실행하는 방식이다. 조금 더 정확히 말하자면 제어권을 반환하는 방식이다.
fcntl() 시스템 콜을 사용하여 특정 소켓의 FD를 Non-blocking 모드로 설정하면, read()나 write() 호출 시 당장 처리할 데이터가 없더라도 대기하지 않고 즉시EAGAIN이나 EWOULDBLOCK 에러를 반환한다. 즉, 제어권을 즉시 반환하는 것이고 프로세스는 I/O를 기다리며 Block 되지 않고 다른 작업을 수행할 수 있게 된다.
Non-Blocking 방식에서는 ****하나의 스레드가 수천 개의 연결을 동시에 처리할 수 있다. 1번 연결에 데이터를 요청하고 바로 돌아와 2번 연결에 데이터를 요청하고, 3번 연결에서 온 데이터를 처리하는 식으로, 잠시도 쉬지 않고 일하는 것이다.
단점
- 프로세스는 I/O가 가능한지 확인하기 위해 무한 루프를 돌며 모든 소켓에 read()를 계속 시도해야 한다. 이를 Busy-waiting이라 하며, CPU 자원을 100% 점유하여 극심한 낭비를 초래하게 된다.
3. I/O 멀티플렉싱 - select, poll
결국 어플리케이션이 계속 Polling 하는 것이 아니라, IO가 준비되면 커널이 알려주는 Event Notification 방식이 필요하다는 것이다.
멀티플렉싱은 여러 개의 신호를 하나의 채널로 합치는 기술이다. 이 개념을 차용한 I/O 멀티플렉싱은 하나의 스레드가 수많은 I/O 파일 디스크립터들을 동시에 감시하고 관리하는 기술이다.
- 이때 파일 디스크립터는 OS가 열려 있는 파일 혹은 I/O 채널을 식별하기 위해 부여하는 고유한 번호이다.
- 유닉스 계열의 OS는 모든것을 파일로 관리한다. 따라서 웹서버가 새로운 클라이언트의 접속을 accept()하면 커널은 이 클라이언트와의 통신을 위한 전용 소켓을 만들고 여기에 파일 디스크립터 번호를 부여하게 된다.
- 전용 소켓을 만든다는 말은 어떤 자원을 할당한다는 말이다.
I/O 멀티플렉싱을 사용하지 않는 Blocking 방식에서는 프로세스가 특정 FD 하나에서 데이터가 올 때 까지 Blocking 되어야 했다.
반면, I/O 멀티플렉싱을 사용한다면 프로세스는 자신이 관리하는 FD 목록 중 어디서든 이벤트가 발생하면 실행 상태가 될 수 있다.
select 와 poll은 I/O 멀티플렉싱을 구현하는 시스템 콜이다.
select()
- 준비: 개발자는 감시할 FD들의 목록을 fd_set 에 저장한다. fd_set은 비트맵과 유사하며, 만약 5번, 8번 FD를 감시하고 싶다면 fd_set의 5번째, 8번째 비트를 1로 설정하면 된다.
- 호출: 준비된 fd_set을 select() 함수에 전달하여 커널을 호출한다. 이때 프로세스는 Block 상태가 된다.
- 커널 작업: 커널은 fd_set에 설정된 모든 FD들을 하나씩 확인하며 I/O 이벤트가 발생하는지 감시한다.
- 반환: 이벤트가 발생하면, 커널은 fd_set의 내용을 직접 수정하여 이벤트가 발생한 FD의 비트만 1로 남기고 나머지는 0으로 바꾼다. 그리고 대기하던 프로세스를 깨운다.
- 확인: 깨어난 프로세스는 fd_set의 모든 비트를 0번부터 다시 검사하여, 어떤 FD의 비트가 1로 남아있는지 확인하고 해당 FD에 대한 I/O 작업을 수행한다.
- 단점
- 감시할 수 있는 FD 개수가 FD_SETSIZE 로 제한된다.
- 호출할 때마다 fd_set을 커널에 복사해야 하고, 커널과 어플리케이션 모두 전체 FD를 매번 스캔해야 하므로 비효율적이이다.
- 호출 후 fd_set이 변경되므로, 다음 호출을 위해 다시 fd_set을 만들어야 한다.
poll()
- poll()은 select()의 FD 개수 제한 문제를 해결한 시스템 콜이다.
- 준비: pollfd 구조체 배열을 만들고, 감시할 FD 번호와 감시할 이벤트의 종류를 지정한다.
- 호출 및 반환: 배열을 poll() 함수에 전달한다. 커널은 이벤트가 발생한 구조체의 revents(return events) 필드에 발생한 이벤트의 종류를 기록하여 반환한다.
- 확인: 깨어난 프로세스는 pollfd 배열 전체를 순회하며 revents 필드가 채워진 구조체를 찾아 작업을 수행한다.
- 단점
- FD 개수 제한은 해결했지만, select()와 마찬가지로 호출 시마다 전체 배열을 커널로 복사하고, 반환 후 전체 배열을 스캔 시간적 오버헤드가 남아있다.
4. Epoll
epoll은 기존 방식의 다음과 같이 해결한 가장 진보된 멀티플렉싱 기술이다.
- 관심 FD 목록의 커널 내 저장: epoll_create()로 epoll 인스턴스를 커널에 생성하면, 커널은 이 인스턴스에 대한 FD 목록을 자체 공간에 유지한다. 어플리케이션은 epoll_ctl()을 통해 이 목록을 추가/삭제할 뿐, select/poll처럼 매번 전체 목록을 커널로 복사할 필요가 없다.
- 이벤트 기반(Event-driven) 동작: epoll_wait() 호출 시, 커널은 select/poll처럼 전체 FD를 스캔하지 않는다. 대신 I/O가 준비된 FD가 발생할 때마다 해당 FD를 커널 내의 Ready List에 추가해둔다. epoll_wait() 은 이 Ready List가 비어있지 않으면 즉시 해당 목록의 내용만 어플리케이션에 전달하고 반환한다.
이러한 구조 덕분에 epoll은 감시하는 전체 FD의 수와 관계없이, 실제로 이벤트가 발생한 FD의 수에만 비례하는 성능을 보장한다.