前言
本文基于 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 const int fd_num_max = 64;
int defaultfd = -1;
int non_event = 0;
class PollServer {
public:
PollServer(uint16_t port = defaultport) : _port(port) {
for (int i = 0; i < fd_num_max; i++) {
_event_fds[i].fd = defaultfd;
_event_fds[i].events = non_event;
_event_fds[i].revents = non_event;
}
}
bool Init() {
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter();
void Recver(int fd, int pos);
void Dispatcher();
void Start();
void PrintFd();
~PollServer() { _listensock.Close(); }
private:
Sock _listensock;
uint16_t _port;
struct pollfd _event_fds[fd_num_max];
};
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 < ) ;
(Info, , clientip.(), clientport, sock);
pos = ;
(; pos < fd_num_max; pos++) {
(_event_fds[pos].fd == defaultfd) ;
}
(pos == fd_num_max) {
(Warning, , sock);
(sock);
} {
_event_fds[pos].fd = sock;
_event_fds[pos].events = POLLIN;
_event_fds[pos].revents = non_event;
();
}
}
{
buffer[];
n = (fd, buffer, (buffer) - );
(n > ) {
buffer[n - ] = ;
cout << << buffer << endl;
} (n == ) {
(Info, , fd);
(fd);
_event_fds[pos].fd = defaultfd;
} {
(Warning, , fd);
(fd);
_event_fds[pos].fd = defaultfd;
}
}
{
cout << ;
( i = ; i < fd_num_max; i++) {
(_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; }
~() {}
{
(level) {
Info: ;
Debug: ;
Warning: ;
Error: ;
Fatal: ;
: ;
}
}
{
t = ();
* ctime = (&t);
leftbuffer[SIZE];
(leftbuffer, (leftbuffer), ,
(level).(), ctime->tm_year + , ctime->tm_mon + ,
ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
(s, format);
rightbuffer[SIZE];
(rightbuffer, (rightbuffer), format, s);
(s);
logtxt[ * SIZE];
(logtxt, (logtxt), , leftbuffer, rightbuffer);
(level, logtxt);
}
{
(printMethod) {
Screen: std::cout << logtxt << std::endl; ;
Onefile: (LogFile, logtxt); ;
Classfile: (level, logtxt); ;
: ;
}
}
{
std::string _logname = path + logname;
fd = (_logname.(), O_WRONLY | O_CREAT | O_APPEND, );
(fd < ) ;
(fd, logtxt.(), logtxt.());
(fd);
}
{
std::string filename = LogFile;
filename += ;
filename += (level);
(filename, logtxt);
}
:
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) {
local;
(&local, , (local));
local.sin_family = AF_INET;
local.sin_port = (port);
local.sin_addr.s_addr = INADDR_ANY;
len = (local);
((sockfd_, ( sockaddr*)&local, len) < ) {
(Fatal, , (errno), errno);
(BindErr);
}
}
{
((sockfd_, backlog) < ) {
(Fatal, , (errno), errno);
(ListenErr);
}
}
{
peer;
len = (peer);
newfd = (sockfd_, ( sockaddr*)&peer, &len);
(newfd < ) {
(Warning, , (errno), errno);
;
}
ipstr[];
(AF_INET, &(peer.sin_addr), ipstr, (ipstr));
*clientip = ipstr;
*clientport = (peer.sin_port);
newfd;
}
{
peer;
(&peer, , (peer));
peer.sin_family = AF_INET;
peer.sin_port = (serverport);
(AF_INET, serverip.(), &(peer.sin_addr));
len = (peer);
n = (sockfd_, ( sockaddr*)&peer, len);
(n == ) {
std::cerr << << serverip << << serverport << << std::endl;
;
}
;
}
{
(sockfd_ > ) {
(sockfd_);
}
}
{ sockfd_; }
~() {}
:
sockfd_;
};
四、poll 的缺点
- poll 解决了 select 的 fd 上限和重置问题,但效率仍有瓶颈。
- poll 仍需用户维护数组,大量遍历(O(N))。在分派器、连接管理器及内核检测中均需遍历。
- 当 fd 数量极大(如 1 亿)时,遍历开销显著影响性能。
- 针对此硬伤,后续将引入 epoll 多路转接方案进行优化。
总结
本文详细讲解了 Linux poll 接口的原理及其在 TCP 服务器中的应用。通过对比 select,展示了 poll 在减少数据拷贝和重置方面的优势,并提供了完整的 C++ 实现代码。尽管 poll 有所改进,但在大规模并发场景下仍存在遍历开销,epoll 是进一步的解决方案。


