【IO】多路转接Select

一、初识 select

系统提供 select 函数来实现多路复用输入/输出模型.

  • select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
select 函数原型
C
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
参数解释:
  • nfds 是文件描述符集合中最大文件描述符加1。
  • readfds 是指向需要监视读操作的文件描述符集合的指针。
  • writefds 是指向需要监视写操作的文件描述符集合的指针。
  • exceptfds 是指向需要监视异常条件的文件描述符集合的指针。
  • timeout 是指定 select 调用应该阻塞的最长时间。
函数返回值:
  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfds,writefds, exceptfds 和 timeout 的值变成不可预测。

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数 n 为负值。
  • ENOMEM 核心内存不足
参数 timeout 取值:
  • NULL:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件 描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。
关于 fd_set 结构

其实这个结构就是一个整数数组, 更严格的说, 是一个 "位图". 使用位图中对应的位来表示要监视的文件描述符.

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

C
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。

二、理解 select 执行过程

想要理解select最主要要理解fd_set结构,fd_set的本质其实是一张位图,他的每一个比特位可以表示一个文件描述符

我们使用select时,需要手动输入告诉内核,要关心哪一些fd,比特位的位置表示文件描述符的编号,比特位的内容表示是否关心这个fd,例如我们要select等待编号为4 5 6号的fd,此时的fd_set内容 ...0111 0000

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);

其实select中的中间几个参数既是输入型参数,也是输出型参数,输入很容易理解,例如上述的例子,用户需要告诉内核哪些需要关心。对于输出来说,select会等待用户关心的fd,等其中的一个或多个事件就绪的话,select就可以通过这些参数来返回,告诉用户具体是关心的哪些fd事件就绪了,它具体的做法是将传入的fd_set结构除了就绪的fd为1,其他的置为0。

这样可能就会出现一种情况,可能关心的有的fd还没有就绪,但是它却被置为0了,所以每次在调用select时就需要我们再次设置要关心的fd,通常我们需要依赖一种数据结构,通常为数组

三、select的特点

可监控的文件描述符个数取决于 sizeof(fd_set)的值. 我这边服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述 符是 512*8=4096.

将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd,

  • 一是用于再 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判 断。
  • 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先),扫描 array 的同时 取得 fd 最大值 maxfd,用于 select 的第一个参数。

fd_set 的大小可以调整,可能涉及到重新编译内核. 感兴趣可以自己去收集相关资料.

select 缺点
  • 每次调用 select, 都需要手动设置 fd 集合, 从接口使用角度来说也非常不便.
  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很 多时会很大
  • 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
  • select 支持的文件描述符数量太小

四、select 使用示例

#include <iostream>
#include <sys/select.h>
#include <string>
#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), _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildListenSocket(_port);
    }
    ~SelectServer()
    {
    }

    void Init()
    {
        //初始化select辅助数组
        for (int i = 0; i < gnum; i++)
        {
            _select_array[i] = gdefaultfd;
        }
        //这里是直接将listen套接字的fd加入到数组中
        _select_array[0] = _listensock->Sockfd();
    }

    void Accepter()
    {
        InetAddr addr;
        int sockfd = _listensock->Accepter(&addr);
        if (sockfd > 0)
        {
            LOG(DEBUG, "get a new link, client info %s:%d\n", addr.IP().c_str(), addr.Port());
            // 将sockfd添加到select辅助数组中
            bool flag = false;
            for (int pos = 1; pos < gnum; pos++)
            {
                if (_select_array[pos] == gdefaultfd)
                {
                    flag = true;
                    _select_array[pos] = sockfd;
                    LOG(INFO, "add %d to fd_array success!\n", sockfd);
                    break;
                }
            }
            // select可以等待的fd是有限的
            if (!flag)
            {
                LOG(WARNING, "Server Is Full!\n");
                ::close(sockfd);
            }
        }
    }

    void HanderIO(int i)
    {
        char buffer[1024];
        ssize_t n = ::recv(_select_array[i], buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;

            std::string responsestr = "HTTP/1.1 200 OK\r\n";
            responsestr += "Content-Type: text/html\r\n";
            responsestr += "\r\n";
            responsestr += "<html><h1>hello Linux</h1></html>";
            ::send(_select_array[i], responsestr.c_str(), responsestr.size(), 0);
        }
        else if (n == 0)
        {
            LOG(INFO, "client quit...\n");
            // 关闭fd
            ::close(_select_array[i]);
            // select 不要在关心这个fd了
            _select_array[i] = gdefaultfd;
        }
        else
        {
            LOG(ERROR, "recv error\n");
            // 关闭fd
            ::close(_select_array[i]);
            // select 不要在关心这个fd了
            _select_array[i] = gdefaultfd;
        }
    }

    void HandlerEvent(fd_set rfds)
    {
        //事件派发(遍历rfds看哪些fd就绪了,根据fd不同的类型处理不同的事件)
        for (int i = 0; i < gnum; i++)
        {
            if (_select_array[i] == gdefaultfd)
                continue;
            // 是关心的fd,但不一定就绪了,接下来检测他是否在rfds中
            if (FD_ISSET(_select_array[i], &rfds))
            {
                // 检测是listenfd还是普通的fd
                if (_listensock->Sockfd() == _select_array[i])
                {
                    // 此时可以accept了,一定不会等了
                    Accepter();
                }
                else
                {
                    HanderIO(i);
                }
            }
        }
    }

    void Loop()
    {
        while (true)
        {
            // 设置文件描述符
            fd_set rfds;
            FD_ZERO(&rfds);
            int max_fd = gdefaultfd;
            for (int i = 0; i < gnum; i++)
            {
                if (_select_array[i] != gdefaultfd)
                {
                    FD_SET(_select_array[i], &rfds);
                    if (_select_array[i] > max_fd)
                    {
                        max_fd = _select_array[i];
                    }
                }
            }
            struct timeval timeout = {30, 0};
            int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case 0:
                LOG(DEBUG, "time out, %d.%d\n", timeout.tv_sec, timeout.tv_usec);
                break;
            case -1:
                LOG(ERROR, "select error\n");
                break;
            default:
                LOG(INFO, "haved event ready, n : %d\n", n); // 如果事件就绪,但是不处理,select会一直通知我,直到我处理了!
                HandlerEvent(rfds);
                break;
            }
        }
    }

private:
    uint16_t _port;
    SockSPtr _listensock;
    int _select_array[gnum];
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张呱呱_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值