跳到主要内容Linux 高级 IO:I/O 多路转接之 poll 接口原理与 TCP 服务器实现 | 极客日志C++
Linux 高级 IO:I/O 多路转接之 poll 接口原理与 TCP 服务器实现
Linux 下 I/O 多路转接技术中的 poll 机制。对比 select 的局限性(如 fd 上限、重置开销),阐述 poll 的结构体设计优势及接口用法。通过改造 select 版本的 TCP 服务器代码,展示基于 poll 实现高并发网络服务器的具体步骤,包括事件监听、连接管理、数据收发及超时处理,并分析 poll 在大规模 fd 场景下的性能瓶颈。
Eee_1231 浏览 前言
本文基于 Linux 高级 IO 技术,深入讲解 I/O 多路转接中的 poll 机制。在 select 版本的基础上进行扩展,介绍 poll 的接口、原理及 TCP 服务器的实现。
一、前置知识
- select 等待的 fd 数量有限,fd_set 位图只能容纳有限的 1024 个 fd,不支持扩容。
- select 输入输出参数较多,数据拷贝频率高。每次调用需从用户到内核拷贝关心的 fd,再从内核到用户拷贝就绪的 fd。
- select 每次调用都需要对关心的 fd 进行事件重置。
- 用户层需使用第三方数组管理 fd,并进行多次遍历(如重置 rfds、判断 fd 是否在 rfds 中、连接管理器寻找位置等)。
- 虽然 select 理论上可以等待多个 fd,但随着 fd 增多,数据拷贝、重置和遍历带来的开销增加,效率增长缓慢。
- 鉴于 select 的硬伤(fd 上限、重置开销),poll 作为第二种多路转接方案被引入。
二、接口和原理讲解
- poll 返回值 n 为 int 类型:大于 0 代表有 n 个 fd 事件就绪;等于 0 代表超时;小于 0 代表出错。
- timeout 参数单位为毫秒(ms)。若设置 3 秒超时,应传入 3000。
- poll 第一个参数是指向 struct pollfd 数组的指针。该结构体包含 fd(文件描述符)、events(关注事件)、revents(就绪事件)。
- revents 由内核设置。例如读事件就绪设置 POLLIN,写事件就绪设置 POLLOUT。多个事件同时就绪时按位或设置。
- poll 通过数组大小解决 select 的 fd 上限问题。用户决定数组大小,理论上可支持更多 fd,但受限于内存。
- poll 接口更简单,无需像 select 那样理解位图含义,超时时间直接传 int 值。
- poll 解决了 select 的两个硬伤:fd 上限和每次调用需重置事件。struct pollfd 结构使得 events 和 revents 分离,内核只修改 revents,用户下次调用无需重置 events。
- poll 同样需要遍历确认 fd 事件是否就绪,但在用户层维护更简单。
三、poll 版本的 TCP 服务器
1. 代码框架调整
基于 select 版本代码进行修改。将类名改为 PollServer,管理 fd 的数组改为 struct pollfd _event_fds[fd_num_max]。
#include<iostream>
#include<sys/select.h>
#include<unistd.h>
#include<poll.h>
#include"Log.hpp"
#include"Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
static fd_num_max = ;
defaultfd = ;
non_event = ;
{
:
( port = defaultport) : _port(port) {
( i = ; i < fd_num_max; i++) {
_event_fds[i].fd = defaultfd;
_event_fds[i].events = non_event;
_event_fds[i].revents = non_event;
}
}
{
_listensock.();
_listensock.(_port);
_listensock.();
;
}
;
;
;
;
;
~() { _listensock.(); }
:
Sock _listensock;
_port;
_event_fds[fd_num_max];
};
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
const
int
64
int
-1
int
0
class
PollServer
public
PollServer
uint16_t
for
int
0
bool Init()
Socket
Bind
Listen
return
true
void Accepter()
void Recver(int fd, int pos)
void Dispatcher()
void Start()
void PrintFd()
PollServer
Close
private
uint16_t
struct
pollfd
2. 核心逻辑实现
- Start 函数:初始化 listensock 到
_event_fds[0],设置 events 为 POLLIN。调用 poll 循环监听。
- Dispatcher 函数:遍历
_event_fds,检查 revents 是否包含 POLLIN。若是 listensock 则调用 Accepter,否则调用 Recver。
- Accepter 函数:接受新连接,查找空闲位置放入
_event_fds。若满则关闭连接。
- Recver 函数:读取数据。若 read 返回 0 或错误,关闭 fd 并重置位置。
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);
}
}
}
}
void Start() {
_event_fds[0].fd = _listensock.Fd();
_event_fds[0].events = POLLIN;
int timeout = 3000;
for (;;) {
int n = poll(_event_fds, fd_num_max, timeout);
switch (n) {
case 0: cout << "time out..." << endl; break;
case -1: cout << "poll error" << endl; break;
default: cout << "get a new link" << endl; Dispatcher(); break;
}
}
}
void Accepter() {
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if (sock < 0) return;
lg(Info, "accept success, %s:%d, sock fd: %d", clientip.c_str(), clientport, sock);
int pos = 1;
for (; pos < fd_num_max; pos++) {
if (_event_fds[pos].fd == defaultfd) break;
}
if (pos == fd_num_max) {
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();
}
}
void Recver(int fd, int pos) {
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n - 1] = 0;
cout << "get a message: " << buffer << endl;
} else if (n == 0) {
lg(Info, "client quit, me too, 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;
}
}
void PrintFd() {
cout << "online fd list: ";
for (int i = 0; i < fd_num_max; i++) {
if (_event_fds[i].fd != defaultfd) {
cout << _event_fds[i].fd << ' ';
}
}
cout << endl;
}
3. 主程序入口
#include"PollServer.hpp"
#include<memory>
int main() {
unique_ptr<PollServer> svr(new PollServer());
svr->Init();
svr->Start();
return 0;
}
4. Makefile
pollserver:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f pollserver
5. 辅助头文件
Log.hpp
#pragma once
#include<iostream>
#include<string>
#include<ctime>
#include<cstdio>
#include<cstdarg>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log {
public:
Log() { printMethod = Screen; path = "./log/"; }
void Enable(int method) { printMethod = method; }
~Log() {}
std::string levelToString(int level) {
switch (level) {
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error: return "Error";
case Fatal: return "Fatal";
default: return "";
}
}
void operator()(int level, const char* format, ...) {
time_t t = time(nullptr);
struct tm* ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1,
ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[2 * SIZE];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
void printLog(int level, const std::string& logtxt) {
switch (printMethod) {
case Screen: std::cout << logtxt << std::endl; break;
case Onefile: printOneFile(LogFile, logtxt); break;
case Classfile: printClassFile(level, logtxt); break;
default: break;
}
}
void printOneFile(const std::string& logname, const std::string& logtxt) {
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0) return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string& logtxt) {
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
private:
int printMethod;
std::string path;
};
Log lg;
Socket.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Log.hpp"
const int backlog = 10;
enum { SocketErr = 1, BindErr, ListenErr };
class Sock {
public:
Sock() {}
void Socket() {
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0) {
lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(uint16_t port) {
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(local);
if (bind(sockfd_, (struct sockaddr*)&local, len) < 0) {
lg(Fatal, "bind error, %s : %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen() {
if (listen(sockfd_, backlog) < 0) {
lg(Fatal, "listen error, %s : %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* clientip, uint16_t* clientport) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if (newfd < 0) {
lg(Warning, "accept error, %s : %d", strerror(errno), errno);
return -1;
}
char ipstr[128];
inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& serverip, uint16_t serverport) {
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));
socklen_t len = sizeof(peer);
int n = connect(sockfd_, (struct sockaddr*)&peer, len);
if (n == -1) {
std::cerr << "connect to " << serverip << ':' << serverport << "error" << std::endl;
return false;
}
return true;
}
void Close() {
if (sockfd_ > 0) {
close(sockfd_);
}
}
int Fd() { return sockfd_; }
~Sock() {}
private:
int sockfd_;
};
四、poll 的缺点
- poll 解决了 select 的 fd 上限和重置问题,但效率仍有瓶颈。
- poll 仍需用户维护数组,大量遍历(O(N))。在分派器、连接管理器及内核检测中均需遍历。
- 当 fd 数量极大(如 1 亿)时,遍历开销显著影响性能。
- 针对此硬伤,后续将引入 epoll 多路转接方案进行优化。
总结
本文详细讲解了 Linux poll 接口的原理及其在 TCP 服务器中的应用。通过对比 select,展示了 poll 在减少数据拷贝和重置方面的优势,并提供了完整的 C++ 实现代码。尽管 poll 有所改进,但在大规模并发场景下仍存在遍历开销,epoll 是进一步的解决方案。