poll是对select的改进。select的缺点如下:
1 打开的文件描述符fd有限制:为1024
2 有读事件集合,写事件集合和异常事件集合。
3 每一次都要重新设置好需要关心的事件
目录
[一. poll系统调用](#一. poll系统调用)
[1.1 poll函数](#1.1 poll函数)
[1.2 struct pollfd结构体](#1.2 struct pollfd结构体)
[二. poll TCP服务器实现](#二. poll TCP服务器实现)
[2.1 run函数](#2.1 run函数)
[2.2 pollTcpSercer.hpp](#2.2 pollTcpSercer.hpp)
[2.3 pollTcpserver.cc](#2.3 pollTcpserver.cc)
[三. 总结和测试](#三. 总结和测试)
[3.1 总结](#3.1 总结)
[3.2 测试](#3.2 测试)
[3.3 性能测试对比表](#3.3 性能测试对比表)
一. poll系统调用
1.1 poll函数
#include <poll.h>
//poll:关心事件就绪后就会返回
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
//fds:表示需要关心的事件的集合
//nfds:表示需要监控事件的数量
//timeout:超时时间
返回值:返回事件大于0,说明有返回事件,=0说明超时了,小于0说明发送错误
1.2 struct pollfd结构体
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
包含了就绪事件的fd和该事件的关心方式events,以及返回结果revents
二. poll TCP服务器实现
首先拿出我们上次的select服务器代码,然后删除select的逻辑。其他部分都是可用的
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <cstring>
#include <functional>
#include <thread>
namespace YZC
{
// 设置默认端口和最大backlog
const int defaultPort = 8080;
const int maxBacklog = 128;
// 设置回调函数
using func_t = std::function<int(int)>;
class tcpServer
{
public:
tcpServer(func_t func, int port = defaultPort)
: _port(port), _callback(func) {}
void init()
{
// 1.创建socket
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "sockte err" << std::endl;
exit(-1);
}
std::cout << "socket success" << std::endl;
// 2 bind绑定fd和端口
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
// 设置地址的信息(协议,ip,端口)
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址
serveraddr.sin_port = htons(_port); // 注意端口16位,2字节需要使用htons。不可
socklen_t len = sizeof(serveraddr);
if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0)
{
std::cerr << "bind err" << std::endl;
exit(-1);
}
std::cout << "bind success" << std::endl;
// 3 设置sockfd为监听fd
if (listen(_listensock, maxBacklog) < 0)
{
std::cerr << "listen err" << std::endl;
exit(-1);
}
std::cout << "listen success" << std::endl;
}
void run()
{
//poll逻辑
}
private:
int _listensock;
int _port;
func_t _callback;
};
int serviceIO(int sockfd)
{
// 这里仅做简单的数据收发
char buffer[128] = {0};
int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (count < 0)
{
std::cerr << "recv err" << std::endl;
exit(-1);
}
if (count == 0)
{
// 对方关闭
return 0;
close(sockfd);
}
printf("client --> server:%s\n", buffer);
send(sockfd, buffer, strlen(buffer), 0);
return count;
}
int serviceHTTP(int sockfd)
{
// 这里仅做简单的数据收发
char buffer[128] = {0};
int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (count < 0)
{
std::cerr << "recv err" << std::endl;
exit(-1);
}
if (count == 0)
{
// 对方关闭
return 0;
close(sockfd);
}
printf("client --> server:%s\n", buffer);
std::string outbuffer;
std::string body = "<h1>hello world</h1>";
outbuffer =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: " +
std::to_string(body.size()) + "\r\n"
"Server: Apache/2.4.41\r\n"
"Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n"
"X-Frame-Options: DENY\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Referrer-Policy: strict-origin-when-cross-origin\r\n"
"\r\n" // 空行分隔头部和正文
+ body;
// 无脑向客户端发送一个简单http响应
send(sockfd, outbuffer.c_str(), outbuffer.size(), 0);
return count;
}
}
主要逻辑就是位于run函数中
2.1 run函数⭐
poll服务器中run函数的逻辑和select中的逻辑非常类似。
1 创建关心事件集合,并且关心监视事件,创建最大文件描述符maxfd
2 循环等待poll返回就绪事件集合
3 首先处理监听事件,然后线性扫描其他fd并判断该事件是否就绪
3 监听事件新增连接需要关心新事件,其他fd执行相对应的读写方法即可
4 注意如果客户端关闭,需要close fd 然后在事件集合中清空这个fd
cpp
void run()
{
// 首先创建pollfd数组
struct pollfd fds[fdnums] = {0};
// 注册监视fd到poll关心事件中
fds[_listensock].fd = _listensock;
fds[_listensock].events = POLLIN;
int maxfd = _listensock;
//
while (true)
{
int n = poll(fds, maxfd + 1, 0);
if (fds[_listensock].revents & POLLIN)
{
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
// 将新的fd增加到rfd中,更新最大fd
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
maxfd = sockfd;
}
// 注意poll和select一样仍需要遍历所有关心的fd
for (int i = _listensock + 1; i < maxfd + 1; i++)
{
// 普通读写事件就绪
// 处理读事件
if (fds[i].revents & POLLIN)
{
int n = _callback(i);
if (n == 0)
{
// 说明对方关闭,重新处理关心的事件
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
}
}
}
2.2 pollTcpSercer.hpp
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#include <cstring>
#include <functional>
#include <thread>
const int fdnums = 100000;
namespace YZC
{
// 设置默认端口和最大backlog
const int defaultPort = 8080;
const int maxBacklog = 128;
// 设置回调函数
using func_t = std::function<int(int)>;
class tcpServer
{
public:
tcpServer(func_t func, int port = defaultPort)
: _port(port), _callback(func) {}
void init()
{
// 1.创建socket
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "sockte err" << std::endl;
exit(-1);
}
std::cout << "socket success" << std::endl;
// 2 bind绑定fd和端口
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
// 设置地址的信息(协议,ip,端口)
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址
serveraddr.sin_port = htons(_port); // 注意端口16位,2字节需要使用htons。不可
socklen_t len = sizeof(serveraddr);
if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0)
{
std::cerr << "bind err" << std::endl;
exit(-1);
}
std::cout << "bind success" << std::endl;
// 3 设置sockfd为监听fd
if (listen(_listensock, maxBacklog) < 0)
{
std::cerr << "listen err" << std::endl;
exit(-1);
}
std::cout << "listen success" << std::endl;
}
void run()
{
// 首先创建pollfd数组
struct pollfd fds[fdnums] = {0};
// 注册监视fd到poll关心事件中
fds[_listensock].fd = _listensock;
fds[_listensock].events = POLLIN;
int maxfd = _listensock;
//
while (true)
{
int n = poll(fds, maxfd + 1, 0);
if (fds[_listensock].revents & POLLIN)
{
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
// 将新的fd增加到rfd中,更新最大fd
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
maxfd = sockfd;
}
// 注意poll和select一样仍需要遍历所有关心的fd
for (int i = _listensock + 1; i < maxfd + 1; i++)
{
// 普通读写事件就绪
// 处理读事件
if (fds[i].revents & POLLIN)
{
int n = _callback(i);
if (n == 0)
{
// 说明对方关闭,重新处理关心的事件
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
}
}
}
private:
int _listensock;
int _port;
func_t _callback;
};
int serviceIO(int sockfd)
{
// 这里仅做简单的数据收发
char buffer[128] = {0};
int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (count < 0)
{
std::cerr << "recv err" << std::endl;
exit(-1);
}
if (count == 0)
{
// 对方关闭
return 0;
close(sockfd);
}
printf("client --> server:%s\n", buffer);
send(sockfd, buffer, strlen(buffer), 0);
return count;
}
int serviceHTTP(int sockfd)
{
// 这里仅做简单的数据收发
char buffer[128] = {0};
int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (count < 0)
{
std::cerr << "recv err" << std::endl;
exit(-1);
}
if (count == 0)
{
// 对方关闭
return 0;
close(sockfd);
}
printf("client --> server:%s\n", buffer);
std::string outbuffer;
std::string body = "<h1>hello world</h1>";
outbuffer =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: " +
std::to_string(body.size()) + "\r\n"
"Server: Apache/2.4.41\r\n"
"Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n"
"X-Frame-Options: DENY\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Referrer-Policy: strict-origin-when-cross-origin\r\n"
"\r\n" // 空行分隔头部和正文
+ body;
// 无脑向客户端发送一个简单http响应
send(sockfd, outbuffer.c_str(), outbuffer.size(), 0);
return count;
}
}
2.3 pollTcpserver.cc
cpp
#include "tcpServer.hpp"
#include <iostream>
#include <memory>
using namespace std;
// tcp 服务器,启动方式与udp server一样
//./tcpServer + local_port //我们将本主机的所有ip与端口绑定
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " lock_port\n\n";
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
}
uint16_t serverport = atoi(argv[1]);
unique_ptr<YZC::tcpServer> tsvr(new YZC::tcpServer(YZC::serviceIO, serverport));
tsvr->init();
tsvr->run();
return 0;
}
运行结果如下:

三. 总结和测试⭐
3.1 总结
poll是对select的修补和改进,主要点如下:
1 取消了最大fd的限制,有用户自行决定
2 将多种事件集合集中为一个事件集合,编码方便
3 通过结构体控制每一个fd的fd,event,revent。编码清晰
4 无需每一次调用后重新关心所有事件,只需要重新设置关闭的事件
不过poll并没有改变select的其他致命缺点:
每一次都需要将关心的事件拷贝进出内核,频繁的拷贝和系统调用降低性能。
每一次都要线性扫描所有关心的事件,加入有 100w连接,消耗的时间巨大。
3.2 测试
同理我们使用wrk测试一下相同条件下,poll服务器的QPS如何。首先拿出我们上一篇文章的测试结果用于对比。(云服务器的配置是 2核2G)
上篇文章的测试结果如下:

使用wrk分别测试 1000/10000/25000/55555并发连接的QPS。结果如下
1000
cpp
[yzc@study wrk]$ ./wrk -c 1000 -d 10s -t 10 http://47.105.37.157:8080
Running 10s test @ http://47.105.37.157:8080
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 133.86ms 261.53ms 2.00s 90.31%
Req/Sec 1.76k 1.49k 14.08k 90.23%
170114 requests in 10.10s, 42.99MB read
Socket errors: connect 0, read 0, write 0, timeout 391
Requests/sec: 16843.90
Transfer/sec: 4.26MB
10000
cpp
[yzc@study wrk]$ ./wrk -c 10000 -d 10s -t 10 http://47.105.37.157:8081
Running 10s test @ http://47.105.37.157:8081
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 79.63ms 204.09ms 2.00s 93.12%
Req/Sec 1.74k 3.72k 30.87k 88.68%
151096 requests in 10.11s, 38.19MB read
Socket errors: connect 0, read 0, write 0, timeout 902
Requests/sec: 14939.97
Transfer/sec: 3.78MB
25000
cpp
[yzc@study wrk]$ ./wrk -c 25000 -d 10s -t 50 http://47.105.37.157:8080
Running 10s test @ http://47.105.37.157:8080
50 threads and 25000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 172.55ms 168.40ms 1.99s 92.70%
Req/Sec 421.93 792.25 13.60k 91.79%
80420 requests in 32.49s, 20.32MB read
Socket errors: connect 0, read 442, write 0, timeout 694
Requests/sec: 2475.52
Transfer/sec: 640.64KB
55555
cpp
[root@study wrk]# ./wrk -c 55555 -d 10s -t 100 http://47.105.37.157:8080
Running 10s test @ http://47.105.37.157:8080
100 threads and 55555 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 402.39ms 404.39ms 2.00s 87.15%
Req/Sec 204.56 467.00 13.82k 92.72%
152163 requests in 1.67m, 38.46MB read
Socket errors: connect 15515, read 2144, write 394, timeout 9859
Requests/sec: 1514.17
Transfer/sec: 391.85KB
制作成表格如下:
3.3 性能测试对比表
| 并发数 | 架构 | 线程数 | QPS | 总请求数 | 平均延迟 | 吞吐量 | 错误数 | 错误类型 | 测试状态 | 数据来源 |
|---|---|---|---|---|---|---|---|---|---|---|
| 1,000 | 多进程 | 10 | 7,281 | 73,625 | 100ms | 1.84MB/s | 482 | 482超时 | ✅ 正常 | 原表 |
| 1,000 | 多线程 | 10 | 8,650 | 87,421 | 126ms | 2.19MB/s | 73 | 73超时 | ✅ 最佳 | 原表 |
| 1,000 | select | 10 | 15,965 | 160,346 | 48ms | 4.03MB/s | 286 | 286超时 | 🎯 优异 | 原表 |
| 1,000 | poll | 10 | 16,844 | 170,114 | 134ms | 4.26MB/s | 391 | 391超时 | 🎯 优异 | 本次测试 |
| 10,000 | 多进程 | 10 | 5,522 | 55,745 | 102ms | 1.40MB/s | 433 | 123读+310超时 | ✅ 正常 | 原表 |
| 10,000 | 多线程 | 10 | 7,375 | 74,453 | 194ms | 1.86MB/s | 353 | 107读+246超时 | ✅ 最佳 | 原表 |
| 10,000 | poll | 10 | 14,940 | 151,096 | 80ms | 3.78MB/s | 902 | 902超时 | 🎯 优异 | 本次测试 |
| 25,000 | 多进程 | 50 | 1,042 | 35,604 | 420ms | 270KB/s | 10,972 | 77读+8932写+1963超时 | ▲ 高压稳定 | 原表 |
| 25,000 | 多线程 | 50 | 313 | 24,298 | 205ms | 81KB/s | 953 | 691读+262超时 | ▲ 性能衰减 | 原表 |
| 25,000 | poll | 50 | 2,476 | 80,420 | 173ms | 640KB/s | 1,136 | 442读+694超时 | ▲ 性能衰减 | 本次测试 |
| 55,555 | 多进程 | 100 | 0 | 0 | 0us | 0B/s | 37,170 | 37170写错误 | ❌ 崩溃 | 原表 |
| 55,555 | 多线程 | 100 | N/A | N/A | N/A | N/A | N/A | 测试被终止 | ❌ 崩溃 | 原表 |
| 55,555 | poll | 100 | 1,514 | 152,163 | 402ms | 392KB/s | 29,912 | 15515连接+2144读+394写+9859超时 | ❌ 严重过载 | 本次测试 |
可以看到,对比于select。poll可以直接处理更多的fd,而select不修改内核情况下只能处理1024个fd。如果修改内核其实select和poll的效率是差不多的
对比于多线程/多进程,使用IO多路复用明显无论是效率还是稳定都更有效。
当然还有更高效的epoll,epoll改善了select/poll的两个致命缺陷。epoll是当代Linux服务器的最多选择