Linux高级IO之select

(。・∀・)ノ゙嗨!你好这里是ky233的主页:这里是ky233的主页,欢迎光临~https://blog.csdn.net/ky233?type=blog

点个关注不迷路⌯'▾'⌯

目录

一、五种IO模型

1.IO效率的问题

2.阻塞IO是最常见的IO模型.

3.非阻塞IO

4.信号驱动IO

5.IO多路转接

6.异步IO

7.小结

二、非阻塞IO

1.fcntl

三、I/O多路转接之select

1.快速认识select接口

2.fd_set

3.接口挑一个重点参数,细致分析

4.编写代码

5.select的优缺点


网络的本质就是:IO,是一次输入和输出

一、五种IO模型

1.IO效率的问题

IO的效率是很低效的,毕竟对方在千里之外。

IO为什么低效,以读为例:

  1. 当我们read/recv的时候,如果底层缓冲区没有数据,read/recv会怎么办?-----阻塞
  2. 当我们read/recv的时候,如果底层缓冲区有数据,read/recv会怎么办?-----拷贝
  3. 所以一次IO=等+数据拷贝
  4. 所以read、recv、write、send等都是在等io就绪,然后发起拷贝

那么什么叫做低效的IO呢?

单位时间,大部分时间这些io类的接口都在等数据!

那么如何提高IO的效率呢?

想办法在单位时间等的比重变低,那么IO的效率就高

2.阻塞IO是最常见的IO模型.

阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式

3.非阻塞IO

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用

4.信号驱动IO

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作

5.IO多路转接

虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态

6.异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

7.小结

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往 往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少

二、非阻塞IO

1.fcntl

一个文件描述符, 默认都是阻塞IO.

函数原型如下:

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

传入的cmd的值不同, 后面追加的参数也不相同. fcntl函数有5种功能

  • 复制一个现有的描述符(cmd=F_DUPFD).
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)
cpp 复制代码
#include <iostream>
#include <cstring>
#include <ctime>
#include <cassert>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>

#include <sys/time.h>

bool SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
    if (fl < 0)
        return false;
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
    return true;
}

int main()
{
   
     // 0
     SetNonBlock(0); //只要设置一次,后续就都是非阻塞了

     char buffer[1024];
     while (true)
     {
         sleep(1);
         errno = 0;
         // 非阻塞的时候,我们是以出错的形式返回,告知上层数据没有就绪:
         // a. 我们如何甄别是真的出错了
         // b. 还是仅仅是数据没有就绪呢?
         // 数据就绪了的话,我们就正常读取就行
         ssize_t s = read(0, buffer, sizeof(buffer) - 1); //出错,不仅仅是错误返回值,errno变量也会被设置,表明出错原因
         if (s > 0)
         {
             buffer[s-1] = 0;
             std::cout << "echo# " << buffer << " errno[---]: " << errno << " errstring: " << strerror(errno) << std::endl;
         }
         else
         {
             // 如果失败的errno值是11,就代表其实没错,只不过是底层数据没就绪
             //std::cout << "read \"error\" " << " errno: " << errno << " errstring: " << strerror(errno) << std::endl;

            if(errno == EWOULDBLOCK || errno == EAGAIN)
            {
              std::cout << "当前0号fd数据没有就绪, 请下一次再来试试吧" << std::endl;
                 continue;
            }
             else if(errno == EINTR)
             {
                 std::cout << "当前IO可能被信号中断,在试一试吧" << std::endl;
                 continue;
             }
             else
            {
                 //进行差错处理
            }
        }
    }
    return 0;

三、I/O多路转接之select

select其实就是帮助用户一次等待多个文件sock,当那些文件sock就绪了,只要通知用户,对应的sock文件就绪了,然后用户在调用如read、recv等接口进行数据读取!

1.快速认识select接口

cpp 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds,
 fd_set *exceptfds, struct timeval *timeout);
  • 参数nfds是需要监视的最大的文件描述符值+1;
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描 述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间
  • 返回值为就绪的fd的个数
  • 至少有一个fd数据就绪or空间就绪,此时就可以进行返回

参数timeout取值:

  • 获取当前时间的时间戳
  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回

2.fd_set

这是一个位图结构,分别表示文件描述符文件描述符集

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

cpp 复制代码
 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的全部位

3.接口挑一个重点参数,细致分析

假设我们今天是一个读文件描述符集

readfds:

在输入时:用户告诉内核我的比特位中,比特位的位置,表示文件描述符值,比特位的内容表示,是否关心,如0100,这个表示,我们关心3这个文件描述符,124,这三个文件描述符我们并不关心

在输出时:内核告诉用户,我是OS,用户你让我关心的多个fd有结果了,比特位的位置依旧表示文件描述符值。比特位的内容表示是否就绪,如0100,表示我们的3号文件描述符已经就绪了,如果是0000,则表示3号文件描述符还未就绪,就绪之后就代表,用户可以直接读取3号而不被阻塞

注意,用户和OS都会修改同一个位图结构,这个参数被用一次之后,就需要被重新设定!

4.编写代码

详情讲解看代码

cpp 复制代码
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__

#include <iostream>
#include <string>
#include <vector>
#include <sys/select.h>
#include <sys/time.h>
#include "Log.hpp"
#include "Sock.hpp"

#define BITS 8
#define NUM (sizeof(fd_set)*BITS)
#define FD_NONE -1

using namespace std;
// select 我们只完成读取,写入和异常不做处理 -- epoll(写完整)
class SelectServer
{
public:
    SelectServer(const uint16_t &port = 8080) : _port(port)
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        logMessage(DEBUG,"%s","create base socket success");
        for(int i = 0; i < NUM; i++) _fd_array[i] = FD_NONE;
        // 规定 : _fd_array[0] = _listensock;
        _fd_array[0] = _listensock;
    }

    void Start()
    {
        while (true)
        {
            // struct timeval timeout = {0, 0};
            // 如何看待listensock? 获取新连接,我们把它依旧看做成为IO,input事件,如果没有连接到来呢?阻塞
            // int sock = Sock::Accept(listensock, ...); //不能直接调用accept了
            // 将listensock添加到读文件描述符集中
            // FD_SET(_listensock, &rfds); 
            // int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);

            // 1. nfds: 随着我们获取的sock越来越多,随着我们添加到select的sock越来越多,注定了nfds每一次都可能要变化,我们需要对它动态计算
            // 2. rfds/writefds/exceptfds:都是输入输出型参数,输入输出不一定以一样的,所以注定了我们每一次都要对rfds进行重新添加
            // 3. timeout: 都是输入输出型参数,每一次都要进行重置,前提是你要的话
            // 1,2 => 注定了我们必须自己将合法的文件描述符需要单独全部保存起来 用来支持:1. 更新最大fd 2.更新位图结构

            DebugPrint();

            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = _listensock;
            for(int i = 0; i < NUM; i++)
            {
                if(_fd_array[i] == FD_NONE) continue;
                FD_SET(_fd_array[i], &rfds);
                if(maxfd < _fd_array[i]) maxfd = _fd_array[i];
            }
            // rfds未来,一定会有两类sock,listensock,普通sock
            // 我们select中,就绪的fd会越来越多!
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case 0:
                // printf("hello select ...\n");
                logMessage(DEBUG, "%s", "time out...");
                break;
            case -1:
                logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
                break;
            default:
                // 成功的
                logMessage(DEBUG, "get a new link event..."); // 为什么会一直打印连接到来呢?连接已经建立完成,就绪了,但是你没有取走,select要一直通知你!
                HandlerEvent(rfds);
                break;
            }
        }
    }

    ~SelectServer()
    {
        if (_listensock >= 0)
            close(_listensock);
    }
private:
    void HandlerEvent(const fd_set &rfds) // fd_set 是一个集合,里面可能会存在多个sock
    {
        for(int i = 0; i < NUM; i++)
        {
            // 1. 去掉不合法的fd
            if(_fd_array[i] == FD_NONE) continue;
            // 2. 合法的就一定就绪了?不一定
            if(FD_ISSET(_fd_array[i], &rfds))
            {
                //指定的fd,读事件就绪
                // 读事件就绪:连接时间到来,accept
                if(_fd_array[i] == _listensock) Accepter();
                else Recver(i);
            }
        }
    }
    void Accepter()
    {
        string clientip;
        uint16_t clientport = 0;
        // listensock上面的读事件就绪了,表示可以读取了
        // 获取新连接了
        int sock = Sock::Accept(_listensock, &clientip, &clientport); // 这里在进行accept会不会阻塞?不会!
        if(sock < 0)
        {
            logMessage(WARNING, "accept error");
            return;
        }
        logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
        // read / recv? 不能!为什么不能?我们不清楚该sock上面数据什么时候到来, recv、read就有可能先被阻塞,IO = 等+数据拷贝
        // 谁可能最清楚呢?select!
        // 得到新连接的时候,此时我们应该考虑的是,将新的sock托管给select,让select帮我们进行检测sock上是否有新的数据
        // 有了数据select,读事件就绪,select就会通知我,我们在进行读取,此时我们就不会被阻塞了
        // 要将sock添加 给 select, 其实我们只要将fd放入到数组中即可!
        int pos = 1;
        for(; pos < NUM; pos++){
            if(_fd_array[pos] == FD_NONE) break;
        }
        if(pos == NUM){
            logMessage(WARNING, "%s:%d", "select server already full,close: %d", sock);
            close(sock);
        }else{
            _fd_array[pos] = sock;
        }
    }
    void Recver(int pos)
    {
        // 读事件就绪:INPUT事件到来、recv,read
        logMessage(DEBUG, "message in, get IO event: %d", _fd_array[pos]);
        // 暂时先不做封装, 此时select已经帮我们进行了事件检测,fd上的数据一定是就绪的,即 本次 不会被阻塞
        // 这样读取有bug吗?有的,你怎么保证以读到了一个完整包文呢?
        char buffer[1024];
        int n = recv(_fd_array[pos], buffer, sizeof(buffer)-1, 0);
        if(n > 0){
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fd_array[pos], buffer);
        }
        else if(n == 0){
            logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[pos]);
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
        else{
            logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[pos], errno, strerror(errno));
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
    }

    void DebugPrint()
    {
        cout << "_fd_array[]: ";
        for(int i = 0; i < NUM; i++)
        {
            if(_fd_array[i] == FD_NONE) continue;
            cout << _fd_array[i] << " ";
        }
        cout << endl;
    }
private:
    uint16_t _port;
    int _listensock;
    int _fd_array[NUM];
    // int _fd_write[NUM];
    // std::vector<int> arr;
};

#endif

5.select的优缺点

优点:任何一个多路转接都具有

  • 效率高,在单位时间内,等的比重大大减小
  • 省资源,应用场景:有大量的连接,但是只有少量的活跃

缺点:

  • 为了维护第三方数组,服务器会充满大量的遍历,时间复杂度时On的,在OS底层也要关心fd,也要进行遍历
  • 每一次都要对select输出参数重新设定
  • 能够同时管理的fd个数是有上限的
  • 因为几乎每一个参数都是输入输出型的,select一定会频繁的进行用户到内核,内核到用户的参数数据拷贝
  • 编码复杂
相关推荐
ling-4517 分钟前
ifconfig 不显示 Linux 虚拟机常规网卡的 IP 地址
服务器·网络·php
菜鸟xy..38 分钟前
winhex软件简单讲解,虚拟磁盘分区介绍
linux·运维·服务器
网硕互联的小客服42 分钟前
如何排查服务器内存泄漏问题
linux·运维·服务器·安全·ssh
驰驰的老爸1 小时前
elk单机版安装
运维·jenkins
Evoxt 益沃斯1 小时前
How to enable Qemu Guest Agent for Virtual Machines
linux·运维·服务器·qemu
钟离墨笺1 小时前
【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(未成功版)
linux·服务器·网络
CVer儿1 小时前
ubuntu挂载固态硬盘
linux·运维·ubuntu
明达技术1 小时前
MR30分布式IO携手PLC实现手工作业产线自动化升级
运维·分布式·自动化
hang11.1 小时前
Web服务器配置
运维·服务器
地球空间-技术小鱼1 小时前
学习笔记-AMD CPU 命名
linux·服务器·人工智能·笔记·学习