I/O 멀티플렉싱 실전 가이드 | select, poll, epoll, kqueue, IOCP
이 글의 핵심
C10K 문제를 논할 때 빠지지 않는 주제가 I/O 멀티플렉싱이다. 여기서는 select·poll부터 epoll·kqueue·IOCP까지, “언제 무엇이 유리한지”를 코드 흐름과 함께 짚는다.
목차
- I/O 멀티플렉싱이란?
- Blocking I/O의 문제점
- select - 기본 멀티플렉싱
- poll - select 개선
- epoll - Linux 고성능
- kqueue - BSD/macOS
- IOCP - Windows 고성능
- 성능 비교 및 선택 가이드
사전 지식 (초보자를 위한 기초)
1. 소켓(Socket)이란?
소켓은 네트워크 통신의 양 끝점이다. 연결·송신·수신·종료 같은 동작은 전부 이 fd를 통해 이뤄진다.
간단한 서버 예제:
다음은 c를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 서버 소켓 생성
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 주소 바인딩
bind(server_fd, ...);
// 연결 대기
listen(server_fd, 10);
// 클라이언트 연결 수락
int client_fd = accept(server_fd, ...);
// 데이터 받기
char buffer[1024];
recv(client_fd, buffer, sizeof(buffer), 0);
// 데이터 보내기
send(client_fd, "Hello", 5, 0);
// 연결 종료
close(client_fd);
2. Blocking vs Non-blocking I/O
Blocking I/O (블로킹)
아래 코드는 c를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// recv()는 데이터가 올 때까지 대기 (멈춤!)
char buffer[1024];
int n = recv(client_fd, buffer, sizeof(buffer), 0);
// ↑ 데이터가 오기 전까지 여기서 멈춤
printf("Received: %s\n", buffer);
아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
타임라인:
시간 →
0초: recv() 호출
1초: (대기 중...)
2초: (대기 중...)
3초: 데이터 도착! recv() 반환
3초: printf() 실행
문제: 데이터를 기다리는 동안 아무것도 못함!
Non-blocking I/O (논블로킹)
아래 코드는 c를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 소켓을 Non-blocking으로 설정
fcntl(client_fd, F_SETFL, O_NONBLOCK);
// recv()가 즉시 반환
int n = recv(client_fd, buffer, sizeof(buffer), 0);
if (n == -1 && errno == EAGAIN) {
// 데이터가 없음, 다른 일 할 수 있음
printf("No data yet, doing other work...\n");
}
3. 동시 접속 처리 방법
방법 1: 프로세스/스레드 per 연결
아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 클라이언트마다 스레드 생성
while (1) {
int client_fd = accept(server_fd, ...);
pthread_create(&thread, NULL, handle_client, &client_fd);
}
// 문제: 클라이언트 10,000명 = 스레드 10,000개 (메모리 부족!)
방법 2: I/O 멀티플렉싱 (이 글의 주제!)
아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 하나의 스레드로 여러 소켓 동시 처리
while (1) {
// 여러 소켓 중 준비된 것만 처리
int ready = select(max_fd, &read_fds, ...);
for (int fd = 0; fd < max_fd; fd++) {
if (FD_ISSET(fd, &read_fds)) {
// 이 소켓에 데이터 있음
handle_client(fd);
}
}
}
// 장점: 스레드 1개로 10,000개 연결 처리 가능!
1. I/O 멀티플렉싱이란?
정의
I/O 멀티플렉싱은 하나의 스레드로 여러 소켓의 준비 상태를 한 번에 기다렸다가, 준비된 것만 처리하는 기법이다. 블로킹으로 한 연결에 묶이지 않게 하려는 목적이 크다.
동작 원리
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
┌──────────────────────────────────────┐
│ 애플리케이션 (1개 스레드) │
└──────────────┬───────────────────────┘
│
│ select/epoll/kqueue
│ "어떤 소켓이 준비됐나요?"
│
┌──────────────▼───────────────────────┐
│ 커널 (OS) │
│ Socket 1: 데이터 있음 │
│ Socket 2: 대기 중 │
│ Socket 3: 데이터 있음 │
│ Socket 4: 대기 중 │
└──────────────┬───────────────────────┘
│
│ "Socket 1, 3이 준비됐어요"
│
┌──────────────▼───────────────────────┐
│ 애플리케이션 │
│ Socket 1 처리 → recv() │
│ Socket 3 처리 → recv() │
└──────────────────────────────────────┘
2. Blocking I/O의 문제점
단일 클라이언트 처리
다음은 c를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 간단한 에코 서버
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, ...);
listen(server_fd, 10);
while (1) {
int client_fd = accept(server_fd, ...); // 연결 대기 (Blocking)
char buffer[1024];
int n = recv(client_fd, buffer, sizeof(buffer), 0); // 데이터 대기 (Blocking)
send(client_fd, buffer, n, 0); // 에코
close(client_fd);
}
// 문제: 한 번에 1명만 처리 가능!
// 클라이언트 A가 recv()에서 대기 중이면
// 클라이언트 B는 accept()조차 못함
멀티 스레드 방식
다음은 c를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void* handle_client(void* arg) {
int client_fd = *(int*)arg;
char buffer[1024];
int n = recv(client_fd, buffer, sizeof(buffer), 0);
send(client_fd, buffer, n, 0);
close(client_fd);
return NULL;
}
int main() {
while (1) {
int client_fd = accept(server_fd, ...);
pthread_t thread;
pthread_create(&thread, NULL, handle_client, &client_fd);
pthread_detach(thread);
}
}
// 문제:
// - 클라이언트 10,000명 = 스레드 10,000개
// - 메모리: 10,000 × 8MB = 80GB (스택 크기)
// - 컨텍스트 스위칭 오버헤드
3. select - 기본 멀티플렉싱
select란?
select는 가장 오래된 I/O 멀티플렉싱 방법이다. (1983년 BSD Unix)
아래 코드는 c를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
int select(
int nfds, // 최대 fd + 1
fd_set *readfds, // 읽기 준비 확인할 fd 집합
fd_set *writefds, // 쓰기 준비 확인할 fd 집합
fd_set *exceptfds, // 예외 확인할 fd 집합
struct timeval *timeout // 타임아웃
);
select 예제
다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define MAX_CLIENTS 1024
#define PORT 8080
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 10);
fd_set master_fds; // 모든 소켓 저장
FD_ZERO(&master_fds);
FD_SET(server_fd, &master_fds);
int max_fd = server_fd;
printf("Server listening on port %d\n", PORT);
while (1) {
fd_set read_fds = master_fds; // 복사 (select가 수정함)
// 준비된 소켓 대기
int ready = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (ready < 0) {
perror("select");
break;
}
// 모든 소켓 확인
for (int fd = 0; fd <= max_fd; fd++) {
if (!FD_ISSET(fd, &read_fds)) {
continue; // 준비 안됨
}
if (fd == server_fd) {
// 새 연결
int client_fd = accept(server_fd, NULL, NULL);
FD_SET(client_fd, &master_fds);
if (client_fd > max_fd) {
max_fd = client_fd;
}
printf("New client: %d\n", client_fd);
} else {
// 기존 클라이언트 데이터
char buffer[1024];
int n = recv(fd, buffer, sizeof(buffer), 0);
if (n <= 0) {
// 연결 종료
printf("Client %d disconnected\n", fd);
close(fd);
FD_CLR(fd, &master_fds);
} else {
// 에코
send(fd, buffer, n, 0);
}
}
}
}
close(server_fd);
return 0;
}
select의 장단점
장점: POSIX 계열에서 널리 있고 API가 단순하며 타임아웃을 걸기 쉽다.
단점: FD_SETSIZE 근처에서 한계가 보이고, 매 호출마다 fd 집합을 다시 잡아야 해서 연결이 많을수록 부담이 커진다.
select 성능 문제
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
클라이언트 1,000명 연결:
매 select() 호출마다:
1. fd_set 복사 (1,000개)
2. 커널이 1,000개 모두 확인
3. 애플리케이션이 1,000개 순회
→ O(n) 복잡도, 비효율적!
4. poll - select 개선
poll이란?
poll은 select의 FD_SETSIZE 제한을 없앤 개선 버전이다. (1986년 SVR3)
아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
int poll(
struct pollfd *fds, // fd 배열
nfds_t nfds, // 배열 크기
int timeout // 타임아웃 (ms)
);
struct pollfd {
int fd; // 파일 디스크립터
short events; // 확인할 이벤트 (POLLIN, POLLOUT)
short revents; // 발생한 이벤트
};
poll 예제
다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <poll.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#define MAX_CLIENTS 10000
#define PORT 8080
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 10);
struct pollfd fds[MAX_CLIENTS];
int nfds = 1;
// 서버 소켓 추가
fds[0].fd = server_fd;
fds[0].events = POLLIN;
printf("Server listening on port %d\n", PORT);
while (1) {
// 준비된 소켓 대기
int ready = poll(fds, nfds, -1);
if (ready < 0) {
perror("poll");
break;
}
// 모든 소켓 확인
for (int i = 0; i < nfds; i++) {
if (fds[i].revents == 0) {
continue; // 이벤트 없음
}
if (fds[i].fd == server_fd) {
// 새 연결
int client_fd = accept(server_fd, NULL, NULL);
fds[nfds].fd = client_fd;
fds[nfds].events = POLLIN;
nfds++;
printf("New client: %d (total: %d)\n", client_fd, nfds - 1);
} else {
// 기존 클라이언트 데이터
char buffer[1024];
int n = recv(fds[i].fd, buffer, sizeof(buffer), 0);
if (n <= 0) {
// 연결 종료
printf("Client %d disconnected\n", fds[i].fd);
close(fds[i].fd);
// 배열에서 제거 (마지막 요소와 교체)
fds[i] = fds[nfds - 1];
nfds--;
i--; // 다시 확인
} else {
// 에코
send(fds[i].fd, buffer, n, 0);
}
}
}
}
close(server_fd);
return 0;
}
poll vs select
| 항목 | select | poll |
|---|---|---|
| fd 개수 제한 | 1024 (FD_SETSIZE) | 무제한 |
| fd_set 복사 | 필요 | 불필요 |
| 성능 | O(n) | O(n) |
| 이식성 | 높음 | 높음 |
poll의 개선점:
- ✅ FD_SETSIZE 제한 제거
- ✅ fd_set 복사 불필요
여전한 문제:
- ❌ O(n) 성능 (모든 fd 순회)
5. epoll - Linux 고성능
epoll이란?
epoll은 Linux의 고성능 I/O 멀티플렉싱이다. (Linux 2.5.44, 2002년)
핵심 차이:
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
select/poll:
- 매번 모든 fd를 커널에 전달
- 커널이 모든 fd 확인 (O(n))
- 애플리케이션이 모든 fd 순회 (O(n))
epoll:
- fd를 커널에 한 번만 등록
- 커널이 준비된 fd만 반환 (O(1))
- 애플리케이션이 준비된 fd만 처리 (O(k), k = 준비된 개수)
epoll API
다음은 c를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 1. epoll 인스턴스 생성
int epoll_fd = epoll_create1(0);
// 2. fd 등록
struct epoll_event event;
event.events = EPOLLIN; // 읽기 이벤트
event.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
// 3. 이벤트 대기
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
// 4. 준비된 fd만 처리
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
// 처리
}
epoll 예제
다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#define MAX_EVENTS 1024
#define PORT 8080
// Non-blocking 설정
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
// 서버 소켓 생성
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblocking(server_fd);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
// epoll 생성
int epoll_fd = epoll_create1(0);
// 서버 소켓 등록
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // Edge-Triggered
event.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
struct epoll_event events[MAX_EVENTS];
printf("Server listening on port %d\n", PORT);
while (1) {
// 이벤트 대기
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == server_fd) {
// 새 연결
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 더 이상 연결 없음
}
perror("accept");
break;
}
set_nonblocking(client_fd);
// 클라이언트 소켓 등록
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
printf("New client: %d\n", client_fd);
}
} else {
// 클라이언트 데이터
char buffer[1024];
while (1) {
int n = recv(fd, buffer, sizeof(buffer), 0);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 더 이상 데이터 없음
}
perror("recv");
break;
}
if (n == 0) {
// 연결 종료
printf("Client %d disconnected\n", fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
// 에코
send(fd, buffer, n, 0);
}
}
}
}
close(server_fd);
close(epoll_fd);
return 0;
}
Edge-Triggered vs Level-Triggered
Level-Triggered (LT, 기본값)
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
데이터가 있는 동안 계속 알림
시간 →
0초: 데이터 100바이트 도착
0초: epoll_wait() 반환 → 이벤트 알림
1초: 50바이트만 읽음
1초: epoll_wait() 반환 → 이벤트 알림 (아직 50바이트 남음)
2초: 50바이트 읽음
2초: epoll_wait() 대기 (데이터 없음)
Edge-Triggered (ET, 고성능)
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
데이터 상태가 변할 때만 알림
시간 →
0초: 데이터 100바이트 도착
0초: epoll_wait() 반환 → 이벤트 알림
1초: 50바이트만 읽음
1초: epoll_wait() 대기 (알림 없음! 주의!)
→ 반드시 EAGAIN까지 읽어야 함!
ET 모드 사용 시 주의:
다음은 c를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 ET 사용
int n = recv(fd, buffer, sizeof(buffer), 0);
send(fd, buffer, n, 0);
// 문제: 데이터가 더 있어도 알림 안옴!
// ✅ 올바른 ET 사용
while (1) {
int n = recv(fd, buffer, sizeof(buffer), 0);
if (n < 0) {
if (errno == EAGAIN) break; // 더 이상 없음
// 에러 처리
}
if (n == 0) break; // 연결 종료
send(fd, buffer, n, 0);
}
6. kqueue - BSD/macOS
kqueue란?
kqueue는 FreeBSD·macOS의 고성능 이벤트 알림 메커니즘이다. (FreeBSD 4.1, 2000년)
epoll과의 차이:
- epoll: I/O 이벤트만
- kqueue: I/O + 파일 변경 + 시그널 + 타이머 등 모든 이벤트
kqueue API
다음은 c를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 1. kqueue 생성
int kq = kqueue();
// 2. 이벤트 등록
struct kevent change;
EV_SET(&change, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
// 3. 이벤트 대기
struct kevent events[MAX_EVENTS];
int n = kevent(kq, NULL, 0, events, MAX_EVENTS, NULL);
// 4. 이벤트 처리
for (int i = 0; i < n; i++) {
int fd = events[i].ident;
// 처리
}
kqueue 예제
다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sys/event.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#define MAX_EVENTS 1024
#define PORT 8080
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
// 서버 소켓 생성
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblocking(server_fd);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
// kqueue 생성
int kq = kqueue();
// 서버 소켓 등록
struct kevent change;
EV_SET(&change, server_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
struct kevent events[MAX_EVENTS];
printf("Server listening on port %d\n", PORT);
while (1) {
// 이벤트 대기
int n = kevent(kq, NULL, 0, events, MAX_EVENTS, NULL);
for (int i = 0; i < n; i++) {
int fd = events[i].ident;
if (fd == server_fd) {
// 새 연결
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) break;
set_nonblocking(client_fd);
// 클라이언트 소켓 등록
EV_SET(&change, client_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
printf("New client: %d\n", client_fd);
}
} else if (events[i].filter == EVFILT_READ) {
// 클라이언트 데이터
char buffer[1024];
int n = recv(fd, buffer, sizeof(buffer), 0);
if (n <= 0) {
// 연결 종료
printf("Client %d disconnected\n", fd);
close(fd);
} else {
// 에코
send(fd, buffer, n, 0);
}
}
}
}
close(server_fd);
close(kq);
return 0;
}
kqueue 고급 기능
파일 변경 감지:
아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 파일 변경 감지
int fd = open("config.txt", O_RDONLY);
struct kevent change;
EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR,
NOTE_WRITE | NOTE_DELETE, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
// 파일이 수정되면 이벤트 발생
타이머:
아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 1초 타이머
struct kevent change;
EV_SET(&change, 1, EVFILT_TIMER, EV_ADD, 0, 1000, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
// 1초마다 이벤트 발생
7. IOCP - Windows 고성능
IOCP란?
IOCP (I/O Completion Port)는 Windows의 고성능 비동기 I/O다.
핵심 차이:
아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
select/epoll/kqueue (Readiness Notification):
- "이 소켓에 데이터가 있어요" 알림
- 애플리케이션이 recv() 호출
- 동기적 처리
IOCP (Completion Notification):
- 애플리케이션이 비동기 recv() 요청
- 커널이 데이터를 버퍼에 복사
- "recv() 완료됐어요" 알림
- 완전히 비동기적
IOCP 동작 원리
아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
1. 애플리케이션: "데이터 받아줘" (WSARecv 호출)
↓
2. 커널: "알았어, 나중에 알려줄게" (즉시 반환)
↓
3. 애플리케이션: 다른 일 함
↓
4. 커널: 데이터 도착! 버퍼에 복사
↓
5. 커널: Completion Port에 알림
↓
6. 애플리케이션: GetQueuedCompletionStatus() → "완료됐어요!"
IOCP 예제
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 8080
#define BUFFER_SIZE 1024
// 소켓별 데이터
struct SocketData {
SOCKET socket;
WSAOVERLAPPED overlapped;
WSABUF wsaBuf;
char buffer[BUFFER_SIZE];
DWORD flags;
};
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 서버 소켓 생성
SOCKET server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(server_socket, (sockaddr*)&addr, sizeof(addr));
listen(server_socket, SOMAXCONN);
// IOCP 생성
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 서버 소켓을 IOCP에 연결
CreateIoCompletionPort((HANDLE)server_socket, iocp, (ULONG_PTR)server_socket, 0);
printf("Server listening on port %d\n", PORT);
// Accept 시작
SOCKET client_socket = accept(server_socket, NULL, NULL);
while (1) {
DWORD bytes_transferred;
ULONG_PTR completion_key;
LPOVERLAPPED overlapped;
// 완료된 I/O 대기
BOOL result = GetQueuedCompletionStatus(
iocp,
&bytes_transferred,
&completion_key,
&overlapped,
INFINITE
);
if (!result) {
printf("GetQueuedCompletionStatus failed\n");
continue;
}
SocketData* socket_data = CONTAINING_RECORD(overlapped, SocketData, overlapped);
if (bytes_transferred == 0) {
// 연결 종료
printf("Client disconnected\n");
closesocket(socket_data->socket);
delete socket_data;
continue;
}
// 데이터 처리 (에코)
socket_data->wsaBuf.len = bytes_transferred;
WSASend(
socket_data->socket,
&socket_data->wsaBuf,
1,
NULL,
0,
&socket_data->overlapped,
NULL
);
// 다음 recv 요청
ZeroMemory(&socket_data->overlapped, sizeof(OVERLAPPED));
socket_data->wsaBuf.len = BUFFER_SIZE;
socket_data->flags = 0;
WSARecv(
socket_data->socket,
&socket_data->wsaBuf,
1,
NULL,
&socket_data->flags,
&socket_data->overlapped,
NULL
);
}
closesocket(server_socket);
CloseHandle(iocp);
WSACleanup();
return 0;
}
IOCP 스레드 풀
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Worker 스레드
DWORD WINAPI WorkerThread(LPVOID param) {
HANDLE iocp = (HANDLE)param;
while (1) {
DWORD bytes_transferred;
ULONG_PTR completion_key;
LPOVERLAPPED overlapped;
GetQueuedCompletionStatus(iocp, &bytes_transferred,
&completion_key, &overlapped, INFINITE);
// I/O 처리
// ...
}
return 0;
}
int main() {
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// CPU 코어 수만큼 스레드 생성
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
int num_threads = sysInfo.dwNumberOfProcessors * 2;
for (int i = 0; i < num_threads; i++) {
CreateThread(NULL, 0, WorkerThread, iocp, 0, NULL);
}
// 메인 스레드는 accept만 처리
while (1) {
SOCKET client = accept(server_socket, NULL, NULL);
CreateIoCompletionPort((HANDLE)client, iocp, (ULONG_PTR)client, 0);
// 비동기 recv 시작
// ...
}
}
8. 성능 비교
벤치마크 결과
테스트 환경:
- 동시 연결: 10,000개
- 메시지 크기: 1KB
- 에코 서버
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
처리량(요청/초, 예시):
1. IOCP (Windows): 250,000
2. epoll (Linux): 200,000
3. kqueue (macOS): 180,000
4. poll: 50,000
5. select: 30,000
CPU 사용률(예시):
1. IOCP: 20%
2. epoll: 25%
3. kqueue: 30%
4. poll: 60%
5. select: 80%
복잡도 비교
| 메커니즘 | 등록 | 대기 | 처리 | 전체 |
|---|---|---|---|---|
| select | O(n) | O(n) | O(n) | O(n) |
| poll | O(1) | O(n) | O(n) | O(n) |
| epoll | O(1) | O(1) | O(k) | O(k) |
| kqueue | O(1) | O(1) | O(k) | O(k) |
| IOCP | O(1) | O(1) | O(k) | O(k) |
k = 준비된 이벤트 수
연결 수에 따른 성능
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
연결 수: 100개
- select: 충분히 빠름
- epoll: 약간 더 빠름
- 차이: 거의 없음
연결 수: 1,000개
- select: 느려지기 시작
- epoll: 여전히 빠름
- 차이: 2~3배
연결 수: 10,000개
- select: 매우 느림 (FD_SETSIZE 초과)
- epoll: 여전히 빠름
- 차이: 10배 이상
연결 수: 100,000개
- select: 불가능
- epoll: 가능 (C10K 문제 해결)
- 차이: 무한대
9. 플랫폼별 선택 가이드
선택 기준
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
Linux:
✅ epoll (최고 성능)
- 대규모 연결 (10,000+)
- 프로덕션 서버
Windows:
✅ IOCP (최고 성능)
- 완전 비동기
- 스레드 풀 활용
macOS/FreeBSD:
✅ kqueue (최고 성능)
- epoll과 유사한 성능
- 다양한 이벤트 지원
크로스 플랫폼:
✅ libuv, libevent, Boost.Asio
- 플랫폼별 최적 메커니즘 자동 선택
- Node.js, Nginx가 사용
라이브러리 추천
libuv (Node.js 사용)
다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <uv.h>
void on_read(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf) {
if (nread < 0) {
uv_close((uv_handle_t*)client, NULL);
return;
}
// 에코
uv_write_t* req = malloc(sizeof(uv_write_t));
uv_buf_t wrbuf = uv_buf_init(buf->base, nread);
uv_write(req, client, &wrbuf, 1, NULL);
}
void on_connection(uv_stream_t* server, int status) {
uv_tcp_t* client = malloc(sizeof(uv_tcp_t));
uv_tcp_init(uv_default_loop(), client);
if (uv_accept(server, (uv_stream_t*)client) == 0) {
uv_read_start((uv_stream_t*)client, alloc_buffer, on_read);
}
}
int main() {
uv_tcp_t server;
uv_tcp_init(uv_default_loop(), &server);
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", 8080, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
uv_listen((uv_stream_t*)&server, 128, on_connection);
printf("Server listening on port 8080\n");
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
return 0;
}
Boost.Asio (C++)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <boost/asio.hpp>
#include <iostream>
using boost::asio::ip::tcp;
class Session : public std::enable_shared_from_this<Session> {
public:
Session(tcp::socket socket) : socket_(std::move(socket)) {}
void start() {
do_read();
}
private:
void do_read() {
auto self(shared_from_this());
socket_.async_read_some(
boost::asio::buffer(buffer_),
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
do_write(length);
}
}
);
}
void do_write(std::size_t length) {
auto self(shared_from_this());
boost::asio::async_write(
socket_,
boost::asio::buffer(buffer_, length),
[this, self](boost::system::error_code ec, std::size_t) {
if (!ec) {
do_read();
}
}
);
}
tcp::socket socket_;
char buffer_[1024];
};
class Server {
public:
Server(boost::asio::io_context& io_context, short port)
: acceptor_(io_context, tcp::endpoint(tcp::v4(), port)) {
do_accept();
}
private:
void do_accept() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket))->start();
}
do_accept();
}
);
}
tcp::acceptor acceptor_;
};
int main() {
boost::asio::io_context io_context;
Server server(io_context, 8080);
std::cout << "Server listening on port 8080\n";
io_context.run(); // 내부적으로 epoll/kqueue/IOCP 사용
return 0;
}
10. 실전 패턴
Reactor 패턴 (epoll, kqueue)
아래 코드는 text를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
Reactor 패턴:
- 이벤트 루프
- 이벤트 발생 시 핸들러 호출
- 동기적 처리
┌─────────────────────────────────┐
│ Event Loop │
│ while (1) { │
│ events = epoll_wait() │
│ for event in events: │
│ handler(event) │
│ } │
└─────────────────────────────────┘
구현:
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Reactor {
public:
void register_handler(int fd, std::function<void()> handler) {
handlers_[fd] = handler;
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = fd;
epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &event);
}
void run() {
struct epoll_event events[MAX_EVENTS];
while (1) {
int n = epoll_wait(epoll_fd_, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
handlers_[fd](); // 핸들러 호출
}
}
}
private:
int epoll_fd_ = epoll_create1(0);
std::unordered_map<int, std::function<void()>> handlers_;
};
// 사용
Reactor reactor;
reactor.register_handler(server_fd, [&]() {
int client_fd = accept(server_fd, NULL, NULL);
reactor.register_handler(client_fd, [client_fd]() {
char buffer[1024];
int n = recv(client_fd, buffer, sizeof(buffer), 0);
if (n > 0) {
send(client_fd, buffer, n, 0);
}
});
});
reactor.run();
Proactor 패턴 (IOCP)
아래 코드는 text를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
Proactor 패턴:
- 비동기 I/O 요청
- 완료 시 핸들러 호출
- 완전히 비동기적
┌─────────────────────────────────┐
│ Proactor │
│ 1. async_read(handler) │
│ 2. 커널이 읽기 수행 │
│ 3. 완료 시 handler 호출 │
└─────────────────────────────────┘
11. C10K 문제
C10K 문제란?
C10K (Concurrent 10,000 connections): 동시 접속 10,000명 처리 문제
아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
2000년대 초반:
- 서버 1대로 동시 접속 10,000명 처리 어려움
- select/poll의 O(n) 성능 문제
- 스레드 per 연결 방식의 메모리 문제
해결책:
- epoll (Linux, 2002)
- kqueue (FreeBSD, 2000)
- IOCP (Windows NT 3.5, 1993)
현재:
- 서버 1대로 100만 연결 처리 가능 (C1M)
- Nginx, Node.js 등이 증명
C10K 해결 사례: Nginx
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Nginx 아키텍처:
- Worker 프로세스: CPU 코어 수만큼
- 각 Worker: epoll로 수만 개 연결 처리
- 비동기 I/O + 이벤트 루프
성능:
- 동시 연결: 100,000+
- 처리량: 50,000 req/s
- 메모리: 수백 MB
12. 실전 최적화
Zero-Copy
일반적인 데이터 전송:
1. 커널 → 애플리케이션 버퍼 (복사 1)
2. 애플리케이션 버퍼 → 커널 (복사 2)
총 2번 복사!
Zero-Copy (sendfile, splice):
아래 코드는 c를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Linux sendfile
#include <sys/sendfile.h>
// 파일을 소켓으로 직접 전송 (복사 없음)
off_t offset = 0;
sendfile(client_fd, file_fd, &offset, file_size);
// 커널 내부에서만 처리, 애플리케이션 버퍼 거치지 않음
SO_REUSEPORT (Linux 3.9+)
아래 코드는 c를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 여러 프로세스가 같은 포트 listen
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
// 커널이 자동으로 로드 밸런싱
// Nginx, HAProxy가 사용
TCP_NODELAY
아래 코드는 c를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Nagle 알고리즘 비활성화 (지연 감소)
int flag = 1;
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
// 작은 패킷도 즉시 전송
// 실시간 애플리케이션 (게임, 채팅)에 필수
13. 실전 서버 아키텍처
단일 스레드 이벤트 루프 (Node.js, Redis)
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
┌─────────────────────────────────┐
│ Main Thread │
│ ┌───────────────────────────┐ │
│ │ Event Loop │ │
│ │ - epoll_wait() │ │
│ │ - 이벤트 처리 │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
장점:
- 간단한 구조
- 락 불필요
- 컨텍스트 스위칭 없음
단점:
- CPU 1개만 사용
- CPU 집약적 작업에 부적합
멀티 프로세스 (Nginx)
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
┌─────────────────────────────────┐
│ Master Process │
│ - 설정 관리 │
│ - Worker 관리 │
└──────────┬──────────────────────┘
│
┌──────┼──────┐
│ │ │
┌───▼──┐ ┌─▼───┐ ┌─▼───┐
│Worker│ │Worker│ │Worker│
│epoll │ │epoll │ │epoll │
└──────┘ └──────┘ └──────┘
장점:
- 멀티 코어 활용
- 프로세스 격리 (안정성)
단점:
- 메모리 사용 증가
- 프로세스 간 통신 필요
스레드 풀 (IOCP, Boost.Asio)
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
┌─────────────────────────────────┐
│ Main Thread │
│ - accept() 처리 │
└──────────┬──────────────────────┘
│
┌──────┼──────┐
│ │ │
┌───▼──┐ ┌─▼───┐ ┌─▼───┐
│Worker│ │Worker│ │Worker│
│Thread│ │Thread│ │Thread│
│ IOCP │ │ IOCP │ │ IOCP │
└──────┘ └──────┘ └──────┘
장점:
- 멀티 코어 활용
- 메모리 공유 (효율적)
단점:
- 동기화 필요 (락)
- 디버깅 어려움
14. 실전 예제: HTTP 서버
epoll 기반 HTTP 서버
다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define MAX_EVENTS 1024
#define PORT 8080
const char* HTTP_RESPONSE =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: 13\r\n"
"\r\n"
"Hello, World!";
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
// 서버 소켓 생성
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblocking(server_fd);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
// epoll 생성
int epoll_fd = epoll_create1(0);
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
struct epoll_event events[MAX_EVENTS];
printf("HTTP Server listening on http://localhost:%d\n", PORT);
while (1) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == server_fd) {
// 새 연결
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) break;
set_nonblocking(client_fd);
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
}
} else {
// HTTP 요청 처리
char buffer[4096];
int n = recv(fd, buffer, sizeof(buffer), 0);
if (n <= 0) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
} else {
// HTTP 응답 전송
send(fd, HTTP_RESPONSE, strlen(HTTP_RESPONSE), 0);
// Keep-Alive 미지원, 즉시 종료
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}
}
}
close(server_fd);
close(epoll_fd);
return 0;
}
컴파일 및 실행:
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
gcc -o http_server http_server.c
./http_server
# 테스트
curl http://localhost:8080
# Hello, World!
# 벤치마크
ab -n 100000 -c 1000 http://localhost:8080/
15. 성능 튜닝
커널 파라미터 최적화
Linux:
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# /etc/sysctl.conf
# 최대 파일 디스크립터
fs.file-max = 1000000
# TCP 버퍼 크기
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# SYN backlog 크기
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192
# TIME_WAIT 소켓 재사용
net.ipv4.tcp_tw_reuse = 1
# 적용
sudo sysctl -p
프로세스 제한:
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# /etc/security/limits.conf
* soft nofile 1000000
* hard nofile 1000000
# 확인
ulimit -n
애플리케이션 최적화
1) 버퍼 크기 조정
다음은 간단한 c 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 소켓 버퍼 크기 증가
int buffer_size = 1024 * 1024; // 1MB
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size));
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size));
2) 타임아웃 설정
아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 읽기 타임아웃
struct timeval timeout;
timeout.tv_sec = 30;
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
3) Keep-Alive
아래 코드는 c를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// TCP Keep-Alive 활성화
int keepalive = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
// Keep-Alive 간격
int keepidle = 60; // 60초 후 첫 probe
int keepintvl = 10; // 10초 간격
int keepcnt = 3; // 3번 실패 시 종료
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
16. 실전 사례 연구
Nginx (epoll)
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
Nginx 설정:
worker_processes auto; # CPU 코어 수
worker_connections 10000; # Worker당 최대 연결
events {
use epoll;
multi_accept on;
}
성능:
- 동시 연결: 100,000+
- 처리량: 50,000 req/s
- 메모리: 500MB
- CPU: 4코어
Node.js (libuv)
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Node.js는 내부적으로 libuv 사용
// libuv가 플랫폼별 최적 메커니즘 선택
// - Linux: epoll
// - macOS: kqueue
// - Windows: IOCP
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(8080);
// 단일 스레드로 수만 개 연결 처리
Redis (epoll/kqueue)
아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Redis 이벤트 루프 (ae.c)
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
// 내부적으로 epoll/kqueue 사용
// 단일 스레드로 초당 100,000+ 명령 처리
17. 비교표
종합 비교
| 항목 | select | poll | epoll | kqueue | IOCP |
|---|---|---|---|---|---|
| 플랫폼 | Unix/Linux/Windows | Unix/Linux | Linux | BSD/macOS | Windows |
| 최대 fd | 1024 | 무제한 | 무제한 | 무제한 | 무제한 |
| 복잡도 | O(n) | O(n) | O(k) | O(k) | O(k) |
| 성능 | 낮음 | 낮음 | 높음 | 높음 | 매우 높음 |
| 이벤트 종류 | I/O | I/O | I/O | I/O+파일+시그널+타이머 | I/O |
| 알림 방식 | Readiness | Readiness | Readiness | Readiness | Completion |
| 스레드 안전 | 아니오 | 아니오 | 예 | 예 | 예 |
선택 가이드
다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
연결 수 < 100:
→ select/poll (충분히 빠름, 간단함)
연결 수 100~1,000:
→ epoll/kqueue (권장)
연결 수 10,000+:
→ epoll/kqueue/IOCP (필수)
크로스 플랫폼:
→ libuv, libevent, Boost.Asio
Windows:
→ IOCP (최고 성능)
Linux:
→ epoll (최고 성능)
macOS:
→ kqueue (최고 성능)
18. 트러블슈팅
자주 발생하는 문제
1) “Too many open files” 에러
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 원인: 파일 디스크립터 제한 초과
# 해결:
ulimit -n 100000
# 영구 설정
echo "* soft nofile 100000" >> /etc/security/limits.conf
echo "* hard nofile 100000" >> /etc/security/limits.conf
2) epoll_wait() 반환 안됨
아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 원인: Edge-Triggered 모드에서 데이터 남음
// 해결: EAGAIN까지 읽기
while (1) {
int n = recv(fd, buffer, sizeof(buffer), 0);
if (n < 0) {
if (errno == EAGAIN) break; // 필수!
}
// 처리
}
3) 메모리 누수
아래 코드는 c를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 원인: 연결 종료 시 리소스 미해제
// 해결:
if (n <= 0) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); // epoll에서 제거
close(fd); // 소켓 닫기
free(client_data); // 메모리 해제
}
4) 성능 저하
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 원인: 커널 파라미터 미조정
# 해결:
# 1. TCP 버퍼 크기 증가
# 2. backlog 크기 증가
# 3. TIME_WAIT 재사용 활성화
19. 고급 주제
io_uring (Linux 5.1+, 2019)
io_uring은 Linux의 차세대 비동기 I/O다.
아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
기존 (epoll):
- 시스템 콜 필요 (커널 ↔ 유저 전환)
- epoll_wait() → recv() → send()
io_uring:
- 공유 메모리 링 버퍼
- 시스템 콜 최소화
- 배치 처리 가능
성능:
- epoll 대비 2~3배 빠름
- 특히 고부하에서 효과적
io_uring 예제:
다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <liburing.h>
struct io_uring ring;
io_uring_queue_init(256, &ring, 0);
// 비동기 read 요청
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
io_uring_submit(&ring);
// 완료 대기
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 결과 처리
int bytes_read = cqe->res;
io_uring_cqe_seen(&ring, cqe);
eBPF + XDP (초고성능)
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
eBPF (Extended Berkeley Packet Filter):
- 커널 내부에서 패킷 처리
- 유저 공간 전환 없음
- DDoS 방어, 로드 밸런싱
XDP (eXpress Data Path):
- 네트워크 드라이버 단계에서 처리
- 초당 수천만 패킷 처리
- Cloudflare, Facebook이 사용
20. 프로덕션 체크리스트
필수 설정
논블로킹 소켓, epoll/kqueue에서 ET 사용 시 읽기 루프 처리, ulimit 등 fd 상한, TCP 버퍼·SO_REUSEADDR·실시간 트래픽이면 TCP_NODELAY, Keep-Alive, EAGAIN/EINTR 처리, 연결 종료 시 리소스 해제, 로그·메트릭은 운영 단계에서 빠지기 쉬우니 미리 자리를 잡는다.
모니터링
아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 연결 수 확인
netstat -an | grep ESTABLISHED | wc -l
# 소켓 상태 확인
ss -s
# epoll 사용 확인
strace -e epoll_wait ./server
# 성능 프로파일링
perf record -g ./server
perf report
FAQ
Q1. select는 이제 쓰지 않나?
연결이 적거나 이식성이 최우선이면 select로도 충분한 경우가 있다. 동시 연결이 커지면 epoll·kqueue 쪽이 부담이 적다.
Q2. epoll과 IOCP 중 더 빠른 쪽은?
벤치마크·워크로드마다 달라서 숫자만으로 단정하기 어렵다. Windows면 IOCP, Linux면 epoll처럼 플랫폼에 맞추는 게 먼저다.
Q3. Node.js는 내부적으로 무엇을 쓰나?
libuv가 OS별로 epoll·kqueue·IOCP 등을 고른다. 애플리케이션 코드는 그 위에서 돈다고 보면 된다.
Q4. 멀티스레드와 멀티플렉싱을 같이 쓸 수 있나?
흔하다. accept만 전담하고 워커마다 epoll 루프를 돌리거나, CPU 코어 수에 맞춰 나누는 식으로 설계한다.
요약
핵심 정리
I/O 멀티플렉싱:
- 하나의 스레드로 여러 I/O 동시 처리
- 고성능 네트워크 서버의 핵심
메커니즘 비교:
- select/poll: O(n), 소규모
- epoll/kqueue: O(k), 대규모
- IOCP: Completion 기반, Windows
선택 기준:
- Linux → epoll
- macOS/BSD → kqueue
- Windows → IOCP
- 크로스 플랫폼 → libuv, Boost.Asio
성능:
- select: ~30,000 req/s
- epoll: ~200,000 req/s
- IOCP: ~250,000 req/s
학습 로드맵
블로킹 소켓과 select로 멀티 소켓의 뼈대를 만든 뒤, 논블로킹과 epoll·kqueue로 옮겨 보면 차이가 체감된다. 그다음 HTTP 같은 프로토콜 한 줄, 튜닝·배포, 필요하면 zero-copy·io_uring·프로세스 모델로 넓히면 된다.
다음 글 추천
키워드: I/O Multiplexing, select, poll, epoll, kqueue, IOCP, 비동기 I/O, 네트워크, 고성능, C10K, Reactor, Proactor