参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/128371848
一、poll 介绍
poll 是 Unix/Linux 系统中用于实现多路 IO 复用的一种机制,主要用于监控多个文件描述符(File Descriptor, FD)的状态变化(如可读、可写、异常等)。它允许程序在单个线程中同时处理多个客户端连接,避免了为每个连接创建单独线程的资源开销,是高性能网络服务器的基础组件之一。
二、poll 数据结构
poll 基于 pollfd
结构体来管理文件描述符,其定义如下:
c
struct pollfd {
int fd; // 要监控的文件描述符
short events; // 期望监控的事件(输入事件)
short revents; // 实际发生的事件(输出事件,由内核填充)
};
- events 可设置的事件(常用) :
POLLIN
:数据可读(包括普通数据和带外数据)POLLOUT
:数据可写POLLERR
:发生错误POLLHUP
:连接挂断POLLNVAL
:文件描述符无效
- revents 返回的事件 :是
events
中实际发生的事件的位掩码组合。

我们主要关注表格中标红的事件即可;
注:
- events成员就是用户告诉内核,需要关心哪些文件描述符上的哪些事件。
- revents成员就是右内核告诉用户,你让我关心的这些文件描述上的事件,有哪些文件描述符已经就绪了。
三、poll 函数原型与参数解析
c
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
- 参数说明 :
fds
:pollfd
结构体数组,存储需要监控的文件描述符及事件nfds
:数组中有效元素的数量(需小于FD_SETSIZE
,通常为 1024)timeout
:超时时间(毫秒):-1
:永久阻塞,直到有事件发生0
:立即返回,不阻塞>0
:等待指定毫秒数,超时返回 0
- 返回值 :
>0
:发生事件的文件描述符数量0
:超时,无事件发生-1
:错误(如EINTR
被信号中断,或ENOMEM
内存不足)
四、poll的基本工作流程
我们要实现一个简单的poll服务器,该服务器要做的就是读取客户端发来的数据并进行打印,那么这个poll服务器的工作流程应该是这样的:
-
首先完成基本的套接字创建、绑定和监听。
-
poll的第一个参数是一个数组结构,里面包含了文件描述符、需要关心的事件和实际发送的事件。poll实现的服务器就不需要再利用额外的数组,只需要定义出struct pollfd fd_array数组,每个数组元素都对应着一个文件描述和所关心的事件,只需要内核检测到对应的文件描述符上的事件是否就绪并给予填充。
-
调用poll函数之前,我们需要将监听套接字设置到这个struct pollfd fd_array数组。因为监听套接字的读事件就绪就是有新的连接到来,所有是我们要关心的文件描述符。
-
紧接着不断的事件循环,与select不同的是,poll不需要每次重新设置文件描述符和相应的事件,只要在最初设置好后,以后就会一直帮我们关心相应的文件描述符和事件。因为注册事件和实际事件是分开的。
-
当调用poll函数时,poll检测到某些文件描述符有读事件就绪后,会将其设置到对应文件描述的结构体中的第三个成员中。已告知用户,就可以执行相应的读操作了。
-
如果读事件就绪是监听套接字,则调用accept函数从底层全连接队列中获取已经建立连接好的连接,并将这些连接设置到struct pollfd fd_array数组中,设置好你所需要关心的事件,偏于再次调用poll函数时,关心这些连接上的对应事件。
-
如果读事件就绪是普通的套接字(即建立好连接的哪些套接字),则调用read函数读取客户端发来的数据并进行打印输出。
-
当然读事件就绪也可能是因为客户端关闭了连接,此时服务器应该调用close关闭该套接字,并将该套接字从struct pollfd fd_array数组中清除,因为下一次不需要再关心该文件描述符了。
五、基于poll 的服务器设计
5.1 代码实现
下面是封装的socket
接口
cpp
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <cstring>
#include <netinet/in.h>
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(1);
}
return sock;
}
static void Bind(int sock, uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cout << "bind error !" << std::endl;
exit(2);
}
}
static void Listen(int sock)
{
if (listen(sock, 5) < 0)
{
std::cerr << "listen error" << std::endl;
exit(3);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr *)&peer, &len);
if (fd >= 0)
{
return fd;
}
else
{
return -1;
}
}
static void Connect(int sock, std::string ip, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
if (connect(sock, (struct sockaddr *)&server, sizeof(server) == 0))
{
std::cout << "connect sucess" << std::endl;
}
else
{
std::cout << "connect failed" << std::endl;
exit(4);
}
}
};
完整的poll
服务器代码
cpp
#include<poll.h>
#include<string>
#include"sock.hpp"
using namespace std;
#define NUM 128
struct pollfd fd_array[NUM]; //pollfd数组
int listen_sock = 0;
void handle(int index){
if(fd_array[index].revents & POLLIN){
std::cout << "sock:" << fd_array[index].fd << " is ready to read !" << std::endl;
if(fd_array[index].fd == listen_sock){
std::cout << "listen_sock:" << listen_sock << "has a new connection !" << std::endl;
int sock = Sock::Accept(fd_array[index].fd);
if(sock >= 0){
std::cout << "get a new connection:" << sock << std::endl;
int pos = 1;
for(pos ; pos < NUM ; ++ pos){
if(fd_array[pos].fd == -1){
break;
}
}
if(pos < NUM){
fd_array[pos].fd = sock;
fd_array[pos].events = 0;
fd_array[pos].revents = 0;
std::cout << "new connection has been added to fd_array[" << pos << "]" << std::endl;
}
else{
std::cout << "server has been full!" << std::endl;
close(sock);
}
}
}
else{
std::cout << "sock:" << fd_array[index].fd << " has s read event !" << std::endl;
char buffer[1024];
memset(buffer,0,sizeof(buffer));
ssize_t len = recv(fd_array[index].fd,buffer,sizeof(buffer),0);
if(len < 0){
std::cout << "recv failed !" << std::endl;
exit(2);
}
else if(len == 0){
close(fd_array[index].fd);
fd_array[index].fd = -1;
fd_array[index].events = 0;
fd_array[index].revents = 0;
std::cout << "fd_array[" << index <<"] has been set -1" << std::endl;
std::cout << "connection has been closed !" << std::endl;
}
else{
buffer[len] = '\0';
std::cout << "sock:" << fd_array[index].fd << "send:" << buffer << std::endl;
}
}
}
}
int main(int argc , char* argv[]){
if(argc < 2){
std::cerr << "argc < 2" << std::endl;
return 1;
}
uint16_t port = (uint16_t)atoi(argv[1]);
listen_sock = Sock::Socket();
Sock::Bind(listen_sock,port);
Sock::Listen(listen_sock);
fd_array[0].fd = listen_sock;
for(int i = 0 ; i < NUM ; ++i){
fd_array[i].fd = -1;
fd_array[i].events = 0;
fd_array[i].revents = 0; //由内核填充
}
std::cout << "server is listening on:" << port << std::endl;
while(true){
int timeout = -1;
int ret = poll(fd_array,NUM,timeout);
if(ret == -1){
std::cerr << "poll error !" << std::endl;
break;
}
else if(ret == 0){
std::cerr << "poll timeout !" << std::endl;
continue;
}
else{
for(int i = 0 ; i < NUM ; ++i){
handle(i);
}
}
}
for(int i = 0 ; i < NUM ; ++i){
if(fd_array[i].fd != -1){
close(fd_array[i].fd);
fd_array[i].fd = -1;
fd_array[i].events = 0;
fd_array[i].revents = 0;
}
}
return 0;
}
编写Makefile
进行编译
shell
CXX = g++
CXXFLAGS = -Wall -std=c++14
SRCS = main.cpp
OBJS = $(SRCS:.cpp=.o)
TARGET = server
all:$(TARGET)
$(TARGET):$(OBJS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)
%.o:%.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY:all clean

5.2 运行结果
客户端连接服务器

客户端发送信息到服务端
客户端关闭

六、总结
6.1 poll的优缺点
优点:
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
- pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式。接口使用比select更方便。
- poll并没有最大数量限制 (但是数量过大后性能也是会下降)。
缺点:
poll中监听的文件描述符数目增多时
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降。
6.2 poll与select对比
特性 | select | poll |
---|---|---|
数据结构 | fd_set (固定大小数组) |
pollfd 结构体数组 |
最大连接数 | 受限于 FD_SETSIZE (通常 1024) |
理论上无固定限制(受系统资源限制) |
事件管理 | 基于位掩码,需手动重置 | 结构体数组,事件独立设置 |
性能 | 线性扫描所有 fd,O (n) | 线性扫描所有 fd,O (n) |
移植性 | 跨平台(Unix/Linux/Windows) | 主要用于 Unix/Linux |
总结 :poll 相比 select 解决了 fd_set
大小固定的问题,但两者在高并发场景下仍存在共同缺陷:每次调用都需遍历所有监控的 fd,当连接数较多而活跃连接较少时,性能会大幅下降