【Linux网络】多路转接poll

文章目录

  • [1. poll的作用和定位](#1. poll的作用和定位)
  • [2. 使用poll改写select服务器](#2. 使用poll改写select服务器)
  • [3. poll的优点和缺点](#3. poll的优点和缺点)

1. poll的作用和定位

poll 是一种 I/O 多路复用机制,用于同时监控多个文件描述符(fd)的状态变化。它允许程序在单个系统调用中等待多个 fd 上的事件(如可读、可写、异常等),一旦某个 fd 上的事件发生,poll 就会返回并通知应用程序。

其核心思想是"只负责等,事件就绪即通知",这使得它非常适合处理大量并发连接的服务器程序。

它是 select函数的改进和替代方案,属于同一类技术,旨在解决单个进程/线程高效管理多个I/O流的问题。

cpp 复制代码
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:指向一个 struct pollfd结构体数组的指针,用于描述多个文件描述符及其关注的事件。

  • nfds:指定 fds数组的长度(即要监视的文件描述符个数)。

  • timeout:超时时间(毫秒)。

    • -1:阻塞等待,直到有事件发生。

    • 0:非阻塞,立即返回,用于轮询。

    • > 0:等待指定毫秒数,超时后返回 0。

struct pollfd 结构体定义:

cpp 复制代码
struct pollfd {
    int   fd;         /* 要监视的文件描述符 */
    short events;     /* 用户关心的事件(输入参数) */
    short revents;    /* 实际发生的事件(输出参数) */
};

events和revents的取值:

工作流程

  1. 调用前:用户填充 fds数组。
  • 对每个关心的 fd,设置其 events字段(例如 POLLIN表示关心可读事件)。

  • 此时,fd和 events有效。这是"用户告诉内核":请帮我监视这个 fd上的这些事件。

  1. 调用 poll:内核开始监视,进程进入等待。

  2. 调用返回后:内核填充 revents字段。

  • 检查每个 fd的 revents字段。如果某位被置位(如 revents & POLLIN为真),表示对应事件已就绪。

  • 此时,fd和 revents有效。这是"内核告诉用户":你让我监视的 fd上,这些事件已经就绪了,可以处理了!

poll 相比 select 解决了哪些问题?
(1)解决了参数重置的问题

在 select 中,fd_set 是一个位掩码结构,每次调用 select 前都需要重新初始化(如 FD_ZERO, FD_SET),因为 select 会修改该集合。而 poll 的 struct pollfd 中,events 和 revents 分离:

  • events:用户设置关心的事件(输入)。
  • revents:内核返回实际发生的事件(输出)。

✅ 优势:调用 poll 后,events 不会被修改,因此可以重复使用同一个 struct pollfd 数组,无需每次重置。

(2)解决了文件描述符数量上限的问题

select 使用固定大小的 fd_set,受限于 FD_SETSIZE(通常是 1024 位),因此最多只能监控 1024 个 fd。而 poll 使用动态数组 struct pollfd *fds,理论上没有上限(受限于系统资源)。

✅ 优势:适用于高并发场景,支持更多并发连接。

注意:当 fd == -1 时,表示该 struct pollfd 条目无效,内核将忽略其 events 字段,不会对该 fd 进行任何监控。这是 poll 提供的一种灵活机制,允许动态管理监控列表。


2. 使用poll改写select服务器

这里我们就不需要采用辅助数组来存储了,直接使用struct pollfd类型的指针,不过我们这里简单一点直接写一个静态数组。逻辑上代码不需要改变,只需要改变为poll的用法。

代码如下:

cpp 复制代码
#pragma once

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

using namespace SocketModule;
using namespace LogModule;

class PollServer
{
    const static int size = 4096;
    const static int defaultfd = -1;
public:
    PollServer(int port):_listensock(std::make_unique<TcpSocket>()), _isrunning(false)
    {
        _listensock->BuildTcpSocketMethod(port);
        for(int i = 0; i < size; i++)
        {
            _fds[i].fd = defaultfd;
            _fds[i].events = 0;
            _fds[i].revents = 0;
        }

        _fds[0].fd = _listensock->Fd(); // 默认将 listensockfd 添加到 _fds 开头
        _fds[0].events = POLLIN;
    }

    void Start()
    {
        int timeout = -1;
        _isrunning = true;
        while(_isrunning)
        {
            PrintFd();
            int n = poll(_fds, size, timeout);
            switch (n)
            {
            case -1:
                LOG(LogLevel::ERROR) << "poll error ...";
                break;
            case 0:
                LOG(LogLevel::INFO) << "time out ...";
                break;
            default:
                // 有事件就绪, 就不仅仅是新连接到来, 还有可能是读事件就绪
                LOG(LogLevel::DEBUG) << "有事件就绪了..., 事件个数n : " << n;
                Dispatcher(); // 处理就绪的事件
                break;
            }
        }
        _isrunning = false;
    }

    // 事件派发器
    void Dispatcher()
    {
        // 此时读事件集合rfds被OS修改为哪些文件描述符已经就绪
        // 有事件就绪, 不仅仅是新连接到来, 还有可能是读事件就绪
        // 指定的文件描述符,在rfds里面,就证明该fd就绪了
        for(int i = 0; i < size; i++)
        {
            if(_fds[i].fd == defaultfd)
                continue;
            // fd合法,但不一定就绪
            if(_fds[i].revents & POLLIN)
            {
                // 此时事件就绪,是新连接到来,还是读事件就绪?
                if(_fds[i].fd == _listensock->Fd())
                {
                    // 如果是监听套接字就绪,那就是新连接到来
                    Accepter();
                }
                else
                {
                    // 读事件就绪
                    Recver(i);
                }
            }
        }

    }

    // 连接管理器
    void Accepter()
    {
        // 新连接到来,我们需要accept接受新连接
        InetAddr client;
        int sockfd = _listensock->Accept(&client);
        if(sockfd >= 0)
        {
            // 获取新链接到来成功, 然后呢??能不能直接read/recv()
            // 当然不行,sockfd是否读就绪,我们不清楚
            // 只有谁最清楚,未来sockfd上是否有事件就绪?肯定是poll!
            // 所以我们需要将新的sockfd,托管给poll!
            LOG(LogLevel::INFO) << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr();

            int pos = 0;
            for( ; pos < size; pos++)
            {
                if(_fds[pos].fd == defaultfd)
                    break;
            }
            if(pos == size)
            {
                LOG(LogLevel::WARNING) << "poll server full ...";
                close(sockfd); // 这里可以选择扩容,但是我们这里直接采用的静态数组
            }
            else
            {
                _fds[pos].fd = sockfd;
                _fds[pos].events = POLLIN;
                _fds[pos].revents = 0;
            }
        }
    }

    // IO处理器
    void Recver(int pos)
    {
        // 处理 sockfd 读事件
        // 我们在这里读取的时候,就不会阻塞了 --- 因为 poll 已经完成等操作了!
        char buffer[1024];
        ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer)-1, 0);
        // recv 读的时候会有bug!因为无法保证能够读取到一个完整的请求!--- TCP 是流式协议!
        // 我们目前先不做处理,等到 epoll 的时候,再做处理!
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;
        }
        else if(n == 0)
        {
            LOG(LogLevel::INFO) << "client quit ...";
            // 此时不再需要让poll帮我们再关心fd
            // 关闭fd
            close(_fds[pos].fd);
            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
        else
        {
            LOG(LogLevel::ERROR) << "recv error ...";
            // 此时不再需要让poll帮我们再关心fd
            // 关闭fd
            close(_fds[pos].fd);
            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
    }

    void PrintFd()
    {
        std::cout << "_fds[]: ";
        for (int i = 0; i < size; i++)
        {
            if (_fds[i].fd == defaultfd)
                continue;
            std::cout << _fds[i].fd << " ";
        }
        std::cout << "\r\n";
    }

    ~PollServer() {}
private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    struct pollfd _fds[size];
    // struct pollfd *_fds;
};

运行测试


3. poll的优点和缺点

poll 的优点

  1. 接口设计更清晰、易用
  • select 使用三个 fd_set 位图(readfds, writefds, exceptfds),需要手动调用 FD_SET/FD_CLR/FD_ISSET 等宏操作,容易出错。
  • poll 使用 struct pollfd 数组,每个元素独立描述一个 fd 及其关注的事件,结构化更强,语义更明确。
  • 事件请求(events)和返回结果(revents)分离,避免了每次调用前重置参数。
  1. 无文件描述符数量上限
  • select 受限于 fd_set 的固定大小(通常是 1024),无法监控更多 fd。
  • poll 使用动态数组 struct pollfd *fds,理论上没有数量限制(受限于系统内存和最大文件描述符数)。
  1. 支持更多事件类型
  • poll 支持如 POLLERR, POLLHUP, POLLNVAL 等更丰富的事件类型,而 select 对这些事件的支持较弱或不直接提供。

poll 的缺点

  1. 每次调用都需要拷贝整个 pollfd 数组
  • poll 每次系统调用时,都需要将用户态的 struct pollfd 数组拷贝到内核态。
  • 当监听的 fd 数量很大时,拷贝开销显著增加,影响性能。
  1. 返回后仍需轮询查找就绪 fd
  • 和 select 类似,poll 返回后,用户程序必须遍历所有 pollfd 元素,检查 revents 字段,找出哪些 fd 就绪。
  • 这种"线性扫描"方式在 fd 数量巨大时效率低下。
  1. 性能随 fd 数量线性下降
  • 即使只有少数 fd 就绪,内核仍需遍历所有 fd 来检查状态。
  • 在高并发场景下(如成千上万连接),效率会明显下降,不适合大规模并发服务器。
  1. 不支持 I/O 多路复用的"边缘触发"模式
  • poll 默认是"水平触发"(Level Triggered),即只要条件满足就会通知,即使上次未处理完。
  • 虽然可以通过设置非阻塞 IO + 边缘触发来模拟,但不如 epoll 原生支持高效。

为什么 poll 不适合大规模并发?

虽然 poll 解决了 select 的 fd 数量限制问题,但其每次调用都需拷贝所有 pollfd 结构体,且返回后仍需用户态轮询,导致:

  • 上下文切换和内存拷贝开销大
  • 随着 fd 数量增加,性能线性下降

因此,在现代高性能网络服务器中,epoll 成为首选,因为它:

  • 使用红黑树管理 fd,查询效率 O(log n)
  • 通过 epoll_wait 只返回就绪 fd,无需轮询
  • 事件注册后,内核维护状态,减少用户态-内核态数据拷贝
相关推荐
杜子不疼.2 小时前
【Linux】进程控制(三):进程程序替换机制与替换函数详解
android·linux·运维
ICT技术最前线2 小时前
H3C策略路由如何优化企业网络?
网络
allk552 小时前
Android 性能优化深水区:电量与网络架构演进
android·网络·性能优化
旧梦吟2 小时前
脚本网页 linux内核源码讲解
linux·前端·stm32·算法·html5
wanhengidc4 小时前
物理服务器与云服务器的不同之处
运维·服务器·网络·游戏
Lucky小小吴4 小时前
ClamAV扫描速度提升6.5倍:服务器杀毒配置优化实战指南
java·服务器·网络·clamav
kaoa0004 小时前
Linux入门攻坚——58、varnish入门
linux·运维·服务器
Xの哲學11 小时前
Linux流量控制: 内核队列的深度剖析
linux·服务器·算法·架构·边缘计算
tuokuac12 小时前
docker中nginx配置报错解决
linux·运维·服务器