高级IO-poll
目录
一、为什么需要 poll?select 的痛点回顾
二、poll 核心原理与数据结构
1. 核心结构:struct pollfd
2. poll 函数原型
三、实战:基于 poll 实现 TCP 服务器
1. 类结构与初始化
2. 处理新连接:Accepter 方法
3. 接收客户端数据:Recver 方法
4. 事件分发:Dispatcher 方法
5. 启动服务器:Start 方法
四、poll 对比 select:优势与局限
优势:
局限:
五、总结:poll 适合什么场景?
在网络编程中,多路复用技术是处理并发连接的基石。上一篇我们探讨了 select 的实现,但其固有的 fd 数量限制和重复初始化问题始终是瓶颈。本文将聚焦 poll 机制 —— 它作为 select 的改进版,解决了不少痛点。我们将通过一个完整的 poll 服务器实现,深入理解其工作原理与优势。
一、为什么需要 poll?select 的痛点回顾
select 作为早期的多路复用方案,存在三个明显缺陷:
- fd 数量上限:由
fd_set位图长度决定(通常默认 1024),无法灵活扩展。 - 输入输出参数混合:每次调用
select都需重新初始化fd_set,重复劳动且效率低。 - 遍历成本高:用户态和内核态都需遍历全部监控 fd 才能确定就绪事件。
poll 的出现正是为了针对性解决这些问题,尤其是前两点。
二、poll 核心原理与数据结构
1. 核心结构:struct pollfd
poll 不再使用位图,而是通过一个结构体数组管理文件描述符(fd)和事件,结构体定义如下:
struct pollfd {int fd; // 待监控的文件描述符short events; // 输入:用户关心的事件(如 POLLIN 表示可读)short revents; // 输出:内核返回的实际就绪事件
};
- 分离输入输出:
events仅用于设置监控需求,revents用于返回结果,无需每次重置。 - 事件类型清晰:支持
POLLIN(可读)、POLLOUT(可写)、POLLERR(错误)等事件,与select功能类似但表达更直接。 
2. poll 函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:struct pollfd数组,存放待监控的 fd 及事件。nfds:数组长度(需监控的 fd 数量)。timeout:超时时间(毫秒):timeout > 0:阻塞等待指定毫秒数。timeout = 0:非阻塞,立即返回。timeout = -1:无限期阻塞,直到有事件就绪。
- 返回值:就绪事件的总数(失败返回 -1,超时返回 0)。
三、实战:基于 poll 实现 TCP 服务器
下面通过代码实现一个完整的 poll 服务器,感受其与 select 的差异。
1. 类结构与初始化
#pragma once
#include <iostream>
#include <poll.h>
#include "Socket.hpp" // 自定义套接字封装类using namespace std;static const uint16_t defaultport = 8888;
static const int fd_num_max = 64; // 可自定义的fd上限(比select更灵活)
int defaultfd = -1; // 标记未使用的fd
int non_event = 0; // 无事件标记class PollServer {
public:PollServer(uint16_t port = defaultport) : _port(port) {// 初始化pollfd数组:所有fd设为-1(未使用),事件设为0for (int i = 0; i < fd_num_max; i++) {_event_fds[i].fd = defaultfd;_event_fds[i].events = non_event;_event_fds[i].revents = non_event;}}bool Init() {// 初始化监听套接字:创建、绑定、监听_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();return true;}// ... 其他方法后续展开
private:Sock _listensock; // 监听套接字uint16_t _port; // 服务器端口struct pollfd _event_fds[fd_num_max]; // pollfd数组,管理所有监控的fd
};
- 与
select不同,poll直接用struct pollfd数组管理 fd,无需单独维护 fd 列表,结构更紧凑。
2. 处理新连接:Accepter 方法
当监听套接字的 POLLIN 事件就绪时,接收新连接并将客户端 fd 加入 _event_fds 数组。
void Accepter() {std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport); // 非阻塞,因poll已通知就绪if (sock < 0) return;lg(Info, "accept success, %s:%d, sock fd:%d", clientip.c_str(), clientport, sock);// 找一个空闲位置存储新客户端fdint pos = 1; // 位置0留给监听套接字for (; pos < fd_num_max; pos++) {if (_event_fds[pos].fd != defaultfd) continue;else break;}if (pos == fd_num_max) { // 服务器fd已满lg(Warning, "server is full, close %d now!", sock);close(sock);} else { // 加入监控,关注可读事件_event_fds[pos].fd = sock;_event_fds[pos].events = POLLIN; // 仅设置一次,无需每次重置_event_fds[pos].revents = non_event;PrintFd(); // 打印当前在线fd}
}
- 关键差异:
events只需初始化时设置一次,后续poll调用会复用,无需像select那样每次清空重设。
3. 接收客户端数据:Recver 方法
当客户端 fd 的 POLLIN 事件就绪时,读取数据并处理连接关闭 / 错误场景。
void Recver(int fd, int pos) {char buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1);if (n > 0) { // 读取成功buffer[n] = 0;cout << "get a message: " << buffer << endl;} else if (n == 0) { // 客户端断开lg(Info, "client quit, close fd: %d", fd);close(fd);_event_fds[pos].fd = defaultfd; // 标记为未使用(从监控中移除)} else { // 读取错误lg(Warning, "recv error, fd: %d", fd);close(fd);_event_fds[pos].fd = defaultfd;}
}
4. 事件分发:Dispatcher 方法
遍历 _event_fds 数组,通过 revents 检查就绪事件,分发给对应处理函数。
void Dispatcher() {for (int i = 0; i < fd_num_max; i++) {int fd = _event_fds[i].fd;if (fd == defaultfd) continue;// 检查内核返回的可读事件if (_event_fds[i].revents & POLLIN) {if (fd == _listensock.Fd()) { // 监听套接字:新连接Accepter();} else { // 客户端套接字:数据可读Recver(fd, i);}}}
}
- 与
select相比,poll通过revents直接返回就绪事件,无需调用FD_ISSET宏,代码更直观。
5. 启动服务器:Start 方法
主循环中调用 poll 监控事件,就绪后通过 Dispatcher 处理。
void Start() {// 监听套接字加入监控,关注可读事件(新连接)_event_fds[0].fd = _listensock.Fd();_event_fds[0].events = POLLIN;int timeout = 3000; // 超时时间3秒for (;;) {// 调用poll监控事件,无需每次重置eventsint n = poll(_event_fds, fd_num_max, timeout);switch (n) {case 0: // 超时cout << "time out... " << endl;break;case -1: // 错误cerr << "poll error" << endl;break;default: // 有事件就绪cout << "get a new event!!!!!" << endl;Dispatcher();break;}}
}
- 核心优势:
poll调用时无需重新初始化_event_fds数组,events字段保持不变,减少重复操作。
四、poll 对比 select:优势与局限
优势:
- 突破 fd 数量上限:
select依赖fd_set位图长度,而poll的 fd 数量由数组大小决定(可自定义,理论上仅受系统最大 fd 限制)。 - 输入输出分离:
events(输入)和revents(输出)分离,无需每次重置事件集,减少代码冗余。 - 无需计算 maxfd:
select需要传入最大 fd + 1,poll直接传入数组长度,更简洁。
局限:
- 遍历开销仍存在:与
select一样,poll返回后仍需遍历整个数组才能找到就绪的 fd,当 fd 数量庞大时效率下降。 - 数据拷贝开销:每次调用
poll仍需将整个pollfd数组拷贝到内核空间,fd 越多拷贝成本越高。
完整代码:
#pragma once#include <iostream>
#include <poll.h>
#include <sys/time.h>
#include "Socket.hpp"using namespace std;static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;
int defaultfd = -1;
int non_event = 0;class PollServer
{
public:PollServer(uint16_t port = defaultport) : _port(port){for (int i = 0; i < fd_num_max; i++){_event_fds[i].fd = defaultfd;_event_fds[i].events = non_event;_event_fds[i].revents = non_event;// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;}}bool Init(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();return true;}void Accepter(){// 我们的连接事件就绪了std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会if (sock < 0) return;lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);// sock -> fd_array[]int pos = 1;for (; pos < fd_num_max; pos++) // 第二个循环{if (_event_fds[pos].fd != defaultfd)continue;elsebreak;}if (pos == fd_num_max){lg(Warning, "server is full, close %d now!", sock);close(sock);// 扩容}else{// fd_array[pos] = sock;_event_fds[pos].fd = sock;_event_fds[pos].events = POLLIN;_event_fds[pos].revents = non_event;PrintFd();// TODO}}void Recver(int fd, int pos){// demochar buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?if (n > 0){buffer[n] = 0;cout << "get a messge: " << buffer << endl;}else if (n == 0){lg(Info, "client quit, me too, close fd is : %d", fd);close(fd);_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除}else{lg(Warning, "recv error: fd is : %d", fd);close(fd);_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除}}void Dispatcher(){for (int i = 0; i < fd_num_max; i++) // 这是第三个循环{int fd = _event_fds[i].fd;if (fd == defaultfd)continue;if (_event_fds[i].revents & POLLIN){if (fd == _listensock.Fd()){Accepter(); // 连接管理器}else // non listenfd{Recver(fd, i);}}}}void Start(){_event_fds[0].fd = _listensock.Fd();_event_fds[0].events = POLLIN;int timeout = 3000; // 3sfor (;;){int n = poll(_event_fds, fd_num_max, timeout);switch (n){case 0:cout << "time out... " << endl;break;case -1:cerr << "poll error" << endl;break;default:// 有事件就绪了,TODOcout << "get a new link!!!!!" << endl;Dispatcher(); // 就绪的事件和fd你怎么知道只有一个呢???break;}}}void PrintFd(){cout << "online fd list: ";for (int i = 0; i < fd_num_max; i++){if (_event_fds[i].fd == defaultfd)continue;cout << _event_fds[i].fd << " ";}cout << endl;}~PollServer(){_listensock.Close();}private:Sock _listensock;uint16_t _port;struct pollfd _event_fds[fd_num_max]; // 数组, 用户维护的!// struct pollfd *_event_fds;// int fd_array[fd_num_max];// int wfd_array[fd_num_max];
};
五、总结:poll 适合什么场景?
poll 是 select 的优化版本,解决了 fd 数量限制和事件集重复初始化问题,在中小并发场景(如 fd 数量 1000-10000)中表现优于 select。但它并未彻底解决遍历和数据拷贝的开销,因此在高并发(如十万级连接)场景中,仍需依赖 epoll(Linux)或 kqueue(BSD)等更高效的机制。
理解 poll 的设计思路 —— 通过结构体数组分离输入输出、灵活扩展 fd 数量 —— 是掌握多路复用技术演进的关键一步。下一篇我们将探讨 epoll,看看它如何进一步突破 poll 的瓶颈。
