poll 函数原理与 TCP 服务器构建详解

news/2025/10/22 11:33:35/文章来源:https://www.cnblogs.com/yxysuanfa/p/19157557

本文将从select函数的缺陷出发,详细介绍poll函数的设计理念、核心参数、使用方法,并通过完整代码实现一个poll版 TCP 服务器,同时对比selectpoll的差异,分析poll的优缺点。

前言:从 select 缺陷到 poll 的诞生

select作为早期 I/O 多路复用技术,存在两个核心缺陷,这直接推动了poll函数的出现

  1. 文件描述符(fd)数量上限select依赖fd_set位图结构(内核固定大小),默认上限通常为 1024(需修改内核参数才能调整),超过则报错。
  2. 参数输入输出耦合selectfd_set既是输入参数(指定要监视的 fd),也是输出参数(标记就绪的 fd)。每次调用select前,需重新初始化fd_set导致用户态频繁遍历和拷贝,增加额外开销。

poll函数针对这两个缺陷做了改进:

  • 突破 fd 数量上限(理论无限制,仅受系统资源约束);
  • 分离输入与输出参数(通过pollfd结构体的eventsrevents字段),无需每次调用前重新设置监视 fd。

但需注意:pollselect核心逻辑一致—— 均通过轮询检测 fd 状态,效率随 fd 数量增加而线性下降。

一、poll 函数核心解析

poll函数的功能与select完全一致:监视并等待多个文件描述符的状态变化,仅关注 I/O 过程中的 “等待” 阶段。

1.1 核心参数:struct pollfd 数组

select使用位图(fd_set)管理 fd,而poll通过pollfd结构体数组管理,每个结构体对应一个待监视的 fd 及其事件。

pollfd 结构体定义
struct pollfd {int fd;         // 待监视的文件描述符(如socket、管道、普通文件)short events;   // 输入参数:用户要监视的事件(位掩码)short revents;  // 输出参数:内核返回的就绪事件(位掩码,用户无需初始化)
};
关键事件宏定义

events(输入)和revents(输出)均通过位掩码表示事件,支持多种事件的组合(用|运算符)。

事件宏含义(events 输入 /revents 输出)对应 select 功能
POLLIN有数据可读(普通 / 优先数据)读事件(FD_SET 读集合)
POLLRDNORM有普通数据可读-
POLLRDBAND有优先数据可读-
POLLPRI有高优先级数据可读(如带外数据)-
POLLOUT写操作不会阻塞(普通 / 优先数据)写事件(FD_SET 写集合)
POLLWRNORM写普通数据不会阻塞-
POLLWRBAND写优先数据不会阻塞-
POLLMSG有 SIGPOLL 消息可用-
POLLERR输出专属:fd 发生错误异常事件
POLLHUP输出专属:fd 发生挂起(如客户端断开连接)异常事件
POLLNVAL输出专属:fd 非法(如未打开)异常事件

使用规则

  • events仅能设置 “输入事件”(如POLLINPOLLOUT,设置POLLERR等输出事件无意义;
  • revents由内核填充,可能包含events中的事件,也可能包含POLLERR等异常事件;
  • 事件组合示例:监视 fd “可读且可写”,需设置events = POLLIN | POLLOUT
  • 事件判断示例:判断 fd 是否可读,需检查revents & POLLIN(非 0 则就绪)。

1.2 参数二:nfds(数组大小)

nfds表示pollfd数组中有效元素的数量,类型为nfds_t(通常是unsigned intunsigned long的别名,取决于系统)。

作用告诉内核需要遍历的pollfd结构体数量,避免内核访问数组越界。

示例:若pollfd数组为fds[2],则nfds = 2(或通过sizeof(fds)/sizeof(fds[0])计算)。

1.3 参数三:timeout(超时时间)

timeout指定poll的阻塞时长,单位为毫秒,是纯输入参数(无select的超时参数未定义问题)。

timeout 取值含义
-1无限阻塞,直到有 fd 就绪或被信号中断
0非阻塞模式,立即返回(无论是否有 fd 就绪)
>0阻塞timeout毫秒,超时后返回(无 fd 就绪)

1.4 返回值(返回状态)

poll的返回值直接反映调用结果,需根据返回值做不同处理:

返回值含义后续操作
-1调用失败(如 fd 非法、内存不足)检查errno(如 EBADF、EINTR),处理错误
0超时(无任何 fd 就绪)无需处理 fd,可重新调用poll
>0就绪 fd 的数量(revents非 0 的结构体个数)遍历pollfd数组,处理revents非 0 的 fd

1.5 poll 函数简单示例(监视文件可读)

以下代码演示如何用poll监视一个文件是否可读,超时时间 5 秒:

#include 
#include 
#include 
#include 
#include 
int main() {// 以只读模式打开文件(需确保test.txt存在)int fd = open("test.txt", O_RDONLY);if (fd < 0) {perror("Failed to open file");return EXIT_FAILURE;}// 初始化pollfd结构体(监视fd的可读事件)struct pollfd fds[1];fds[0].fd = fd;fds[0].events = POLLIN;  // 关注可读事件fds[0].revents = 0;      // 输出参数,可省略初始化(内核会覆盖)int timeout = 5000;  // 超时5秒int ret = poll(fds, 1, timeout);if (ret == -1) {perror("poll failed");close(fd);return EXIT_FAILURE;} else if (ret == 0) {printf("No data within 5 seconds\n");} else if (fds[0].revents & POLLIN) {// 读取文件内容并打印char buf[1024] = {0};ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1);if (bytes_read > 0) {printf("Read %zd bytes: %s\n", bytes_read, buf);}}close(fd);return EXIT_SUCCESS;
}

1.6 select 与 poll 的核心差异

对比维度selectpoll
fd 数量限制有(默认 1024,需改内核)无(仅受系统资源约束)
参数耦合性输入输出耦合(fd_set 需重新初始化)输入输出分离(events 输入,revents 输出)
效率(fd 多时)低(需遍历位图到最大 fd)略高(仅遍历有效 pollfd 数组)
超时精度微秒级(struct timeval)毫秒级(int)
可移植性高(所有 Unix 系统支持)中(部分嵌入式系统不支持)
异常事件处理需单独监视异常集合(如 FD_SET 异常位)自动在 revents 返回(POLLERR、POLLHUP)

二、poll 版 TCP 服务器实现

poll版 TCP 服务器的逻辑与select版类似,核心差异在于用pollfd数组管理 fd,而非fd_set位图。

2.1 整体设计思路

  1. 初始化监听 socket:创建、绑定、监听端口;
  2. 管理 pollfd 数组:将监听 socket 加入数组,设置监视事件(POLLIN);
  3. 循环调用 poll:阻塞等待 fd 就绪,处理超时、错误、就绪三种情况;
  4. 事件处理
    • 监听 socket 就绪:调用accept接受新连接,将新 socket 加入pollfd数组;
    • 通信 socket 就绪:调用read读取客户端数据,处理断开连接或错误。

2.2 完整代码实现

1. 工具类:Socket.hpp(封装 socket 操作)
#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// 错误码定义
enum {SocketErr = 2,BindErr,ListenErr
};
const int backlog = 10;  // 监听队列长度
class Sock {
public:Sock() : sockfd_(-1) {}~Sock() {}// 创建socket(TCP)void Socket() {sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (sockfd_ < 0) {perror("socket error");exit(SocketErr);}// 允许端口复用(解决服务器重启时端口占用问题)int opt = 1;setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));}// 绑定端口void Bind(uint16_t port) {struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;  // 绑定所有网卡IPif (bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {perror("bind error");exit(BindErr);}}// 监听端口void Listen() {if (listen(sockfd_, backlog) < 0) {perror("listen error");exit(ListenErr);}}// 接受新连接(返回新socket,输出客户端IP和端口)int Accept(std::string* clientip, uint16_t* clientport) {struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);if (newfd < 0) {perror("accept error");return -1;}// 转换客户端IP(网络字节序→主机字节序)char ipstr[64] = {0};inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}// 关闭socketvoid Close() {if (sockfd_ != -1) {close(sockfd_);sockfd_ = -1;}}// 获取socket文件描述符int Fd() const { return sockfd_; }
private:int sockfd_;  // 封装的socket fd
};
2. 服务器类:PollServer.hpp
#pragma once
#include 
#include "Socket.hpp"
#include 
#include 
// 配置参数
const uint16_t default_port = 8877;
const std::string default_ip = "0.0.0.0";
const int default_fd = -1;
const int fd_num_max = 64;  // pollfd数组最大长度(可自定义扩容)
const int non_event = 0;    // 无事件标记
class PollServer {
public:PollServer(uint16_t port = default_port, const std::string& ip = default_ip): port_(port), ip_(ip) {// 初始化pollfd数组(全部设为无效fd)for (int i = 0; i < fd_num_max; ++i) {event_fds_[i].fd = default_fd;event_fds_[i].events = non_event;event_fds_[i].revents = non_event;}}~PollServer() {listensock_.Close();  // 关闭监听socket}// 初始化服务器(创建、绑定、监听)bool Init() {listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();std::cout << "Server init success! Port: " << port_ << std::endl;return true;}// 启动服务器(核心循环)void Start() {// 将监听socket加入pollfd数组(下标0)int listen_fd = listensock_.Fd();event_fds_[0].fd = listen_fd;event_fds_[0].events = POLLIN;  // 监视监听socket的可读事件int timeout = 3000;  // 超时3秒while (true) {int ret = poll(event_fds_, fd_num_max, timeout);switch (ret) {case -1:  // 调用失败perror("poll error");break;case 0:   // 超时std::cout << "Poll timeout (3s)..." << std::endl;break;default:  // 有fd就绪std::cout << "Found " << ret << " ready fd(s)!" << std::endl;HandlerEvent();  // 处理就绪事件break;}}}
private:// 处理就绪事件(遍历pollfd数组)void HandlerEvent() {for (int i = 0; i < fd_num_max; ++i) {int fd = event_fds_[i].fd;if (fd == default_fd) continue;  // 跳过无效fd// 检查是否有可读事件(或异常事件)if (event_fds_[i].revents & (POLLIN | POLLERR | POLLHUP)) {if (fd == listensock_.Fd()) {// 监听socket就绪:接受新连接Accept();} else {// 通信socket就绪:读取数据Receiver(fd, i);}}}}// 接受新连接(将新socket加入pollfd数组)void Accept() {std::string client_ip;uint16_t client_port;int new_fd = listensock_.Accept(&client_ip, &client_port);if (new_fd < 0) return;// 找到pollfd数组中的空位int i = 1;  // 下标0留给监听socketfor (; i < fd_num_max; ++i) {if (event_fds_[i].fd == default_fd) break;}if (i == fd_num_max) {// 数组满:关闭新连接(可扩展为动态扩容)std::cout << "Server full! Close new connection (fd: " << new_fd << ")" << std::endl;close(new_fd);} else {// 加入数组:监视可读事件event_fds_[i].fd = new_fd;event_fds_[i].events = POLLIN;event_fds_[i].revents = non_event;std::cout << "New connection: " << client_ip << ":" << client_port << " (fd: " << new_fd << ")" << std::endl;PrintOnlineFds();  // 打印在线fd列表}}// 读取客户端数据(处理断开和错误)void Receiver(int fd, int idx) {char buf[1024] = {0};ssize_t n = read(fd, buf, sizeof(buf) - 1);if (n > 0) {// 读取成功:打印数据std::cout << "Received from fd " << fd << ": " << buf << std::endl;} else if (n == 0) {// 客户端主动断开连接std::cout << "Client fd " << fd << " disconnected" << std::endl;close(fd);event_fds_[idx].fd = default_fd;  // 标记为无效fdPrintOnlineFds();} else {// 读取错误(如连接重置)perror(("Read error on fd " + std::to_string(fd)).c_str());close(fd);event_fds_[idx].fd = default_fd;PrintOnlineFds();}}// 打印当前在线的文件描述符void PrintOnlineFds() {std::cout << "Online fds: ";for (int i = 0; i < fd_num_max; ++i) {if (event_fds_[i].fd != default_fd) {std::cout << event_fds_[i].fd << " ";}}std::cout << std::endl;}
private:uint16_t port_;                // 服务器端口std::string ip_;               // 服务器IP(默认0.0.0.0)Sock listensock_;              // 监听socketstruct pollfd event_fds_[fd_num_max];  // pollfd数组(管理所有待监视fd)
};
3.服务器的main函数:Main.cc
#include "PollServer.hpp"
#include   // 智能指针(自动管理内存)
int main() {// 用智能指针创建服务器对象(避免内存泄漏)std::unique_ptr server(new PollServer(8877));if (!server->Init()) {std::cerr << "Server init failed!" << std::endl;return 1;}// 启动服务器(进入核心循环)server->Start();return 0;
}
4. 编译脚本:Makefile
# 生成服务器可执行文件
poll_server: main.ccg++ -o $@ $^ -std=c++11 -Wall  # -Wall显示警告,增强代码健壮性
# 清理生成的文件
.PHONY: clean
clean:rm -rf poll_server

2.3 服务器测试与运行

  1. 编译运行

    bash

    make  # 编译生成poll_server
    ./poll_server  # 启动服务器
  2. 客户端连接(用telnetnc工具):

    bash

    telnet 127.0.0.1 8877  # 连接本地服务器
  3. 测试效果
    • 客户端输入数据,服务器会打印 “Received from fd XXX: 数据内容”;
    • 客户端断开连接,服务器会移除该 fd 并更新在线列表;
    • 3 秒无事件时,服务器打印 “Poll timeout (3s)...”。

三、poll 的优缺点分析

3.1 优点

  1. 突破 fd 数量限制pollfd数组大小由用户自定义(如示例中fd_num_max=64,可根据需求扩容),无select的 1024 上限;
  2. 输入输出分离events(输入)和revents(输出)分离,无需每次调用poll前重新初始化监视 fd,减少用户态开销;
  3. 异常事件自动返回:无需像select那样单独监视 “异常集合”,内核会在revents中自动标记POLLERR(错误)、POLLHUP(挂起)等异常,简化代码;
  4. fd 效率更高select需遍历到位图中的最大 fd,而poll仅遍历pollfd数组中的有效元素,fd 值较大时效率更优。

3.2 缺点

  1. 轮询机制效率低pollselect一样,需遍历所有监视的 fd 才能确定就绪状态,当 fd 数量庞大(如上万)时,遍历开销急剧增加,效率线性下降;
  2. 用户态需维护数组:需手动管理pollfd数组(如寻找空位、标记无效 fd),代码复杂度略高于select
  3. 数据拷贝开销每次调用poll时,pollfd数组需从用户态拷贝到内核态,fd 数量越多,拷贝开销越大;
  4. 无事件驱动机制无法像epoll那样 “主动通知” 就绪 fd,只能被动轮询,高并发场景下性能不足。

四、总结

pollselect的改进版,核心解决了 “fd 数量上限” 和 “参数耦合” 问题,但其轮询本质未变,仍适用于中低并发场景(如 fd 数量小于 1000)。

若需处理高并发(如上万级连接),需使用 Linux 特有的epoll技术 —— 通过 “事件驱动” 和 “内核维护就绪列表”,避免轮询和频繁数据拷贝,大幅提升效率。

理解poll的设计逻辑,不仅能掌握中并发场景的 I/O 多路复用方案,也为后续学习epoll的优势奠定基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/943218.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 应用多模块开发时,子模块只有 release buildType 时编译报错怎么办?

非常好的问题 👏,而且你这个需求其实是 最常见、最合理 的做法。 你完全正确 —— 如果你的模块(library modules)只需要 release(正式版)构建,而 app 有多个 buildType(比如 dev, prod 等),你 不需要 在每…

ipad协议对个人微信机器人进行二次开发

ipad协议对个人微信机器人进行二次开发、个微协议,微信号二次开发/ipad协议 优势 我们是一家专业服务企业数字化微信管理服务的技术团队,服务于需求SCRM、机器人、营销系统、社群小助手等具有研发能力的企业,同时我们…

西安交通大学国家级医学公关交叉平台实验室建设实拍图

在西安交通大学创新港,医学板块科研平台建设正以“加速度” 推进。 实验室现场,通风管道改造已完工,崭新的地面和实验台通风柜等实验室家具布局井然有序。

2025年10月智能门窗代理厂家全景解析报告,基于专业测评的技术、性能及市场优势深度分析

随着智能家居市场的快速发展,智能门窗行业迎来了快速增长期。据行业统计数据显示,2024年中国智能门窗市场规模预计突破800亿元,年复合增长率保持在18%以上。消费者对智能门窗的需求已从基础功能向智能化、安全性、节…

深入解析:【ROS2学习笔记】话题通信篇:话题通信再探

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Android插件化框架

https://chat.deepseek.com/share/w6duumv7aglpslahak Android插件化框架Shadow深度解析 1. 什么是Android插件化? 1.1 基本概念 Android插件化是一种技术,允许一个已安装的APP(称为宿主)动态加载并运行另一个完整…

完整教程:Python全栈(基础篇)——Day06:后端内容(定义函数+调用函数+实战演示+每日一题)

完整教程:Python全栈(基础篇)——Day06:后端内容(定义函数+调用函数+实战演示+每日一题)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !importa…

【IEEE出版、中国科学院宁波材料所主办】第五届机械自动化与电子信息工程国际学术会议(MAEIE 2025)

IEEE出版、会议规模大,EI检索 第五届机械自动化与电子信息工程国际学术会议(MAEIE 2025) 2025 5th International Conference on Mechanical Automation and Electronic Information Engineering 在这里看会议官网详…

氛围灯动态屏保取色方案二

氛围灯动态屏保取色方案二hue查找:整体偏差不会很大,但是对于亮度较高存在误差,精准度不够 lab查找:整体一般,但是精准度较好,不过算法复杂,增加耗时 hue色相查找存在误差,在有限的256色中,匹配的规则需要调整…

完整教程:Oracle/MySQL/SqlServer/PostgreSQL等数据库的数据类型映射以及各版本数据类型情况说明

完整教程:Oracle/MySQL/SqlServer/PostgreSQL等数据库的数据类型映射以及各版本数据类型情况说明2025-10-22 11:20 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !i…

老弟第一次学 Redis,被坑惨了!小白可懂的保姆级 Redis 教程

这天,产品经理找到你:阿巴阿巴,用户吐槽咱们网站首页加载太慢,快优化!你是小阿巴,刚入职的程序员。 这天,产品经理找到你:阿巴阿巴,用户吐槽咱们网站首页加载太慢,快优化! 你打开监控一看,好家伙!每秒有…

中小企业如何低成本部署电话呼叫软件网页版?一步步教你做

对于许多中小企业而言,电话沟通依然是销售、客服、售后等场景中最直接、最高效的方式。但当业务量增长、员工分布分散后,传统电话模式的问题也随之暴露:号码分散难管理、通话费用高、数据无法留存、外呼行为缺乏监管…

别再手写过滤器!SpringCloud Gateway 内置30 个,少写 80% 重复代码

大家好,我是小富~ 我发现公司的网关项目里有很多的轮子,几乎每个人接手这个项目开发,都会自定义过滤器,导致有非常非常多的过滤器,修改其中一个,指不定就会影响其他的人功能,非常的恼火。 其实在 Spring Cloud…

PCB库文档处理工具

PCB库文档处理工具封装库文档图元清理工具

完整教程:如何更改 SQLserver 数据库存储的位置 想从C盘换到D盘

完整教程:如何更改 SQLserver 数据库存储的位置 想从C盘换到D盘pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "C…

实用指南:Linux 如何创建和计数套接字

实用指南:Linux 如何创建和计数套接字pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Mon…

(web cad drawing)Web CAD SDK Integration Method

Preface We have created an online CAD project based on mxcad, which includes various CAD functions such as previewing, editing drawings, and operating the drawing database. After user integration, it s…

记一次 .NET 某药品缺陷高速检测系统 卡慢分析

一:背景 1. 讲故事 上个月有位朋友找到我,说他们公司的程序当内存达到一定阈值(2g+)之后,业务逻辑明显变慢导致下位机超时报警,想让我看下到底怎么回事,这种问题其实抓dump比较难搞,但朋友也说了有一个增长阈值,…

0254-CLAP-参数默认值

环境Time 2022-12-02 WSL-Ubuntu 22.04 CLAP 4.0.29前言 说明 参考:https://docs.rs/clap/latest/clap/index.html 目标 如果没有提供参数,使用默认值。 Cargo.toml [package] edition = "2021" name = &q…

得物火山引擎:Data Agent驱动财务管理智能升级

作为新一代品质生活购物社区,得物 App 以正品电商和品质生活社区作为两大核心服务,成立十年来,得物 App 始终致力于帮助用户得到美好生活,90后用户占比超9成,已成为年轻用户重要的品质生活购物平台。随着 AI 时代…