poll 是 Linux 中一种多路 IO 复用的系统调用,用于同时监听多个文件描述符(fd)的就绪事件,是对 select 的改进方案
1. poll
核心接口与参数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
| 参数 | 含义 |
|---|---|
| fds | 指向struct pollfd 数组的起始地址,数组中每个元素代表一个待监听的文件描述符及事件 |
| nfds | 数组中元素的个数(待监听的 fd 总数) |
| timeout | 超时时间,单位毫秒 |
timeout 取值:
- timeout > 0:阻塞等待,最多等待 timeout 毫秒
- timeout = 0:非阻塞模式
- timeout = -1:永久阻塞
struct pollfd 结构:
struct pollfd {
int fd; /* 待监听的文件描述符 */
short events; /* 用户告诉内核:要监听的事件(输入参数)位图 */
short revents; /* 内核告诉用户:已就绪的事件(输出参数)位图 */
};
events和revents是位图操作,要熟练运用|和&
- events |= POLLIN:我要监听这个可读事件(用户吩咐内核追加事件)
- revents & POLLIN:这个可读事件来了吗(内核答复用户,判断就绪)
核心事件类型(常用):
其中POLLIN和POLLOUT最常用
| 事件 | 含义 |
|---|---|
| POLLIN | 数据可读(包括普通数据、优先数据) |
| POLLOUT | 数据可写(包括普通数据、优先数据) |
| POLLERR | 发生错误 |
| POLLHUP | 连接被挂断(对方关闭连接) |
| PULLNVAL | 文件描述符无效(未打开) |
2. poll 对 select 的改进
- 参数分离,无需重置:select的 fd_set 是输入输出复用的,每次调用前都要重新设置;poll 将用户关心的事件(events)和内核返回的就绪事件(revents)分离,调用前后无需重置参数。
- 无最大 fd 数量限制:select 受限于FD_SETSIZE(默认 1024),而 poll 没有硬上限,理论上可监听任意数量的文件描述符。
- 支持更多事件类型:提供了更细粒度的事件标识(如
POLLPRI、POLLRDHUP等),可满足不同场景的需求。
3. poll 的优缺点
优点
- 接口更清晰,事件读写分离,使用更便捷。
- 突破了 select 的 fd 数量上限,适合高并发场景
缺点
- 用户态到内核态拷贝开销大:每次调用 poll,都需要将整个
pollfd数组从用户态拷贝到内核态,fd 数量越多,开销越大。 - 仍需遍历所有 fd:poll 返回后,用户需要遍历整个
pollfd数组,检查每个 fd 的revents是否就绪,时间复杂度为 O(n)。 - 大量连接下效率下降:当同时连接的客户端很多,但同一时间只有少量 fd 就绪时,遍历整个数组的成本很高,效率会随监听数量增加线性下降
4. poll代码改写select
#include <iostream>
#include <memory>
#include <sys/select.h>
#include <poll.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class PollServer
{
public:
const static int size = 4096;
const static int defaultfd = -1;
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();
_fds[0].events = POLLIN;
}
void Start()
{
_isrunning = true;
int timeout = -1;
while (_isrunning)
{
PrintFd();
int n = poll(_fds, size, timeout);
if (n == -1)
{
// 出错了
LOG(LogLevel::ERROR) << "poll error";
continue;
}
else if (n == 0)
{
// 超时了
LOG(LogLevel::INFO) << "poll time out...";
continue;
}
else
{
// 成功:有文件描述符就绪
LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
Dispatcher(); // 处理就绪的事件
continue;
}
}
_isrunning = false;
}
void Dispatcher()
{
for (int i = 0; i < size; i++)
{
if (_fds[i].fd == defaultfd)
{
// 空的
continue;
}
if (_fds[i].revents & POLLIN) // 检查这个fd是否已经读就绪了
{
if (_fds[i].fd == _listensock->Fd())
{
// 说明是新来的连接
Accepter();
}
else
{
// 派发任务
Recver(i);
}
}
}
}
void Accepter()
{
InetAddr client;
int sockfd = _listensock->Accept(&client);
if (sockfd < 0)
{
LOG(LogLevel::ERROR) << "accept error";
}
else
{
// accept到新连接,把新fd交给select
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) << "select server full";
close(sockfd);
}
else
{
_fds[pos].fd=sockfd;
_fds[pos].events |= POLLIN;
_fds[pos].revents=0;
}
}
}
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) << "client quit...";
// 2. 关闭fd
close(_fds[pos].fd);
// 1. 把位图中fd不要就绪了
_fds[pos].fd = defaultfd;
_fds[pos].events = 0;
_fds[pos].revents = 0;
}
else
{
LOG(LogLevel::ERROR) << "recv error";
// 2. 关闭fd
close(_fds[pos].fd);
// 1. 把位图中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:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
struct pollfd _fds[size];
};


被折叠的 条评论
为什么被折叠?



