跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux 高级 IO:I/O 多路转接之 poll 接口原理与 TCP 服务器实现

Linux 下 I/O 多路转接技术中的 poll 机制。对比 select 的局限性(如 fd 上限、重置开销),阐述 poll 的结构体设计优势及接口用法。通过改造 select 版本的 TCP 服务器代码,展示基于 poll 实现高并发网络服务器的具体步骤,包括事件监听、连接管理、数据收发及超时处理,并分析 poll 在大规模 fd 场景下的性能瓶颈。

Eee_123发布于 2026/2/7更新于 2026/10/24107 浏览
Linux 高级 IO:I/O 多路转接之 poll 接口原理与 TCP 服务器实现

前言

本文基于 Linux 高级 IO 技术,深入讲解 I/O 多路转接中的 poll 机制。在 select 版本的基础上进行扩展,介绍 poll 的接口、原理及 TCP 服务器的实现。

一、前置知识

  1. select 等待的 fd 数量有限,fd_set 位图只能容纳有限的 1024 个 fd,不支持扩容。
  2. select 输入输出参数较多,数据拷贝频率高。每次调用需从用户到内核拷贝关心的 fd,再从内核到用户拷贝就绪的 fd。
  3. select 每次调用都需要对关心的 fd 进行事件重置。
  4. 用户层需使用第三方数组管理 fd,并进行多次遍历(如重置 rfds、判断 fd 是否在 rfds 中、连接管理器寻找位置等)。
  5. 虽然 select 理论上可以等待多个 fd,但随着 fd 增多,数据拷贝、重置和遍历带来的开销增加,效率增长缓慢。
  6. 鉴于 select 的硬伤(fd 上限、重置开销),poll 作为第二种多路转接方案被引入。

二、接口和原理讲解

  1. poll 返回值 n 为 int 类型:大于 0 代表有 n 个 fd 事件就绪;等于 0 代表超时;小于 0 代表出错。
  2. timeout 参数单位为毫秒(ms)。若设置 3 秒超时,应传入 3000。
  3. poll 第一个参数是指向 struct pollfd 数组的指针。该结构体包含 fd(文件描述符)、events(关注事件)、revents(就绪事件)。
  4. revents 由内核设置。例如读事件就绪设置 POLLIN,写事件就绪设置 POLLOUT。多个事件同时就绪时按位或设置。
  5. poll 通过数组大小解决 select 的 fd 上限问题。用户决定数组大小,理论上可支持更多 fd,但受限于内存。
  6. poll 接口更简单,无需像 select 那样理解位图含义,超时时间直接传 int 值。
  7. poll 解决了 select 的两个硬伤:fd 上限和每次调用需重置事件。struct pollfd 结构使得 events 和 revents 分离,内核只修改 revents,用户下次调用无需重置 events。
  8. 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];
};
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 的缺点

  1. poll 解决了 select 的 fd 上限和重置问题,但效率仍有瓶颈。
  2. poll 仍需用户维护数组,大量遍历(O(N))。在分派器、连接管理器及内核检测中均需遍历。
  3. 当 fd 数量极大(如 1 亿)时,遍历开销显著影响性能。
  4. 针对此硬伤,后续将引入 epoll 多路转接方案进行优化。

总结

本文详细讲解了 Linux poll 接口的原理及其在 TCP 服务器中的应用。通过对比 select,展示了 poll 在减少数据拷贝和重置方面的优势,并提供了完整的 C++ 实现代码。尽管 poll 有所改进,但在大规模并发场景下仍存在遍历开销,epoll 是进一步的解决方案。

目录

  1. 前言
  2. 一、前置知识
  3. 二、接口和原理讲解
  4. 三、poll 版本的 TCP 服务器
  5. 1. 代码框架调整
  6. 2. 核心逻辑实现
  7. 3. 主程序入口
  8. 4. Makefile
  9. 5. 辅助头文件
  10. Log.hpp
  11. Socket.hpp
  12. 四、poll 的缺点
  13. 总结

更多推荐文章

查看全部
  • 动态规划专题:回文串问题与区间 DP
  • 基于 Rust 与 DeepSeek 大模型的智能 API Mock 生成器构建
  • AI 提示词工程:原理、策略与精通之道
  • 汇川 InoRoboLab 机器人软件常规操作要点
  • Linux 内核设计中的核心思想与架构原则
  • AI 大模型公司商业化路径:主流变现模式与技术实现详解
  • Spring 事务管理实战:@Transactional 注解与传播机制深度解析
  • SHCTF 第三届 Web 赛题部分解题复盘
  • Pi0 大模型 CPU 环境快速上手 Web 机器人控制
  • 基于代理服务与 AI 分析的亚马逊电商数据高效爬取方案
  • 基于机器学习的生态组合塘强化城市污水处理厂脱氮优化
  • Python 数据分析师 IDE 选型指南
  • RAG 检索增强生成技术:原理架构与核心模块详解
  • 零基础转行Python工程师的学习经验与职业规划建议
  • AIGC 时代的核心协议:深入理解 Model Context Protocol (MCP)
  • YOLO12 目标检测 WebUI 快速部署实战
  • 2024 年人工智能治理蓝皮书发布:全球治理进入规则构建关键阶段
  • GitHub Copilot Workspace 100 万 Token 上下文:价值与局限分析
  • 从零搭建 AI 系统权限控制系统
  • 数据库 SQL 防火墙:内核级防护与 SQL 注入防御方案

相关免费在线工具

  • 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