【Linux】poll改写select代码

poll 是 Linux 中一种多路 IO 复用的系统调用,用于同时监听多个文件描述符(fd)的就绪事件,是对 select 的改进方案

1. poll

核心接口与参数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数含义
fds指向struct pollfd 数组的起始地址,数组中每个元素代表一个待监听的文件描述符及事件
nfds数组中元素的个数(待监听的 fd 总数)
timeout超时时间,单位毫秒

timeout 取值:

  1. timeout > 0:阻塞等待,最多等待 timeout 毫秒
  2. timeout = 0:非阻塞模式
  3. timeout = -1:永久阻塞

struct pollfd 结构:

struct pollfd {
    int fd;         /* 待监听的文件描述符 */
    short events;   /* 用户告诉内核:要监听的事件(输入参数)位图 */
    short revents;  /* 内核告诉用户:已就绪的事件(输出参数)位图 */
};

events和revents是位图操作,要熟练运用|和&
  • events |= POLLIN:我要监听这个可读事件(用户吩咐内核追加事件)
  • revents & POLLIN:这个可读事件来了吗(内核答复用户,判断就绪)

核心事件类型(常用):

其中POLLIN和POLLOUT最常用

事件含义
POLLIN数据可读(包括普通数据、优先数据)
POLLOUT数据可写(包括普通数据、优先数据)
POLLERR发生错误
POLLHUP连接被挂断(对方关闭连接)
PULLNVAL文件描述符无效(未打开)

2. poll 对 select 的改进

  1. 参数分离,无需重置:select的 fd_set 是输入输出复用的,每次调用前都要重新设置;poll 将用户关心的事件(events)和内核返回的就绪事件(revents)分离,调用前后无需重置参数。
  2. 无最大 fd 数量限制:select 受限于FD_SETSIZE(默认 1024),而 poll 没有硬上限,理论上可监听任意数量的文件描述符。
  3. 支持更多事件类型:提供了更细粒度的事件标识(如 POLLPRIPOLLRDHUP 等),可满足不同场景的需求。

3. poll 的优缺点

优点

  • 接口更清晰,事件读写分离,使用更便捷。
  • 突破了 select 的 fd 数量上限,适合高并发场景

缺点

  1. 用户态到内核态拷贝开销大:每次调用 poll,都需要将整个 pollfd 数组从用户态拷贝到内核态,fd 数量越多,开销越大。
  2. 仍需遍历所有 fd:poll 返回后,用户需要遍历整个 pollfd 数组,检查每个 fd 的 revents 是否就绪,时间复杂度为 O(n)
  3. 大量连接下效率下降:当同时连接的客户端很多,但同一时间只有少量 fd 就绪时,遍历整个数组的成本很高,效率会随监听数量增加线性下降

4. poll代码改写select

PollServer实现

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

using namespace SocketModule;
using namespace LogModule;

class PollServer
{
public:
    const static int size = 4096;
    const static int defaultfd = -1;

    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();
        _fds[0].events = POLLIN;
    }
    void Start()
    {
        _isrunning = true;
        int timeout = -1;
        while (_isrunning)
        {
            PrintFd();

            int n = poll(_fds, size, timeout);

            if (n == -1)
            {
                // 出错了
                LOG(LogLevel::ERROR) << "poll error";
                continue;
            }
            else if (n == 0)
            {
                // 超时了
                LOG(LogLevel::INFO) << "poll time out...";
                continue;
            }
            else
            {
                // 成功:有文件描述符就绪
                LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
                Dispatcher(); // 处理就绪的事件
                continue;
            }
        }
        _isrunning = false;
    }
    void Dispatcher()
    {
        for (int i = 0; i < size; i++)
        {
            if (_fds[i].fd == defaultfd)
            {
                // 空的
                continue;
            }
            if (_fds[i].revents & POLLIN) // 检查这个fd是否已经读就绪了
            {

                if (_fds[i].fd == _listensock->Fd())
                {
                    // 说明是新来的连接
                    Accepter();
                }
                else
                {
                    // 派发任务
                    Recver(i);
                }
            }
        }
    }

    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client);
        if (sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "accept error";
        }
        else
        {
            // accept到新连接,把新fd交给select
            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) << "select server full";
                close(sockfd);
            }
            else
            {
                _fds[pos].fd=sockfd;
                _fds[pos].events |= POLLIN;
                _fds[pos].revents=0;
            }
        }
    }

    void Recver(int pos)
    {
        char buffer[1024];
        ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say@ " << buffer << std::endl;
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "client quit...";
            // 2. 关闭fd
            close(_fds[pos].fd);
            // 1. 把位图中fd不要就绪了
            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
            
        }
        else
        {
            LOG(LogLevel::ERROR) << "recv error";
           // 2. 关闭fd
            close(_fds[pos].fd);
            // 1. 把位图中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";
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~PollServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;

    struct pollfd _fds[size];
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值