当前位置: 首页 > article >正文

【Linux】多路转接select

目录

select的作用和定位

select函数

理解select执行过程

select的特点

select的缺点

select使用示例

poll


select的作用和定位

定位:在IO中,只负责进行等,不负责拷贝。

多路转接的作用是,为了等待多个fd,等待fd上面的新事件就绪(OS底层有数据--读事件就绪,或OS底层有空间--写事件就绪),通知程序员,事件已经就绪,就可以进行IO拷贝了。

select函数

  • 参数nfds是用户等待的多个fd中的最大值+1。(假如fd是1、3、5、7、9,那nfds是9+1=10)
  • 参数timeout是一个输入输出型参数,是一个struct timeval的结构体,里面有两个成员,是一个时间戳类的结构体。假设定义一个timeval的对象,timeval timeout = {5, 0},意思就是告诉select,多路复用啊你现在帮我监视多个文件描述符,策略是5s以内一直阻塞,5s以内如果没有任何一个文件描述符就绪,给我返回一次;如果5s以内,比如第2s,等待的多个文件描述符有多个就绪了,你也给我返回,并且你的timeval里一定要记录下来剩余还有多少时间,假设还剩3s,timeval里面就是{3,0},也就是5s以内阻塞等待,5s过后非阻塞轮询一次。如果timeval timeout = {0, 0},就是让select去等,如果没有就绪立马返回,这就是非阻塞轮询。如果timeval的值设为NULL,表示让select永久阻塞。

  • 返回值,大于0,表示有几个就绪;等于0,表示超时;小于0,表示select出错。

第2、3、4个参数的类型是fd_set,这个数据类型是OS提供的,表示文件描述符集。实际上,fd_set是一个位图结构,那就存在两方面内容,1.比特位的位置:表示的就是文件fd的值,2.比特位的内容,0还是1。

  • 第2、3、4个参数,是输入输出型参数,readfds只关心读事件,writefds只关心写事件,exceptfds只关心异常事件。对于某一个文件描述符,看关心读事件、写事件、异常事件从而加入到对应的fd_set。

我们先关心读事件文件描述符集,当输入这个参数时,就是用户告诉内核,你要帮我关心fd_set集合中的所有fd读事件哦。比特位的位置:文件描述符的编号;比特位的内容:是否关心该fd的事件。当这个参数输出时,就是内核告诉用户,你让我关心的fd_set集合中,都有哪些已经就绪了。比特位的位置:文件描述符的编号;比特位的内容:对应的fd,事件是否发生。如果只有某些文件描述符就绪,那下次select时,可能要求我们每次调用select时,都要进行参数重置!

fd_set结构

这个结构就是一个整数数组,严格说是一个“位图”。使用位图中对应的位表示要监视的文件描述符。

操作系统提供了一组fd_set的接口,来比较方便的操作位图。

void FD_CLR(int fd, fd_set* set); // 用于清除文件描述符集set中相关的fd位
int FD_ISSET(int fd, fd_set* set);//用来测试文件描述符集set中相关fd位是否为真
void FD_SET(int fd, fd_set* set); //用于设置文件描述符集set中相关的fd位
void FD_ZERO(fd_set* set); // 用于清除文件描述符集set中全部位

timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的文件描述符没有事件发生则函数返回0。

常用的代码片段如下:

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
::select(fd+1, &rfds, nullptr, nullptr, nullptr);
if(FD_ISSET(fd, &rfds)){......}

理解select执行过程

关键在于理解fd_set,为方便理解,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

  1. 执行fd_set set; FD_ZERO(&set);则set用位表示是0000 0000。
  2. 若fd=5,则执行FD_SET(fd, &set);然后set变为0001 0000(第5位置1)。
  3. 若再加入fd=2,fd=1,则set变为0001 0011。
  4. 执行select(6, &set, nullptr, nullptr, nullptr)。
  5. 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000 0011。

select要正常工作,需要借助一个辅助数组,来保存所有合法fd。

select的特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)*8的值,可能是1024,每一个bit表示一个文件描述符。
  • 将fd加入select监控集的同时,还要再使用一个fd_array数组保存放到select中的fd。这个fd_array的作用一是用于在select返回后,fd_array作为源数据和fd_set进行FD_ISSET判断;二是select返回后会把以前加入的但无事发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入,扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

select的缺点

  • 每次调用select,都要手动设置fd集合,非常不便。
  • 每次调用select,都要把fd集合从用户态拷贝到内核态,开销很大。
  • 每次调用select都需要在内核遍历传来的所有fd,开销很大。
  • select支持的文件描述符数量太少。

select使用示例

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

using namespace socket_ns;

class SelectServer
{
    const static int gnum = sizeof(fd_set) * 8;
    const static int gdefaultfd = -1;

public:
    SelectServer(uint16_t port) : _port(port), _listensocket(std::make_unique<TcpSocket>())
    {
        _listensocket->BuildListenSocket(_port);
    }
    void InitServer()
    {
        for (int i = 0; i < gnum; i++)
        {
            fd_array[i] = gdefaultfd;
        }
        fd_array[0] = _listensocket->Sockfd(); // 默认添加listensock到数组中
    }
    // 处理新连接
    void Accepter()
    {
        // 我们叫做连接事件就绪,等价于读事件就绪
        InetAddr addr;
        int sockfd = _listensocket->Accepter(&addr); // 会不会被阻塞?一定不会!
        if (socket > 0)
        {
            LOG(DEBUG, "get a new link, client info %s : %d\n", addr.Ip(), addr.Port());
            // 已经获得了一个新的sockfd
            //  接下来可以读取吗?绝对不能读!读取的时候,条件不一定满足
            // 谁最清楚底层fd的数据是否已经就绪了呢?通过select!
            // 想办法把新的fd添加给select,由select统一进行监管
            // select为什么等待的fd越来越多?
            // 只要将新的fd,添加到fd_array中即可!
            bool flag = false;
            for (int pos = 1; pos < gnum; pos++)
            {
                if (fd_array[pos] == gdefaultfd)
                {
                    flag = true;
                    fd_array[pos] = sockfd;
                    LOG(INFO, "add %d to fd_array success!\n", sockfd);
                    break;
                }
            }
            if (!flag)
            {
                LOG(WARNING, "Server is Full!\n");
                ::close(sockfd);
            }
        }
        else
        {
            return;
        }
    }
    // 处理新IO
    void HandlerIO(int i)
    {
        // 普通的文件描述符,正常读写
        char buffer[1024];
        ssize_t n = ::recv(fd_array[i], buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞吗?不会
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say#" << buffer << std::endl;
            std::string echo_str = "[ server echo info ]";
            echo_str += buffer;
            ::send(fd_array[i], echo_str.c_str(), sizeof(echo_str), 0);
        }
        else if (n == 0)
        {
            LOG(INFO, "client quit...\n");
            // 关闭fd
            // select下次不要再关心这个fd了
            ::close(fd_array[i]);
            fd_array[i] = gdefaultfd;
        }
        else
        {
            LOG(ERROR, "recv error\n");
            // 关闭fd
            // select下次不要再关心这个fd了
            ::close(fd_array[i]);
            fd_array[i] = gdefaultfd;
        }
    }
    // 一定会存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
    void HandlerEvent(fd_set &rfds)
    {
        //事件派发
        for (int i = 0; i < gnum; i++)
        {
            if (fd_array[i] == gdefaultfd)
                continue;
            // fd一定是合法的fd
            // 合法的fd不一定就绪,判断fd是否就绪?
            if (FD_ISSET(fd_array[i], &rfds))
            {
                // 读事件就绪
                // 1.listensockfd 2.normal sockfd
                if (_listensocket->Sockfd() == fd_array[i])
                {
                    Accepter();
                }
                else
                {
                    HandlerIO(i);
                }
            }
        }
    }
    void Loop()
    {
        while (true)
        {
            // 1.文件描述符进行初始化
            fd_set rfds;
            FD_ZERO(&rfds);
            int max_fd = gdefaultfd;

            // 2. 合法的fd,添加到rfds集合中
            for (int i = 0; i < gnum; i++)
            {
                if (fd_array[i] == gdefaultfd)
                    continue;
                FD_SET(fd_array[i], &rfds);
                // 2.1更新出最大的文件fd值
                if (max_fd < fd_array[i])
                {
                    max_fd = fd_array[i];
                }
            }

            struct timeval timeout = {3, 0};

            //_listensocket->Accepter();                                                        // 不能,listensock && accept 我们把他看成IO类的函数,只关心新连接的到来,等价于读事件就绪
            int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, &timeout); // 临时
            switch (n)
            {
            case 0:
                LOG(DEBUG, "timeout, %d.%d\n", timeout.tv_sec, timeout.tv_usec);
                break;
            case -1:
                LOG(ERROR, "select error\n");
                break;
            default:
                LOG(INFO, "have event ready, n : %d\n", n); // 如果事件就绪,但是不处理,select就会一直通知我,知道处理了。
                HandlerEvent(rfds);
                PrintDebug();
                sleep(1);
                break;
            }
        }
    }

    void PrintDebug()
    {
        std::cout << "fd list: ";
        for (int i = 0; i < gnum; i++)
        {
            if (fd_array[i] == gdefaultfd)
                continue;
            std::cout << fd_array[i] << " ";
        }
        std::cout << "\n";
    }
    ~SelectServer() {}

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensocket;

    // select要正常工作,需要借助一个辅助数组,来保存所有合法fd
    int fd_array[gnum];
};

poll

poll的出现解决了select的两个问题:1.支持的文件描述符太少;2.每次都要对文件描述符集重置。

作用:和select一样。

定位:只负责等,一旦等待就绪,就会事件派发。

参数timeout和select中的类似,但是以毫秒为单位设定的超时时间,这个参数只做输入,并不能传出剩余时间;timeout=0表示非阻塞,timeout=-1表示阻塞;返回值ret>0,表示有几个fd就绪了,ret=0,表示超时,ret<0,poll出错。第一个参数fds表示“数组”起始地址,第二个参数表示该“数组”元素个数。 struct pollfd是什么呢?是一个结构体:

这个结构体有3个字段,short events表示事件类型,有16个bit位,下面的宏名称占据不同的比特位,未来想让该文件描述符设置对某些事件的关心,就可以这样,events=POLLIN|POLLOUT。 poll也要做到,1.用户告诉内核,你要帮我关心哪些fd上的哪些时间(int fd,short events) 2.内核告诉用户,你让我关心的哪些fd上的哪些事件已经就绪了(int fd,short revents)。这样就不需要因为接口设计的缺陷,对fd和关心的事件进行重新设定了。

events和revents的取值是:

这些取值就是宏。

“数组”大小nfds可以自己随意设置,所以poll等待的文件描述符理论上没有上限,解决了文件描述符数量太少的问题。

但是poll同样有一个严重的缺点,poll底层也需要遍历所有的fd,来获取就绪的fd和它的事件。


http://www.kler.cn/a/506035.html

相关文章:

  • 数字人助力企业出海增长,魔珐科技亮相2025晋江跨境电商峰会
  • 【EI 会议征稿通知】第七届机器人与智能制造技术国际会议 (ISRIMT 2025)
  • 告别 Excel,拥抱 R 语言:开启数据分析新时代
  • vue3使用vue-native-websocket-vue3通讯
  • 算法每日双题精讲 —— 二分查找(二分查找,在排序数组中查找元素的第一个和最后一个位置)
  • 迅为RK3568开发板篇OpenHarmony配置HDF驱动控制LED-新增 topeet子系统-编写 bundle.json文件
  • PyTorch使用教程(1)—PyTorch简介
  • MongoDB 学习建模与设计思路--统计数据更新案例
  • Ubuntu本地部署网站
  • 实现小球不断往下滚动
  • LabVIEW实现油浸式变压器自主监测与实时报告
  • tplink rt406路由器如何配置端口映射
  • 人脸识别SDK(Android)之Github示例及集成详解
  • C语言初阶习题【29】杨氏矩阵
  • 代码随想录算法训练营day30
  • python爬虫根据需要查找某个链接并保存
  • 阿里云 EMR 发布托管弹性伸缩功能,支持自动调整集群大小,最高降本60%
  • 如何解决 XGBoost 控制台警告:版本不一致导致的模型加载问题
  • day10_Structured Steaming
  • 【MATLAB代码】CV和CA模型组成的IMM(滤波方式为UKF),可复制粘贴源代码
  • 神经网络常见操作(卷积)输入输出
  • 【微服务】SpringBoot 通用异常处理方案使用详解
  • PyTorch使用教程(3)-Tensor包
  • C语言预处理艺术:编译前的魔法之旅
  • 人工智能-机器学习之多分类分析(项目实战二-鸢尾花的多分类分析)
  • git仓库迁移(从一个平台的仓库迁移到另一个平台的仓库)