참고
- 레디스가 빠른 이유: https://velog.io/@redjen/%EB%A0%88%EB%94%94%EC%8A%A4%EB%8A%94-%EC%99%9C-%EB%B9%A0%EB%A5%BC%EA%B9%8C
- select 와 epoll: https://ozt88.tistory.com/21
- https://velog.io/@im2sh/%EC%86%8C%EC%BC%93-17-select%EB%B3%B4%EB%8B%A4-%EB%82%98%EC%9D%80-poll
select
- FD 한도: 기본 FD_SETSIZE=1024 (빌드 시 키우면 늘릴 수 있지만 보통 1024로 가정)
- FD 관심목록: 사용자 공간이 비트셋을 매 호출마다 재구성해 커널로 전달
- 복잡도: 커널이 maxfd까지 선형 스캔 O(n), 사용자도 FD_ISSET로 선형 확인 O(n)
시나리오 (select)
- 사용자가 사용할 소켓 FD 집합(비트셋, 최대1024bit)을 준비하고 select를 호출한다. (비트셋이 커널로 복사됨)
- 커널은 블록되어 있다가 타임아웃/이벤트가 발생하면, 사용자가 넘긴 비트셋을 선형 스캔(O(N))하여 준비된 FD만 표시한다.
- 커널은 표시된 비트셋을 사용자에게 복사해서 돌려준다.
- 사용자는 자신이 넘겼던 비트셋을 다시 선형 스캔(O(N))하여 어떤 FD가 준비됐는지 확인한다.
- 준비된 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)
- 사용자는 감시할 소켓들로 pollfd 배열을 만들고(각 항목에 fd와 events 설정) poll() 호출 (배열이 커널로 복사)
- 커널은 블록되어 있다가 타임아웃/이벤트 발생 시, 배열을 선형 스캔(O(n)) 하며 각 fd의 현재 상태를 조사한다.
→ 준비된 항목에 revents 세팅 - 커널은 수정된 배열을 사용자에게 돌려준다.
- 사용자는 배열을 선형 스캔(O(n)) 하며 revents != 0 인 fd들을 찾아 어떤 이벤트가 준비되었는지 확인한다.
- 준비된 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)
- 사용자가 epoll_create1() 로 epoll 인스턴스(epfd)를 만든다.
- 감시할 소켓이 생길 때마다 epoll_ctl(epfd, ADD, fd, EPOLLIN|EPOLLOUT|...) 로 등록한다. (커널 FD관심목록 rbtree에 저장)
- 사용자는 epoll_wait(epfd, events, ...) 로 대기한다.
- 데이터가 들어오거나 송신 버퍼가 비는 등 상태 변화가 있으면, 커널이 해당 fd를 준비 큐(ready list) 에 올리고 epoll_wait를 깨운다.
- epoll_wait는 준비된 항목만 events[]로 반환한다. (스캔 O(#ready))
- 사용자는 events[]를 순회하며 fd별로 read/write/err 처리한다.
- 다 보냈다면 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만)로 갈수록 선형 비용이 감당 불가
- O(n) 스캔 지옥: 1만 FD를 “주기적으로 전부 훑는” 순간, 지연을 낮게 유지하려면 스캔 주기를 짧게 해야 하고
'컴퓨터과학 > 운영체제' 카테고리의 다른 글
| [Copy-on-Write] Redis BGSAVE OOM 원인 (5) | 2025.06.15 |
|---|---|
| [Zero-Copy] 파일 전송 최적화와 Kafka 코드 분석 (6) | 2025.05.31 |
| UNIX Time-Sharing System(1974) 논문 정리 (4) | 2025.05.12 |