高并发网络开发:select、poll、epoll 多路复用实战
2026/7/22 18:12:36 网站建设 项目流程

高并发网络开发:select、poll、epoll 多路复用实战

一、引言

上一篇文章我们写了个 TCP 服务端——单线程、一次只服务一个客户端。这种模式在嵌入式设备上勉强能用(毕竟通常只有一个上位机连接),但遇到需要同时服务几十上百个客户端的高并发场景,就彻底歇菜了。

那是不是可以每个客户端都fork一个进程或者pthread_create一个线程来处理?理论可行,但进程/线程的开销太大了——内存、调度、上下文切换,数量一多系统直接崩溃。

正确的解法是IO 多路复用(Multiplexing):一个线程同时监听多个 Socket,哪个有数据就处理哪个。Linux 提供了三种标准 API:select、poll、epoll。本文一次讲清三者的区别,并给出 epoll 的高并发实战代码。

二、三兄弟对比

特性selectpollepoll
监听上限FD_SETSIZE(默认1024)无限制无限制
数据结构fd_set(位图,固定大小)struct pollfd(动态数组)红黑树 + 就绪链表
遍历方式每次 O(n) 全量扫描每次 O(n) 全量扫描O(1) 只返回就绪的
数据复制每次调用都要全部重新设置每次调用传入只需添加/删除一次
内核态开销大(每次重新设置)小(事件驱动)
适用规模几十个连接几百个连接几千到几十万连接

一句话:select 和 poll 每隔一段时间去逐一巡查所有连接(轮询模式),epoll 则是让内核主动通知哪个连接有事件(事件驱动模式)。

select/poll: 用户态 ─── 遍历所有fd(1到N) ─── 内核 ↓ 用户态 ←── 返回所有fd,你判断哪些就绪 ←── 内核 epoll: 用户态 ─── epoll_ctl 注册事件 ─── 内核(红黑树) 用户态 ←── epoll_wait 只返回就绪的fd ←── 内核(就绪链表)

三、select 示例(仅供学习)

#include<sys/select.h>#include<sys/time.h>fd_set readfds;FD_ZERO(&readfds);FD_SET(server_fd,&readfds);while(1){fd_set tmp=readfds;// 每次都要复制!因为 select 会修改它intret=select(max_fd+1,&tmp,NULL,NULL,NULL);if(ret<0){perror("select");break;}for(intfd=0;fd<=max_fd;fd++){if(!FD_ISSET(fd,&tmp))continue;if(fd==server_fd){// 新连接intclient=accept(server_fd,...);FD_SET(client,&readfds);if(client>max_fd)max_fd=client;}else{// 客户端数据handle_client(fd);}}}

select 的槽点很明显:

  • fd_set大小固定,最大 1024
  • 每次调用前要重新复制fd_set
  • 每次调用后要遍历所有 fd 找出就绪的

四、epoll 实战:高并发服务端

epoll 是 Linux 独有的高性能 IO 多路复用机制,也是 Nginx、Redis 等高性能服务器的底层基石。

三个核心函数

#include<sys/epoll.h>// 创建 epoll 实例intepoll_create1(intflags);// 注册/修改/删除监听事件intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);// 等待事件intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);

扩展实现,epoll 内部使用红黑树管理所有监听的文件描述符(增删改 O(log n)),当某个 fd 就绪时,内核将其插入到就绪链表epoll_wait直接从链表中取——复杂度 O(1)。

完整的 epoll 高并发服务器

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<errno.h>#include<fcntl.h>#include<arpa/inet.h>#include<sys/epoll.h>#definePORT8888#defineMAX_EVENTS1024#defineBUF_SIZE1024/* 设置 fd 为非阻塞 */staticvoidset_nonblocking(intfd){intflags=fcntl(fd,F_GETFL,0);fcntl(fd,F_SETFL,flags|O_NONBLOCK);}/* 处理客户端数据 */staticvoidhandle_client(intclient_fd){charbuf[BUF_SIZE];ssize_tn=read(client_fd,buf,sizeof(buf)-1);if(n<=0){// 客户端断开if(n==0){printf("[客户端 %d] 正常断开\n",client_fd);}elseif(errno!=EAGAIN){perror("read");}close(client_fd);return;}buf[n]='\0';printf("[客户端 %d] 收到: %s\n",client_fd,buf);// 回显(简化处理)write(client_fd,buf,n);}intmain(void){intserver_fd,epoll_fd;structsockaddr_inaddr;// ---------- 1. 创建监听 Socket ----------server_fd=socket(AF_INET,SOCK_STREAM,0);set_nonblocking(server_fd);intopt=1;setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));memset(&addr,0,sizeof(addr));addr.sin_family=AF_INET;addr.sin_addr.s_addr=INADDR_ANY;addr.sin_port=htons(PORT);bind(server_fd,(structsockaddr*)&addr,sizeof(addr));listen(server_fd,SOMAXCONN);// ---------- 2. 创建 epoll 实例 ----------epoll_fd=epoll_create1(0);if(epoll_fd<0){perror("epoll_create1");exit(1);}// ---------- 3. 将 server_fd 加入 epoll ----------structepoll_eventev,events[MAX_EVENTS];ev.events=EPOLLIN;// 监听可读事件ev.data.fd=server_fd;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,server_fd,&ev);printf("epoll 高并发服务器启动, 端口=%d\n",PORT);// ---------- 4. 事件循环 ----------while(1){intnfds=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);// -1 = 永久等待if(nfds<0){perror("epoll_wait");break;}for(inti=0;i<nfds;i++){intfd=events[i].data.fd;if(fd==server_fd){// 新连接while(1){structsockaddr_inclient_addr;socklen_tlen=sizeof(client_addr);intclient=accept(server_fd,(structsockaddr*)&client_addr,&len);if(client<0){if(errno==EAGAIN||errno==EWOULDBLOCK)break;// 没有更多连接了perror("accept");break;}printf("[新客户端] fd=%d, %s:%d\n",client,inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));set_nonblocking(client);ev.events=EPOLLIN|EPOLLET;// 边缘触发ev.data.fd=client;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,client,&ev);}}else{// 客户端数据handle_client(fd);}}}close(server_fd);close(epoll_fd);return0;}

边缘触发 (ET) vs 水平触发 (LT)

epoll 有两种工作模式:

模式行为说明
LT(Level Triggered,默认)fd 只要还有数据可读,每次epoll_wait都会通知不容易丢事件,代码简单
ET(Edge Triggered)只在 fd 状态变化时通知一次(比如从无数据变为有数据)性能更高,但必须用非阻塞 IO + 循环读到 EAGAIN

上面代码使用了EPOLLET(边缘触发),代价是代码必须循环accept和循环read直到 EAGAIN,但换来了更高的吞吐量。

// ET 模式下的正确处理方式:循环读到无数据为止while(1){ssize_tn=read(fd,buf,sizeof(buf));if(n<0){if(errno==EAGAIN)break;// 读完了// 真错误close(fd);break;}if(n==0){close(fd);// 对端关闭break;}// 处理这 n 字节数据...}

五、三种 IO 模型的性能拓扑

并发连接数 │ │ epoll ─────────→ │ poll ─────→ │ select ──→ │ fork ─→ └────────────────────────────────────→ 性能

在几十个连接的量级,三种方案差别不大。但到了几百甚至几千个连接,epoll 的优势就碾压式地显现出来——因为它只处理有事件的 fd,不浪费一个 CPU 周期在空闲连接上

六、总结

  1. select:实现简单但不堪重负(最大 1024,每次全量复制)
  2. poll:取消了 fd 数量限制,但仍是轮询模式
  3. epoll:事件驱动,红黑树管理,O(1) 获取就绪事件——高性能服务器的标配
  4. LT vs ET:LT 省心,ET 高效但要求非阻塞 IO
  5. 实际选择:嵌入式设备几十个连接用 select/poll 完全够,工业级服务器用 epoll

把 epoll 的epoll_create1 → epoll_ctl → epoll_wait这套流程写熟练,你就算真正入门了 Linux 高性能网络编程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询