高级IO-poll

目录

[一、为什么需要 poll?select 的痛点回顾](#一、为什么需要 poll?select 的痛点回顾)

[二、poll 核心原理与数据结构](#二、poll 核心原理与数据结构)

[1. 核心结构:struct pollfd](#1. 核心结构:struct pollfd)

[2. poll 函数原型](#2. poll 函数原型)

[三、实战:基于 poll 实现 TCP 服务器](#三、实战:基于 poll 实现 TCP 服务器)

[1. 类结构与初始化](#1. 类结构与初始化)

[2. 处理新连接:Accepter 方法](#2. 处理新连接:Accepter 方法)

[3. 接收客户端数据:Recver 方法](#3. 接收客户端数据:Recver 方法)

[4. 事件分发:Dispatcher 方法](#4. 事件分发:Dispatcher 方法)

[5. 启动服务器:Start 方法](#5. 启动服务器:Start 方法)

[四、poll 对比 select:优势与局限](#四、poll 对比 select:优势与局限)

优势:

局限:

[五、总结:poll 适合什么场景?](#五、总结:poll 适合什么场景?)


在网络编程中,多路复用技术是处理并发连接的基石。上一篇我们探讨了 select 的实现,但其固有的 fd 数量限制和重复初始化问题始终是瓶颈。本文将聚焦 poll 机制 ------ 它作为 select 的改进版,解决了不少痛点。我们将通过一个完整的 poll 服务器实现,深入理解其工作原理与优势。

一、为什么需要 poll?select 的痛点回顾

select 作为早期的多路复用方案,存在三个明显缺陷:

  1. fd 数量上限 :由 fd_set 位图长度决定(通常默认 1024),无法灵活扩展。
  2. 输入输出参数混合 :每次调用 select 都需重新初始化 fd_set,重复劳动且效率低。
  3. 遍历成本高:用户态和内核态都需遍历全部监控 fd 才能确定就绪事件。

poll 的出现正是为了针对性解决这些问题,尤其是前两点。

二、poll 核心原理与数据结构

1. 核心结构:struct pollfd

poll 不再使用位图,而是通过一个结构体数组管理文件描述符(fd)和事件,结构体定义如下:

cpp 复制代码
struct pollfd {
    int fd;         // 待监控的文件描述符
    short events;   // 输入:用户关心的事件(如 POLLIN 表示可读)
    short revents;  // 输出:内核返回的实际就绪事件
};
  • 分离输入输出events 仅用于设置监控需求,revents 用于返回结果,无需每次重置。
  • 事件类型清晰 :支持 POLLIN(可读)、POLLOUT(可写)、POLLERR(错误)等事件,与 select 功能类似但表达更直接。

2. poll 函数原型

cpp 复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fdsstruct pollfd 数组,存放待监控的 fd 及事件。
  • nfds:数组长度(需监控的 fd 数量)。
  • timeout:超时时间(毫秒):
    • timeout > 0:阻塞等待指定毫秒数。
    • timeout = 0:非阻塞,立即返回。
    • timeout = -1:无限期阻塞,直到有事件就绪。
  • 返回值:就绪事件的总数(失败返回 -1,超时返回 0)。

三、实战:基于 poll 实现 TCP 服务器

下面通过代码实现一个完整的 poll 服务器,感受其与 select 的差异。

1. 类结构与初始化

cpp 复制代码
#pragma once
#include <iostream>
#include <poll.h>
#include "Socket.hpp"  // 自定义套接字封装类

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;  // 可自定义的fd上限(比select更灵活)
int defaultfd = -1;                // 标记未使用的fd
int non_event = 0;                 // 无事件标记

class PollServer {
public:
    PollServer(uint16_t port = defaultport) : _port(port) {
        // 初始化pollfd数组:所有fd设为-1(未使用),事件设为0
        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;
    }
    // ... 其他方法后续展开
private:
    Sock _listensock;               // 监听套接字
    uint16_t _port;                 // 服务器端口
    struct pollfd _event_fds[fd_num_max];  // pollfd数组,管理所有监控的fd
};
  • select 不同,poll 直接用 struct pollfd 数组管理 fd,无需单独维护 fd 列表,结构更紧凑。

2. 处理新连接:Accepter 方法

当监听套接字的 POLLIN 事件就绪时,接收新连接并将客户端 fd 加入 _event_fds 数组。

cpp 复制代码
void Accepter() {
    std::string clientip;
    uint16_t clientport = 0;
    int sock = _listensock.Accept(&clientip, &clientport);  // 非阻塞,因poll已通知就绪
    if (sock < 0) return;

    lg(Info, "accept success, %s:%d, sock fd:%d", clientip.c_str(), clientport, sock);

    // 找一个空闲位置存储新客户端fd
    int pos = 1;  // 位置0留给监听套接字
    for (; pos < fd_num_max; pos++) {
        if (_event_fds[pos].fd != defaultfd) continue;
        else break;
    }

    if (pos == fd_num_max) {  // 服务器fd已满
        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();  // 打印当前在线fd
    }
}
  • 关键差异:events 只需初始化时设置一次,后续 poll 调用会复用,无需像 select 那样每次清空重设。

3. 接收客户端数据:Recver 方法

当客户端 fd 的 POLLIN 事件就绪时,读取数据并处理连接关闭 / 错误场景。

cpp 复制代码
void Recver(int fd, int pos) {
    char buffer[1024];
    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
    if (n > 0) {  // 读取成功
        buffer[n] = 0;
        cout << "get a message: " << buffer << endl;
    } else if (n == 0) {  // 客户端断开
        lg(Info, "client quit, 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;
    }
}

4. 事件分发:Dispatcher 方法

遍历 _event_fds 数组,通过 revents 检查就绪事件,分发给对应处理函数。

cpp 复制代码
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);
            }
        }
    }
}
  • select 相比,poll 通过 revents 直接返回就绪事件,无需调用 FD_ISSET 宏,代码更直观。

5. 启动服务器:Start 方法

主循环中调用 poll 监控事件,就绪后通过 Dispatcher 处理。

cpp 复制代码
void Start() {
    // 监听套接字加入监控,关注可读事件(新连接)
    _event_fds[0].fd = _listensock.Fd();
    _event_fds[0].events = POLLIN;

    int timeout = 3000;  // 超时时间3秒
    for (;;) {
        // 调用poll监控事件,无需每次重置events
        int n = poll(_event_fds, fd_num_max, timeout);

        switch (n) {
            case 0:  // 超时
                cout << "time out... " << endl;
                break;
            case -1:  // 错误
                cerr << "poll error" << endl;
                break;
            default:  // 有事件就绪
                cout << "get a new event!!!!!" << endl;
                Dispatcher();
                break;
        }
    }
}
  • 核心优势:poll 调用时无需重新初始化 _event_fds 数组,events 字段保持不变,减少重复操作。

四、poll 对比 select:优势与局限

优势:

  1. 突破 fd 数量上限select 依赖 fd_set 位图长度,而 poll 的 fd 数量由数组大小决定(可自定义,理论上仅受系统最大 fd 限制)。
  2. 输入输出分离events(输入)和 revents(输出)分离,无需每次重置事件集,减少代码冗余。
  3. 无需计算 maxfdselect 需要传入最大 fd + 1,poll 直接传入数组长度,更简洁。

局限:

  1. 遍历开销仍存在 :与 select 一样,poll 返回后仍需遍历整个数组才能找到就绪的 fd,当 fd 数量庞大时效率下降。
  2. 数据拷贝开销 :每次调用 poll 仍需将整个 pollfd 数组拷贝到内核空间,fd 越多拷贝成本越高。

完整代码:

cpp 复制代码
#pragma once

#include <iostream>
#include <poll.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
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;

            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
        }
    }
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }
    void Accepter()
    {
        // 我们的连接事件就绪了
        std::string clientip;
        uint16_t clientport = 0;
        int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
        if (sock < 0) return;
        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // sock -> fd_array[]
        int pos = 1;
        for (; pos < fd_num_max; pos++) // 第二个循环
        {
            if (_event_fds[pos].fd != defaultfd)
                continue;
            else
                break;
        }
        if (pos == fd_num_max)
        {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
            // 扩容
        }
        else
        {
            // fd_array[pos] = sock;
            _event_fds[pos].fd = sock;
            _event_fds[pos].events = POLLIN;
            _event_fds[pos].revents = non_event;
            PrintFd();
            // TODO
        }
    }
    void Recver(int fd, int pos)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get a messge: " << buffer << endl;
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            close(fd);
            _event_fds[pos].fd = defaultfd; // 这里本质是从select中移除
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            close(fd);
            _event_fds[pos].fd = defaultfd; // 这里本质是从select中移除
        }
    }
    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 // non listenfd
                {
                    Recver(fd, i);
                }
            }
        }
    }
    void Start()
    {
        _event_fds[0].fd = _listensock.Fd();
        _event_fds[0].events = POLLIN;
        int timeout = 3000; // 3s
        for (;;)
        {
            int n = poll(_event_fds, fd_num_max, timeout);
            switch (n)
            {
            case 0:
                cout << "time out... " << endl;
                break;
            case -1:
                cerr << "poll error" << endl;
                break;
            default:
                // 有事件就绪了,TODO
                cout << "get a new link!!!!!" << endl;
                Dispatcher(); // 就绪的事件和fd你怎么知道只有一个呢???
                break;
            }
        }
    }
    void PrintFd()
    {
        cout << "online fd list: ";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (_event_fds[i].fd == defaultfd)
                continue;
            cout << _event_fds[i].fd << " ";
        }
        cout << endl;
    }
    ~PollServer()
    {
        _listensock.Close();
    }

private:
    Sock _listensock;
    uint16_t _port;
    struct pollfd _event_fds[fd_num_max]; // 数组, 用户维护的!
    // struct pollfd *_event_fds;

    // int fd_array[fd_num_max];
    // int wfd_array[fd_num_max];
};

五、总结:poll 适合什么场景?

pollselect 的优化版本,解决了 fd 数量限制和事件集重复初始化问题,在中小并发场景(如 fd 数量 1000-10000)中表现优于 select。但它并未彻底解决遍历和数据拷贝的开销,因此在高并发(如十万级连接)场景中,仍需依赖 epoll(Linux)或 kqueue(BSD)等更高效的机制。

理解 poll 的设计思路 ------ 通过结构体数组分离输入输出、灵活扩展 fd 数量 ------ 是掌握多路复用技术演进的关键一步。下一篇我们将探讨 epoll,看看它如何进一步突破 poll 的瓶颈。

相关推荐
Chiang木2 小时前
C++进阶:coroutine 协程
开发语言·c++·协程
ivy159868377152 小时前
JM20329是一款高性能、低功耗的USB桥接芯片,实现串行接口(如SATA、IDE)与USB接口之间的数据转换。
c语言·开发语言·ide·嵌入式硬件·eureka·音视频·视频编解码
渡我白衣2 小时前
深入 Linux 内核启动:从按下电源到用户登录的全景解剖
java·linux·运维·服务器·开发语言·c++·人工智能
七夜zippoe2 小时前
Java 9+模块化系统(JPMS)详解:设计与迁移实践
java·开发语言·maven·模块化·jmm
三川6982 小时前
1. 网络编程基础
开发语言·网络
百***37483 小时前
PHP进阶-在Ubuntu上搭建LAMP环境教程
开发语言·ubuntu·php
what_20183 小时前
idea启动项目配置环境变量(nacos 命名空间)
java·开发语言
Fantasydg3 小时前
JSP学习
java·开发语言·学习
太空程序猿3 小时前
数据类型与变量
java·开发语言