【Linux 35】多路转接 - poll

发布于:2025-02-10 ⋅ 阅读:(30) ⋅ 点赞:(0)

🌈 一、poll 初步认识

  • poll 的诞生主要是为了解决 select 以下两个缺点(select 剩下的缺点在 poll 中依然存在):
    1. select 可以等待的文件描述符的数量太少。
    2. select 的输入输出参数混合。
  • 系统调用 poll 也可以让程序同时监视多个文件描述符上的事件是否就绪,和 select 的定位相同 & 使用场景一致。

🌈 二、poll 函数原型

⭐ 1. poll 函数介绍

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

1. 参数说明

  • fds:一个由 poll 函数所监视的结构体数组,其中的每一个元素都包含三部分内容(文件描述符、监视的事件集合、就绪的事件集合)
  • nfds:fds 数组的长度
  • timeout:poll 函数的超时时间(单位 ms)

2. 参数 timeout 的取值

  • -1:调用 poll 后进行阻塞等待,直到被监视的文件描述符上有事件就绪为止。
  • 0:调用 poll 后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll 都会立即返回。
  • 特定的时间值:调用 poll 后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在指定时间后 poll 进行超时返回。

3. 返回值说明

  • 调用 poll 函数成功:返回已经有事件就绪的文件描述符的个数。
  • 调用 poll 函数失败:返回 -1,同时设置错误码,错误码的可能取值如下:
    • EFAULT:fds数组不包含在调用程序的地址空间中。
    • EINTR:此调用被信号所中断。
    • EINVAL:nfds值超过RLIMIT_NOFILE值。
    • ENOMEM:核心内存不足。
  • timeout 时间耗尽:返回 0

⭐ 2. struct pollfd 结构体介绍

1. struct pollfd 的成员

  • 其中,fd 和 events 由用户填写,revents 由操作系统填写。
struct pollfd
{
    int fd;				// 监视的文件描述符,若设置为负数则忽略 events 字段且设置 revents 字段为 0
    short int events;	// 需要监视 fd 这个文件描述符上的哪些事件	
    short int revents;	// poll 函数返回时,告知用户 fd 这个文件描述符上的哪些事件已经就绪
};

2. events 和 revents 的取值

事件 描述 是否可作为输入 是否可作为输出
POLLIN 数据(包括普通数据和优先数据)可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读(Linux 不支持)
POLLPRI 高优先级数据可读,比如 TCP 带外数据
POLLOUT 数据(包括普通数据和优先数据)可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作,它由 GNU 引入
POLLERR 错误
POLLHUP 挂起。比如管道的写端被关闭后,读端描述符上将收到 POLLHUP 事件
POLLNVAL 文件描述符没有打开

🌈 三、poll 的优缺点

⭐ 1. poll 的优点

  • struct pollfd 结构体中包含了 eventsrevents 两个字段,相当于将 select 的输入输出型参数进行分离。因此在每次调用poll之前,不需要像 select 一样重新对参数进行设置。
  • 由于 fds 数组的大小没有上限,就导致了 poll 函数能够监视的文件描述符的数量没有上限
  • poll 可以同时等待多个文件描述符,能够提高 IO 的效率。

⭐ 2. poll 的缺点

  • 和 select 函数一样,当poll返回后,需要遍历fds数组来获取就绪的文件描述符。
  • 每次调用 poll 时,都需要把大量的 struct pollfd 结构从用户态拷贝到内核态,这个开销也会随着 poll 监视的文件描述符数目的增多而增大。
  • 同时每次调用 poll 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。

🌈 sipoll 使用示例

#pragma once

#include <iostream>
#include <algorithm>
#include <sys/poll.h>

#include "socket.h"

using std::cerr;
using std::make_unique;
using std::max;
using std::unique_ptr;

using namespace socket_ns;

class poll_server
{
    const static int fd_max_num = 1024;
    const static int defauld_fd = -1;

private:
    uint16_t _port;
    unique_ptr<Socket> _listen_socket;
    bool _isrunning;
    struct pollfd* _rfds = nullptr;
    int _num;

public:
    poll_server(uint16_t port)
        : _port(port)
        , _listen_socket(make_unique<tcp_socket>())
        , _isrunning(false)
        , _num(fd_max_num)
    {}

    // 初始化服务器
    void init_server()
    {
        _listen_socket->build_listen_socket(_port);
        _rfds = new struct pollfd[_num];

        for (size_t i = 0; i < _num; i++)
        {
            _rfds[i].fd = defauld_fd;
            _rfds[i].events = 0;
            _rfds[i].revents = 0;
        }

        // 最初只有一个文件描述符
        _rfds[0].fd = _listen_socket->sockfd();
        _rfds[0].events |= POLLIN;
    }

    // 获取连接(处理 listen fd 的就绪事件)
    void accept_connection()
    {
        // 有新连接到来 - 连接事件就绪
        // 处理 listen 套接字
        Inet_addr addr;
        int sockfd = _listen_socket->accepter(&addr); // 由于 select 的原因,一定不会阻塞
        if (sockfd > 0)
        {
            LOG(DEBUG, "get a new link success, client info: %s:%d", addr.ip().c_str(), addr.port());

            // 将新的 fd 交给 poll 托管
            int pos = 0;
            for (; pos < _num; pos++)
            {
                if (_rfds[pos].fd == defauld_fd)
                {
                    _rfds[pos].fd = sockfd;
                    _rfds[pos].events |= POLLIN;
                    break;
                }
            }
            if (pos == _num)
            {
                close(sockfd);
            }
        }
    }

    // 处理 IO(处理普通 fd 的就绪事件)
    void handle_io(int i)
    {
        // 处理普通 sockfd,正常读写
        char buffer[1024];
        // 这里的读取不会阻塞,因为 select 已经判断出 fd 的读事件就绪
        ssize_t n = ::recv(_rfds[i].fd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = '\0';
            cout << "client say: " << buffer << endl;
            // 服务器给客户端回复
            string content = "<html><body><h1>hello world</h1></body></html>";
            string echo_str = "HTTP/1.0 200 OK\r\n";
            echo_str += "Content-Type: text/html\r\n";
            echo_str += "Content-Length: " + to_string(content.size()) + "\r\n\r\n";
            echo_str += content;
            // 发送数据
            ::send(_rfds[i].fd, echo_str.c_str(), echo_str.size(), 0);
        }
        else if (0 == n)
        {
            // 对端关闭连接
            LOG(INFO, "client quit, sockfd: %d", _rfds[i].fd);
            close(_rfds[i].fd);
            _rfds[i].fd = defauld_fd; // 让 select 不要再关心这个 fd 了
        }
        else
        {
            // 读取失败
            LOG(ERROR, "recv error, sockfd: %d", _rfds[i].fd);
            close(_rfds[i].fd);
            _rfds[i].fd = defauld_fd;
        }
    }

    // 处理已经就绪的事件 (一定会同时存在大量就绪的 fd,可能是普通 sockfd,也可能是 listensockfd)
    void handle_event()
    {
        for (size_t i = 0; i < _num; i++)
        {
            if (_rfds[i].fd == defauld_fd)
                continue;
            
            int fd = _rfds[i].fd;
            int revents = _rfds[i].revents;

            if (revents & POLLIN)
            {
                if (fd == _listen_socket->sockfd())
                    accept_connection();    // 处理连接事件
                else
                    handle_io(i);          // 处理 IO 事件
            }
        }
    }

    // 展示所有合法的 fd
    void print_all_legal_fd()
    {
        cout << "legal fd list: ";
        for (size_t i = 0; i < fd_max_num; i++)
            if (_rfds[i].fd != defauld_fd)
                cout << _rfds[i].fd << " ";
        cout << endl;
    }

    // 运行服务器
    void loop()
    {
        _isrunning = true;

        while (true)
        {
            int timeout = -1;
            int n = ::poll(_rfds, _num, timeout);
            
            switch (n)
            {
            case 0:
                LOG(DEBUG, "timeout, %d", timeout);
                break;
            case -1:
                LOG(ERROR, "poll error");
                break;
            default:
                LOG(INFO, "haved event ready, n: %d", n); // 如果事件就绪,但时不处理,select 会一直通知我,直到事件被处理为止
                handle_event();
                print_all_legal_fd();
                sleep(1);
                break;
            }
        }
    }

    ~poll_server()
    {
        if (_listen_socket)
            close(_listen_socket->sockfd());
    }
};

网站公告

今日签到

点亮在社区的每一天
去签到