当前位置: 首页 > news >正文

用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)

  1. 无文件描述符数量限制

    • 这是 poll 相对于 select 最显著的优点。 poll 使用一个由用户分配的 struct pollfd 数组,其大小仅受系统内存和进程所能打开的文件描述符上限(ulimit -n)的限制,彻底摆脱了 select 那个 1024 的硬性限制 (FD_SETSIZE)。这使得 poll 能够轻松处理数千个并发连接。
  2. 更优的事件管理

    • poll 将“监听的事件”(events 字段)和“实际发生的事件”(revents 字段)分离开。
    • 无需重复初始化:内核在返回时只会修改 revents 字段,而不会改动 events。这意味着你不需要在每次调用 poll 之前都重新设置整个监听集合,大大简化了编程逻辑,也减少了一些微小的运行时开销。
  3. 更丰富的事件类型

    • poll 提供了比 select 更细粒度的事件类型,例如:
      • POLLRDHUP (Linux 2.6.17+): 对端关闭连接(或关闭写一半),非常有用,无需读取就能知道连接已关闭。
      • POLLPRI: 高优先级数据可读(例如带外数据 OOB)。
      • POLLERR, POLLHUP, POLLNVAL 这些错误事件被单独定义在 revents 中,无需像 select 那样通过异常集合来处理。
    • 这使得事件处理更加精确和清晰。
  4. 更好的扩展性

    • 在处理大量连接时,虽然性能仍是 O(n),但 poll 不会像 select 那样在 1024 这个数量级上遇到天花板,为应用程序提供了更大的扩展空间。

poll 的缺点 (依然存在的根本问题)

尽管 poll 解决了 select 的一些表面问题,但它与 select 共享了最致命的根本性缺陷:

  1. 性能随连接数线性下降 (与 select 一样)

    • 这是 poll 最大的缺点,也是它被 epoll 淘汰的主要原因。
    • 工作原理poll 本质上仍然是轮询。每次调用时,无论文件描述符是否活跃,内核都必须线性扫描整个传入的 pollfd 数组。应用程序在 poll 返回后,也需要线性扫描整个数组来查找哪些 revents 不为零。
    • 后果:当管理的连接数(n)非常大(例如上万),但其中只有很少一部分是活跃的时,这种 O(n) 的扫描开销会变得极其巨大,造成严重的性能瓶颈。
  2. 大量的内存拷贝开销 (与 select 一样)

    • 每次调用 poll,都需要将整个(可能非常庞大的)pollfd 数组从用户空间拷贝到内核空间。
    • poll 返回时,内核又需要将整个(包含更新后的 revents 的)数组拷贝回用户空间。
    • 对于海量连接,这种内存拷贝的消耗是惊人的。
  3. 水平触发 (LT) 模式可能引起效率问题

    • poll 只支持水平触发(Level-Triggered)模式。
    • 含义:只要文件描述符的读缓冲区不为空,poll 就会一直报告该fd可读;只要写缓冲区有空间,就会一直报告可写。
    • 问题:如果应用程序一次没有读完所有数据,下次调用 poll 时会立即再次返回,告知该fd依然可读。这可能导致程序频繁地被唤醒处理同一个fd,如果处理不当,会影响效率。(注意:这不是 poll 独有的问题,select 也是 LT 模式,但 epoll 提供了高效的边缘触发 ET 模式作为解决方案)。

总结对比表格

特性selectpollepoll (作为对比)
连接数限制有 (1024)
事件管理差 (需每次重建集合) (监听与返回分离)最好 (内核托管)
事件类型少 (读/写/异常)丰富 (POLLRDHUP等)丰富 (EPOLLRDHUP等)
内核实现机制轮询轮询回调 (Callback)
性能随连接数增长线性下降 O(n)线性下降 O(n)高效 O(1)
内存拷贝开销 (每次拷贝整个fd_set) (每次拷贝整个pollfd数组) (共享内存+mmap)
触发模式水平触发 (LT)水平触发 (LT)支持水平触发(LT)和边缘触发(ET)

结论与建议

  • pollselect 的一个优秀替代品。它解决了 select 的最大痛点——文件描述符数量限制,并提供了更清晰的事件管理接口。

  • 然而,poll 并没有解决 select 最根本的性能问题。它和 select 一样,在面对“万级连接,但只有少量活跃”的经典高性能网络场景(例如 IM、推送服务、HTTP长连接网关)时,性能会急剧下降。

  • 使用 poll 的场景

    1. 需要处理的连接数超过 1024,但还远未达到万级规模(例如几千个)。
    2. 追求跨平台兼容性(虽然不如 select 普遍,但 poll 在大多数 UNIX-like 系统上也可用),同时又需要突破 select 的数量限制。
    3. 应用程序的连接活跃度很高,即大部分被监听的连接在同一时间都是活跃的。这样,O(n) 的扫描才不算浪费。
  • 避免使用 poll 的场景

    1. 需要处理成千上万个并发连接,且其中只有少部分是活跃的。这是 epollkqueue 的绝对主场。
    2. 性能和可扩展性有极致要求的场景。

一言以蔽之:poll 打破了 select 的牢笼,但依然被困在“轮询”的深渊里。而 epoll 则用“回调”机制真正实现了飞跃。 在现代Linux高性能网络编程中,poll 通常只是一个过渡选择,最终都会走向 epoll

http://www.lryc.cn/news/626199.html

相关文章:

  • RabbitMQ:SpringAMQP Direct Exchange(直连型交换机)
  • 在Excel和WPS表格中为多个数字同时加上相同的数值
  • 如何解析PDF中的复杂表格数据
  • UniApp 实现pdf上传和预览
  • Go语言快速入门指南(面向Java工程师)
  • 智慧校园中IPTV融合对讲:构建高效沟通新生态
  • DHCP详解
  • sqlite-gui:一款开源免费、功能强大的SQLite开发工具
  • Netty 集成 protobuf
  • 代码随想录刷题——字符串篇(七)
  • 机械原理的齿轮怎么学?
  • Transformer中的编码器和解码器是什么?
  • ubuntu安装kconfig-frontends提示报错
  • SpringAI——向量存储(vector store)
  • 【Netty4核心原理⑫】【异步处理双子星 Future 与 Promise】
  • 企业架构是什么?解读
  • Leetcode 深度优先搜索 (6)
  • 骑行初体验
  • 从“为什么”到“怎么做”——Linux Namespace 隔离实战全景地图
  • CentOS安装SNMPWalk
  • Vue.prototype 的作用
  • 基于 STM32 单片机的远程老人监测系统设计
  • 从踩坑到精通:Java 深拷贝与浅拷贝
  • 算法题Day3
  • 1688商品详情API接口操作指南及实战讲解
  • 告别手写文档!Spring Boot API 文档终极解决方案:SpringDoc OpenAPI
  • 信号和共享内存
  • 理解MCP:开发者的新利器
  • string 题目练习 过程分析 具体代码
  • Redis(10)如何连接到Redis服务器?