多路复用 poll -- poll 的介绍,poll 的优缺点,poll 版本的 TCP 回显服务器

目录

1. poll 的介绍

1.1 poll 函数介绍

1.2 常用事件标记

2. poll 的优缺点

3. 使用 poll 实现 TCP 回显服务器

3.1 前置代码

3.2 代码实现


1. poll 的介绍

        poll 是 Linux/Unix 系统中改进型的 I/O 多路复用机制,设计目标是解决 select 的核心痛点(如 FD 数量硬限制、fd_set 集合操作繁琐),同时保留 I/O 多路复用的核心逻辑(单个进程监控多个 FD)。

1.1 poll 函数介绍

原型:
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 
头文件:
    #include <poll.h>
 
参数:
    fds:struct pollfd 数组地址,存储所有待监控的 FD 及其事件配置。
    nfds:fds 数组的长度,即需要监控的 FD 总数,无上限。
    timeout:timeout > 0,阻塞 timeout 毫秒,若期间有 FD 准备就绪则立即返回,
    超时无 FD 就绪则返回0;timeout = 0,非阻塞模式,有无 FD 都立即返回;timeout = -1,
    阻塞等待,直到有 FD 就绪才返回。
 
返回值:
    >0:表示具体有多少个就绪的 fd。
    =0:表示在超时时间内没有 fd 准备就绪。
    <0:select 错误。
功能:
    让一个进程 / 线程同时监控多个文件描述符(File Descriptor,FD),
    等待其中任意一个或多个 FD 变为 “就绪状态”(可读、可写或发生异常),
    从而避免为每个 FD 单独创建线程 / 进程,提升 I/O 效率。
// struct pollfd 结构体

struct pollfd {
    int   fd;         // 待监控的文件描述符(FD)
    short events;     // 期望监控的事件(输入参数,如 POLLIN 表示监控可读)
    short revents;    // 实际发生的事件(输出参数,由内核填充,如 POLLIN 表示可读就绪)
};

1.2 常用事件标记

        下列常用事件标记的宏值,决定了 poll 监听 fd 的哪些事件的宏。

2. poll 的优缺点

        优点:

        1. 无 FD 数量硬限制poll 用动态数组(pollfd 数组)替代 select 的 fd_set 位图,摆脱了 FD_SETSIZE(默认 1024)的硬限制。

        2. FD 与事件管理更灵活:每个 FD 的 “监控事件”(如可读、可写)和 “就绪事件”(如实际发生的事件)通过 pollfd 结构体的 events(输入)和 revents(输出)分离,不会像 select 那样破坏原监控集合。减少代码冗余。

        3. 跨平台兼容性强:遵循 POSIX 标准,支持 Linux、Unix、macOS 等主流类 Unix 系统,Windows 也可通过 WSAPoll 接口实现兼容(逻辑一致)。

        缺点:

        1. 线性扫描效率低poll 返回后,无法直接获取 “就绪 FD 列表”,仍需遍历整个 pollfd 数组,通过检查 revents 字段判断 FD 是否就绪。时间复杂度为 O (n),当 FD 数量达到万级以上时,遍历开销会急剧增大,成为性能瓶颈。

        2. 内核 / 用户态拷贝开销:每次调用 poll 时,需将整个 pollfd 数组从用户态拷贝到内核态;返回时,内核需将更新后的 revents 字段(整个数组)拷贝回用户态。FD 越多,拷贝耗时越长,浪费 CPU 资源。

3. 使用 poll 实现 TCP 回显服务器

3.1 前置代码

        参考多路复用 select 中的前置代码。

3.2 代码实现

// pollServer.hpp -- poll 服务器类

#pragma once

#include <iostream>
#include <memory>
#include <unistd.h>
#include <poll.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)
    {
        // 1. 创建套接字并绑定端口号并开始进行监听
        _listensock->BuildTcpSocketMethod(port);

        // 2. 将 pollfd 数组中的 fd 全部默认设置为 -1
        for (int i = 0; i < size; ++i)
            _fds[i].fd = defaultfd;

        // 3. 将  pollfd 数组中首元素的 fd 设置为监听套接字的 fd
        _fds[0].fd = _listensock->Fd();
        _fds[0].events = POLLIN;
    }

    void Start()
    {
        _isrunning = true;

        while (_isrunning)
        {
            PrintFd();
            int n = poll(_fds, size, -1);
            switch (n)
            {
            case -1:
                // poll 错误
                LOG(LogLevel::ERROR) << "poll error";
                break;
            case 0:
                // 2. poll 阻塞等待超时
                LOG(LogLevel::INFO) << "poll time out...";
                break;
            default:
                // 3. 读事件就绪(listen 套接字也是读事件就绪)
                LOG(LogLevel::DEBUG) << "读事件就绪..., n: " << n;
                Dispatcher(); // 进行事件派发
                break;
            }
        }

        _isrunning = false;
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~PollServer() {}

    // 事件派发器
    void Dispatcher()
    {
        for (int i = 0; i < size; ++i)
        {
            // 1. 排除不合法的 fd
            if (_fds[i].fd == defaultfd)
                continue;
            // 2. 将合法且就绪的 fd 进行事件派发
            if (_fds[i].revents & POLLIN)
            {
                if (_fds[i].fd == _listensock->Fd())
                    Accepter();
                else
                    Recver(i);
            }
        }
    }

    // 连接管理器
    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client); // 此时的 accept 不会被阻塞
        if (sockfd >= 0)
        {
            LOG(LogLevel::INFO) << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr();
            // 将新连接托管给 select
            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)
    {
        char buffer[1024];
        ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 这里读的时候会有 bug,不能保证一次读取全部的数据
        if (n > 0)
        {
            // 1. 读取到客户端传入的数据
            buffer[n] = 0;
            std::cout << "sockfd: " << _fds[pos].fd << ", client say@ " << buffer << std::endl;
        }
        else if (n == 0)
        {
            // 2. 客户端退出 -- 关闭连接并将其 fd 从 pollfd 数组中移除
            LOG(LogLevel::INFO) << "sockfd: " << _fds[pos].fd << ", client quit...";
            close(_fds[pos].fd);

            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
        else
        {
            // 3. 读取出错 -- 关闭连接并将其 fd 从 pollfd 数组中移除
            LOG(LogLevel::ERROR) << "recv error";
            close(_fds[pos].fd);

            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
    }

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

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    pollfd _fds[size];
};
// main.cc -- 主函数

#include "PollServer.hpp"

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        exit(USAGE_ERR);
    }

    Enable_Console_Log_Strategy();
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);
    svr->Start();
    return 0;
}

        效果与使用 select 实现的服务器相同。

<think>首先,用户的问题是:“我想理解网络编程中poll如何实现多路复用 请问网络编程中poll如何实现IO多路复用”。我需要基于系统级指令和用户可见层的指导来答。 系统级指令: 1. 所有行内数学表达式使用$...$格式。 2. 独立公式使用$$...$$格式并单独成段。 3. LaTeX语法正确。 4. 使用中文答。 5. 生成相关问题。 6. 答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层: 1. 答结构清晰,帮助用户逐步解决问题,尽量保证答真实可靠。 参考站内引用: - 引用[1]:描述了poll的结构体和事件类型。 - 引用[2]:也描述了poll结构体。 用户的问题是关于poll如何实现IO多路复用,所以我需要基于这些引用来构建答。 答结构: - 由于用户要求逐步解决问题,我应该从基础开始解释poll的概念。 - 然后,描述poll的工作机制。 - 最后,解释如何实现多路复用。 关键点: - poll函数允许监视多个文件描述符(sockets)的状态。 - 它使用一个结构体数组(struct pollfd)来指定要监视的文件描述符和事件。 - poll函数阻塞直到有事件发生或超时。 - 当事件发生时,poll,并更新revents字段,指示哪些事件发生了。 步骤: 1. 介绍poll的概念。 2. 解释struct pollfd结构。 3. 描述poll函数的使用。 4. 说明如何实现多路复用。 5. 提供简单示例(如果需要)。 6. 生成相关问题。 确保使用中文答。 生成相关问题:在答结束后,添加相关问题部分。 引用标识:在答中,如果引用站内内容,添加[^1]或[^2]。引用[1]和[^2]是提供的,所以我应该自然地引用它们。 答大纲: 1. 引言:解释IO多路复用的概念。 2. poll介绍:基于引用描述poll。 3. poll的实现机制:详细说明如何工作。 4. 代码示例:提供一个简单的C语言示例来展示poll的使用。 5. 总结:简要总结poll的优势。 6. 相关问题:生成2-3个相关问题。 数学表达式:用户问题不涉及数学,所以可能不需要数学表达式。但如果有,比如在解释事件时,使用行内格式。 现在,构建答。</think>### 网络编程中poll如何实现IO多路复用 IO多路复用网络编程中的关键技术,它允许一个进程同时监视多个文件描述符(如套接字),以高效处理I/O事件。poll是Linux系统提供的一种IO多路复用机制,它通过事件驱动的方式实现多路复用。下面我将逐步解释其工作原理和实现方式。 #### 1. **IO多路复用的基本概念** IO多路复用解决了传统阻塞I/O的效率问题。在单线程中,程序需要同时处理多个连接时,如果使用阻塞I/O,每个连接都需要独立的线程或进程,这会消耗大量资源。poll通过一个系统调用监视多个文件描述符的状态,当其中任何一个文件描述符准备好读写时,poll函数返,通知程序进行相应操作。这避免了线程切换的开销,提高了并发性能。 #### 2. **poll的核心机制:struct pollfd结构体** poll的核心是使用`struct pollfd`结构体数组来指定要监视的文件描述符和事件。每个结构体代表一个被监视的文件描述符,其定义如下: ```c struct pollfd { int fd; // 文件描述符,如套接字 short events; // 用户设置的事件掩码,指定要监视的事件 short revents; // 内核返的事件掩码,表示实际发生的事件 }; ``` - **fd**:指定要监视的文件描述符(例如,网络套接字)。 - **events**:由用户设置,表示需要监视的事件类型(如可读、可写)。常见事件包括: - `POLLIN`:有数据可读(例如,套接字接收到数据)。 - `POLLOUT`:写数据不会导致阻塞(例如,套接字发送缓冲区有空闲)。 - `POLLERR`:文件描述符发生错误(非法事件)。 - 其他事件如`POLLRDNORM`(普通数据可读)等,具体见引用[^1]。 - **revents**:由内核在poll函数返时设置,表示实际发生的事件。用户通过检查revents来判断哪些事件已触发,从而避免无效轮询。 这种设计允许程序传递多个`struct pollfd`结构体,一次性监视多个文件描述符[^1][^2]。 #### 3. **poll函数的工作流程** poll函数的原型为: ```c int poll(struct pollfd *fds, nfds_t nfds, int timeout); ``` - **fds**:指向`struct pollfd`数组的指针。 - **nfds**:数组的长度(即监视的文件描述符数量)。 - **timeout**:超时时间(毫秒)。如果为负数,poll会无限阻塞;如果为0,则立即返。 **工作流程**: 1. **初始化**:程序创建一个`struct pollfd`数组,设置每个元素的fd和events字段。例如: ```c struct pollfd fds[2]; fds[0].fd = sock1; // sock1是套接字描述符 fds[0].events = POLLIN; // 监视可读事件 fds[1].fd = sock2; fds[1].events = POLLOUT; // 监视可写事件 ``` 2. **调用poll**:程序调用`poll(fds, 2, 1000)`,表示监视2个文件描述符,超时为1秒。此时,内核会阻塞当前线程,直到以下情况之一发生: - 某个文件描述符的事件发生(如sock1有数据到达)。 - 超时时间到。 3. **内核处理**:内核监视所有指定的文件描述符。当事件发生时(例如,sock1变为可读),内核更新对应结构体的revents字段(如设置`fds[0].revents = POLLIN`)。 4. **返结果**:poll就绪的文件描述符数量。程序遍历数组,检查revents字段: - 如果`revents & POLLIN`为真,表示该文件描述符可读,程序可调用read函数。 - 如果`revents & POLLERR`为真,表示发生错误,需处理异常。 - 示例代码片段: ```c int ret = poll(fds, 2, 1000); if (ret > 0) { for (int i = 0; i < 2; i++) { if (fds[i].revents & POLLIN) { // 处理sock1的可读事件 } if (fds[i].revents & POLLERR) { // 处理错误 } } } ``` #### 4. **如何实现多路复用** poll实现多路复用的关键在于: - **事件驱动**:程序不主动轮询每个文件描述符,而是通过poll函数让内核集中监视所有描述符的状态变化。这减少了CPU占用率。 - **批量处理**:一次poll调用可监视数百个文件描述符(仅受系统限制),当多个事件同时发生时,poll所有就绪的描述符,程序批量处理它们。 - **避免阻塞**:通过timeout参数,程序可以控制阻塞时间,实现非阻塞或超时机制。 - **优势与局限**:相比select函数,poll没有文件描述符数量限制(select通常限制为1024),且revents字段避免了事件掩码的重复设置。但poll在大量文件描述符时性能可能下降,因为每次调用都需要遍历整个数组(时间复杂度为$O(n)$)。 这种机制在网络服务器中广泛应用,例如Web服务器处理多个客户端连接时,使用poll可以高效管理读写事件[^1][^2]。 #### 5. **简单示例:基于poll的Echo服务器** 以下是一个简化的C语言示例,展示poll如何用于处理多个客户端连接: ```c #include <poll.h> #include <unistd.h> #include <stdio.h> #include <sys/socket.h> int main() { int server_fd = /* 创建服务器套接字并绑定 */; listen(server_fd, 5); struct pollfd fds[10]; fds[0].fd = server_fd; fds[0].events = POLLIN; // 监视新连接 while (1) { int ret = poll(fds, 10, 1000); // 监视10个描述符,超时1秒 if (ret > 0) { if (fds[0].revents & POLLIN) { // 接受新客户端连接 int client_fd = accept(server_fd, NULL, NULL); // 将client_fd添加到fds数组,监视POLLIN } for (int i = 1; i < 10; i++) { if (fds[i].revents & POLLIN) { char buffer[1024]; read(fds[i].fd, buffer, sizeof(buffer)); // 读取数据 write(fds[i].fd, buffer, sizeof(buffer)); // 数据 } } } } return 0; } ``` 在这个示例中,poll监视服务器套接字和多个客户端套接字,实现高效的多路复用。 #### 6. **总结** poll通过事件掩码和内核通知机制,实现了高效IO多路复用。它适用于中等并发场景,但需注意在高并发时性能可能不如epoll。理解poll是学习更高级机制(如epoll)的基础[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值