文章目录
- [1. poll的作用和定位](#1. poll的作用和定位)
- [2. 使用poll改写select服务器](#2. 使用poll改写select服务器)
- [3. poll的优点和缺点](#3. poll的优点和缺点)
1. poll的作用和定位
poll 是一种 I/O 多路复用机制,用于同时监控多个文件描述符(fd)的状态变化。它允许程序在单个系统调用中等待多个 fd 上的事件(如可读、可写、异常等),一旦某个 fd 上的事件发生,poll 就会返回并通知应用程序。
其核心思想是"只负责等,事件就绪即通知",这使得它非常适合处理大量并发连接的服务器程序。
它是 select函数的改进和替代方案,属于同一类技术,旨在解决单个进程/线程高效管理多个I/O流的问题。
cpp
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
fds:指向一个 struct pollfd结构体数组的指针,用于描述多个文件描述符及其关注的事件。
-
nfds:指定 fds数组的长度(即要监视的文件描述符个数)。
-
timeout:超时时间(毫秒)。
-
-1:阻塞等待,直到有事件发生。
-
0:非阻塞,立即返回,用于轮询。
-
> 0:等待指定毫秒数,超时后返回 0。
-
struct pollfd 结构体定义:
cpp
struct pollfd {
int fd; /* 要监视的文件描述符 */
short events; /* 用户关心的事件(输入参数) */
short revents; /* 实际发生的事件(输出参数) */
};
events和revents的取值:


工作流程:
- 调用前:用户填充 fds数组。
-
对每个关心的 fd,设置其 events字段(例如 POLLIN表示关心可读事件)。
-
此时,fd和 events有效。这是"用户告诉内核":请帮我监视这个 fd上的这些事件。
-
调用 poll:内核开始监视,进程进入等待。
-
调用返回后:内核填充 revents字段。
-
检查每个 fd的 revents字段。如果某位被置位(如 revents & POLLIN为真),表示对应事件已就绪。
-
此时,fd和 revents有效。这是"内核告诉用户":你让我监视的 fd上,这些事件已经就绪了,可以处理了!
poll 相比 select 解决了哪些问题?
(1)解决了参数重置的问题
在 select 中,fd_set 是一个位掩码结构,每次调用 select 前都需要重新初始化(如 FD_ZERO, FD_SET),因为 select 会修改该集合。而 poll 的 struct pollfd 中,events 和 revents 分离:
- events:用户设置关心的事件(输入)。
- revents:内核返回实际发生的事件(输出)。
✅ 优势:调用 poll 后,events 不会被修改,因此可以重复使用同一个 struct pollfd 数组,无需每次重置。
(2)解决了文件描述符数量上限的问题
select 使用固定大小的 fd_set,受限于 FD_SETSIZE(通常是 1024 位),因此最多只能监控 1024 个 fd。而 poll 使用动态数组 struct pollfd *fds,理论上没有上限(受限于系统资源)。
✅ 优势:适用于高并发场景,支持更多并发连接。
注意:当 fd == -1 时,表示该 struct pollfd 条目无效,内核将忽略其 events 字段,不会对该 fd 进行任何监控。这是 poll 提供的一种灵活机制,允许动态管理监控列表。
2. 使用poll改写select服务器
这里我们就不需要采用辅助数组来存储了,直接使用struct pollfd类型的指针,不过我们这里简单一点直接写一个静态数组。逻辑上代码不需要改变,只需要改变为poll的用法。
代码如下:
cpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/poll.h>
#include <unistd.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class PollServer
{
const static int size = 4096;
const static int defaultfd = -1;
public:
PollServer(int port):_listensock(std::make_unique<TcpSocket>()), _isrunning(false)
{
_listensock->BuildTcpSocketMethod(port);
for(int i = 0; i < size; i++)
{
_fds[i].fd = defaultfd;
_fds[i].events = 0;
_fds[i].revents = 0;
}
_fds[0].fd = _listensock->Fd(); // 默认将 listensockfd 添加到 _fds 开头
_fds[0].events = POLLIN;
}
void Start()
{
int timeout = -1;
_isrunning = true;
while(_isrunning)
{
PrintFd();
int n = poll(_fds, size, timeout);
switch (n)
{
case -1:
LOG(LogLevel::ERROR) << "poll error ...";
break;
case 0:
LOG(LogLevel::INFO) << "time out ...";
break;
default:
// 有事件就绪, 就不仅仅是新连接到来, 还有可能是读事件就绪
LOG(LogLevel::DEBUG) << "有事件就绪了..., 事件个数n : " << n;
Dispatcher(); // 处理就绪的事件
break;
}
}
_isrunning = false;
}
// 事件派发器
void Dispatcher()
{
// 此时读事件集合rfds被OS修改为哪些文件描述符已经就绪
// 有事件就绪, 不仅仅是新连接到来, 还有可能是读事件就绪
// 指定的文件描述符,在rfds里面,就证明该fd就绪了
for(int i = 0; i < size; i++)
{
if(_fds[i].fd == defaultfd)
continue;
// fd合法,但不一定就绪
if(_fds[i].revents & POLLIN)
{
// 此时事件就绪,是新连接到来,还是读事件就绪?
if(_fds[i].fd == _listensock->Fd())
{
// 如果是监听套接字就绪,那就是新连接到来
Accepter();
}
else
{
// 读事件就绪
Recver(i);
}
}
}
}
// 连接管理器
void Accepter()
{
// 新连接到来,我们需要accept接受新连接
InetAddr client;
int sockfd = _listensock->Accept(&client);
if(sockfd >= 0)
{
// 获取新链接到来成功, 然后呢??能不能直接read/recv()
// 当然不行,sockfd是否读就绪,我们不清楚
// 只有谁最清楚,未来sockfd上是否有事件就绪?肯定是poll!
// 所以我们需要将新的sockfd,托管给poll!
LOG(LogLevel::INFO) << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr();
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;
}
}
}
// IO处理器
void Recver(int pos)
{
// 处理 sockfd 读事件
// 我们在这里读取的时候,就不会阻塞了 --- 因为 poll 已经完成等操作了!
char buffer[1024];
ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer)-1, 0);
// recv 读的时候会有bug!因为无法保证能够读取到一个完整的请求!--- TCP 是流式协议!
// 我们目前先不做处理,等到 epoll 的时候,再做处理!
if(n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
}
else if(n == 0)
{
LOG(LogLevel::INFO) << "client quit ...";
// 此时不再需要让poll帮我们再关心fd
// 关闭fd
close(_fds[pos].fd);
_fds[pos].fd = defaultfd;
_fds[pos].events = 0;
_fds[pos].revents = 0;
}
else
{
LOG(LogLevel::ERROR) << "recv error ...";
// 此时不再需要让poll帮我们再关心fd
// 关闭fd
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";
}
~PollServer() {}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
struct pollfd _fds[size];
// struct pollfd *_fds;
};
运行测试:

3. poll的优点和缺点
poll 的优点
- 接口设计更清晰、易用
- select 使用三个 fd_set 位图(readfds, writefds, exceptfds),需要手动调用 FD_SET/FD_CLR/FD_ISSET 等宏操作,容易出错。
- poll 使用 struct pollfd 数组,每个元素独立描述一个 fd 及其关注的事件,结构化更强,语义更明确。
- 事件请求(events)和返回结果(revents)分离,避免了每次调用前重置参数。
- 无文件描述符数量上限
- select 受限于 fd_set 的固定大小(通常是 1024),无法监控更多 fd。
- poll 使用动态数组 struct pollfd *fds,理论上没有数量限制(受限于系统内存和最大文件描述符数)。
- 支持更多事件类型
- poll 支持如 POLLERR, POLLHUP, POLLNVAL 等更丰富的事件类型,而 select 对这些事件的支持较弱或不直接提供。
poll 的缺点
- 每次调用都需要拷贝整个 pollfd 数组
- poll 每次系统调用时,都需要将用户态的 struct pollfd 数组拷贝到内核态。
- 当监听的 fd 数量很大时,拷贝开销显著增加,影响性能。
- 返回后仍需轮询查找就绪 fd
- 和 select 类似,poll 返回后,用户程序必须遍历所有 pollfd 元素,检查 revents 字段,找出哪些 fd 就绪。
- 这种"线性扫描"方式在 fd 数量巨大时效率低下。
- 性能随 fd 数量线性下降
- 即使只有少数 fd 就绪,内核仍需遍历所有 fd 来检查状态。
- 在高并发场景下(如成千上万连接),效率会明显下降,不适合大规模并发服务器。
- 不支持 I/O 多路复用的"边缘触发"模式
- poll 默认是"水平触发"(Level Triggered),即只要条件满足就会通知,即使上次未处理完。
- 虽然可以通过设置非阻塞 IO + 边缘触发来模拟,但不如 epoll 原生支持高效。
为什么 poll 不适合大规模并发?
虽然 poll 解决了 select 的 fd 数量限制问题,但其每次调用都需拷贝所有 pollfd 结构体,且返回后仍需用户态轮询,导致:
- 上下文切换和内存拷贝开销大
- 随着 fd 数量增加,性能线性下降
因此,在现代高性能网络服务器中,epoll 成为首选,因为它:
- 使用红黑树管理 fd,查询效率 O(log n)
- 通过 epoll_wait 只返回就绪 fd,无需轮询
- 事件注册后,内核维护状态,减少用户态-内核态数据拷贝