当我们需要同时处理多个网络连接时,传统阻塞式IO会为每个连接创建独立线程,这在C10K问题(单机维护1万个连接)场景下会耗尽系统资源。IO多路复用技术通过单线程监控多个文件描述符(fd)的状态变化,实现用1个线程处理成百上千个连接。
我在处理高并发爬虫服务时,曾用多线程方案处理500个连接就导致CPU占用率突破90%,而改用epoll后单线程即可稳定处理3000+连接。这种技术特别适合即时通讯、游戏服务器等需要高并发的场景。
select是POSIX标准最早提供的方案,其函数原型为:
c复制int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
实现原理:
性能瓶颈:
实际测试:监控1000个活跃连接时,select的CPU占用比epoll高8倍
poll通过改进数据结构解决了select的部分缺陷:
c复制int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; // 文件描述符
short events; // 监控的事件
short revents; // 返回的事件
};
核心改进:
现存问题:
Linux 2.6引入的epoll是当前最优解决方案,其核心API包括:
c复制int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
突破性设计:
在4核8G的云服务器上对三种方案进行压测(单位:QPS):
| 连接数 | select | poll | epoll |
|---|---|---|---|
| 100 | 12,000 | 13,500 | 15,200 |
| 1000 | 1,200 | 1,800 | 14,800 |
| 10000 | 崩溃 | 320 | 12,600 |
关键发现:
epoll_wait的maxevents:
(连接数/线程数)*1.2EPOLLONESHOT模式:
c复制event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
当多个线程/进程阻塞在同一个epoll_fd时,新连接可能唤醒所有等待者。推荐两种方案:
c复制int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int));
c复制event.events |= EPOLLEXCLUSIVE;
案例1:事件丢失
c复制while((n = read(fd, buf, BUF_SIZE)) > 0) {
// 处理数据
}
if (n < 0 && errno != EAGAIN) {
// 错误处理
}
案例2:CPU 100%
c复制if (events[i].events & (EPOLLERR | EPOLLHUP)) {
close(events[i].data.fd);
continue;
}
python复制import selectors
sel = selectors.DefaultSelector() # 自动选择最佳实现
def accept(sock):
conn, addr = sock.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn):
data = conn.recv(1024)
if data:
print(f"Received: {data.decode()}")
else:
sel.unregister(conn)
conn.close()
Go运行时使用epoll的优化实现:
当网卡收到数据包时:
通过sendfile系统调用实现:
c复制sendfile(out_fd, in_fd, NULL, file_size);
ET模式特点:
LT模式特点:
生产环境建议:对延迟敏感型服务用ET,一般业务用LT更稳妥
c复制void *worker(void *arg) {
while(1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(int i=0; i<n; i++) {
handle_event(events[i]);
}
}
}
每个线程独立epoll_fd:
c复制void *worker(void *arg) {
int epfd = epoll_create(1);
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
// 独立事件循环
}
io_uring(Linux 5.1+):
c复制struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, listen_fd, NULL, NULL, 0);
io_uring_submit(&ring);