用poll改写select
用poll改写select
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/poll.h> // poll函数头文件
#include "Socket.hpp"
#include "Log.hpp"using namespace SocketModule;
using namespace LogModule;class PollServer
{// pollfd数组大小const static int size=4096;// 默认无效文件描述符const static int defaultfd=-1;public:// 构造函数:初始化监听Socket和pollfd数组PollServer(int port):_listensock(std::make_unique<TcpSocket>()),_isrunning(false){// 创建并监听TCP Socket_listensock->BuildTcpSocketMethod(port);// 初始化pollfd数组for(int i=0;i<size;i++){// 文件描述符初始为无效_fds[i].fd=defaultfd;// 监听事件初始为0_fds[i].events=0;// 返回事件初始为0_fds[i].revents=0;}// 设置监听Socket到数组第一个位置_fds[0].fd=_listensock->Fd();// 监听读事件(新连接)_fds[0].events=POLLIN;}//主事件循环void Start(){// poll超时时间:-1表示无限等待int timeout=-1;_isrunning=true;while(_isrunning){// 打印当前监控的fd(调试用)// 调用poll监听所有文件描述符int n=poll(_fds,size,timeout);switch (n){case -1: // 错误情况LOG(LogLevel::ERROR) << "poll error";break;case 0: // 超时(未发生任何事件)LOG(LogLevel::INFO) << "poll time out...";break;default: // 有事件就绪LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;Dispatcher(); // 分发处理就绪事件break;}}_isrunning=false;}//事件分发器 void Dispatcher(){// 遍历所有pollfd结构体for(int i=0;i<size;i++){// 跳过无效fdif(_fds[i].fd==defaultfd)continue;// 检查是否有读事件就绪if(_fds[i].fd==_listensock->Fd()){// 接受新连接Accepter();}else{// 读取数据Recver(i); }}// 检查是否有读事件就绪}//新连接处理void Accepter(){InetAddr client;// 接受新连接int sockfd=_listensock->Accept(&client);// 接受成功if(sockfd>=0){LOG(LogLevel::INFO) << "get a new link, sockfd: "<< sockfd << ", client is: " << client.StringAddr();// 在pollfd数组中寻找空位int pos=0;for( ;pos<size;pos++){if (_fds[pos].fd == defaultfd)break;}// 数组已满if(pos==size){LOG(LogLevel::WARNING) << "poll server full";// 关闭连接close(sockfd); }// 找到空位,添加新连接else{// 设置文件描述符_fds[pos].fd = sockfd;// 监听读事件_fds[pos].events = POLLIN;// 重置返回事件_fds[pos].revents = 0; }}}// 参数是pollfd数组中的位置void Recver(int pos)
{char buffer[1024];// 读取客户端数据ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0);if (n > 0) // 正常读取到数据{buffer[n] = 0; // 添加字符串结束符std::cout << "client say@ " << buffer << std::endl;}else if (n == 0) // 客户端关闭连接{LOG(LogLevel::INFO) << "clien quit...";close(_fds[pos].fd); // 关闭文件描述符_fds[pos].fd = defaultfd; // 从监控数组中移除_fds[pos].events = 0; // 清除事件监听_fds[pos].revents = 0; // 清除返回事件}else // 读取错误{LOG(LogLevel::ERROR) << "recv error";close(_fds[pos].fd); // 关闭文件描述符_fds[pos].fd = defaultfd; // 从监控数组中移除_fds[pos].events = 0; // 清除事件监听_fds[pos].revents = 0; // 清除返回事件}
}// 打印当前监控的所有文件描述符(调试用)
void PrintFd()
{std::cout << "_fds[]: ";for (int i = 0; i < size; i++){if (_fds[i].fd == defaultfd)continue;std::cout << _fds[i].fd << " ";}std::cout << "\r\n";
}// 停止服务器
void Stop()
{_isrunning = false;
}~PollServer(){}private:// 监听Socket智能指针std::unique_ptr<Socket> _listensock;// 服务器运行状态标志bool _isrunning;// pollfd结构体数组struct pollfd _fds[size];
};
poll 的优点 (相对于 select)
-
无文件描述符数量限制
- 这是
poll
相对于select
最显著的优点。poll
使用一个由用户分配的struct pollfd
数组,其大小仅受系统内存和进程所能打开的文件描述符上限(ulimit -n
)的限制,彻底摆脱了select
那个 1024 的硬性限制 (FD_SETSIZE
)。这使得poll
能够轻松处理数千个并发连接。
- 这是
-
更优的事件管理
poll
将“监听的事件”(events
字段)和“实际发生的事件”(revents
字段)分离开。- 无需重复初始化:内核在返回时只会修改
revents
字段,而不会改动events
。这意味着你不需要在每次调用poll
之前都重新设置整个监听集合,大大简化了编程逻辑,也减少了一些微小的运行时开销。
-
更丰富的事件类型
poll
提供了比select
更细粒度的事件类型,例如:POLLRDHUP
(Linux 2.6.17+): 对端关闭连接(或关闭写一半),非常有用,无需读取就能知道连接已关闭。POLLPRI
: 高优先级数据可读(例如带外数据 OOB)。POLLERR
,POLLHUP
,POLLNVAL
这些错误事件被单独定义在revents
中,无需像select
那样通过异常集合来处理。
- 这使得事件处理更加精确和清晰。
-
更好的扩展性
- 在处理大量连接时,虽然性能仍是 O(n),但
poll
不会像select
那样在 1024 这个数量级上遇到天花板,为应用程序提供了更大的扩展空间。
- 在处理大量连接时,虽然性能仍是 O(n),但
poll 的缺点 (依然存在的根本问题)
尽管 poll
解决了 select
的一些表面问题,但它与 select
共享了最致命的根本性缺陷:
-
性能随连接数线性下降 (与 select 一样)
- 这是
poll
最大的缺点,也是它被epoll
淘汰的主要原因。 - 工作原理:
poll
本质上仍然是轮询。每次调用时,无论文件描述符是否活跃,内核都必须线性扫描整个传入的pollfd
数组。应用程序在poll
返回后,也需要线性扫描整个数组来查找哪些revents
不为零。 - 后果:当管理的连接数(n)非常大(例如上万),但其中只有很少一部分是活跃的时,这种 O(n) 的扫描开销会变得极其巨大,造成严重的性能瓶颈。
- 这是
-
大量的内存拷贝开销 (与 select 一样)
- 每次调用
poll
,都需要将整个(可能非常庞大的)pollfd
数组从用户空间拷贝到内核空间。 - 当
poll
返回时,内核又需要将整个(包含更新后的revents
的)数组拷贝回用户空间。 - 对于海量连接,这种内存拷贝的消耗是惊人的。
- 每次调用
-
水平触发 (LT) 模式可能引起效率问题
poll
只支持水平触发(Level-Triggered)模式。- 含义:只要文件描述符的读缓冲区不为空,
poll
就会一直报告该fd可读;只要写缓冲区有空间,就会一直报告可写。 - 问题:如果应用程序一次没有读完所有数据,下次调用
poll
时会立即再次返回,告知该fd依然可读。这可能导致程序频繁地被唤醒处理同一个fd,如果处理不当,会影响效率。(注意:这不是poll
独有的问题,select
也是 LT 模式,但epoll
提供了高效的边缘触发 ET 模式作为解决方案)。
总结对比表格
特性 | select | poll | epoll (作为对比) |
---|---|---|---|
连接数限制 | 有 (1024) | 无 | 无 |
事件管理 | 差 (需每次重建集合) | 好 (监听与返回分离) | 最好 (内核托管) |
事件类型 | 少 (读/写/异常) | 丰富 (POLLRDHUP等) | 丰富 (EPOLLRDHUP等) |
内核实现机制 | 轮询 | 轮询 | 回调 (Callback) |
性能随连接数增长 | 线性下降 O(n) | 线性下降 O(n) | 高效 O(1) |
内存拷贝开销 | 大 (每次拷贝整个fd_set) | 大 (每次拷贝整个pollfd数组) | 小 (共享内存+mmap) |
触发模式 | 水平触发 (LT) | 水平触发 (LT) | 支持水平触发(LT)和边缘触发(ET) |
结论与建议
-
poll
是select
的一个优秀替代品。它解决了select
的最大痛点——文件描述符数量限制,并提供了更清晰的事件管理接口。 -
然而,
poll
并没有解决select
最根本的性能问题。它和select
一样,在面对“万级连接,但只有少量活跃”的经典高性能网络场景(例如 IM、推送服务、HTTP长连接网关)时,性能会急剧下降。 -
使用
poll
的场景:- 需要处理的连接数超过 1024,但还远未达到万级规模(例如几千个)。
- 追求跨平台兼容性(虽然不如
select
普遍,但poll
在大多数 UNIX-like 系统上也可用),同时又需要突破select
的数量限制。 - 应用程序的连接活跃度很高,即大部分被监听的连接在同一时间都是活跃的。这样,O(n) 的扫描才不算浪费。
-
避免使用
poll
的场景:- 需要处理成千上万个并发连接,且其中只有少部分是活跃的。这是
epoll
或kqueue
的绝对主场。 - 对性能和可扩展性有极致要求的场景。
- 需要处理成千上万个并发连接,且其中只有少部分是活跃的。这是
一言以蔽之:poll
打破了 select
的牢笼,但依然被困在“轮询”的深渊里。而 epoll
则用“回调”机制真正实现了飞跃。 在现代Linux高性能网络编程中,poll
通常只是一个过渡选择,最终都会走向 epoll
。