• 【Linux】IO操作


    典型 IO 模型

    IO 操作指的就是数据的输入输出操作;IO 过程可以分为两个步骤:等待 IO 就绪、数据拷贝

    阻塞 IO

    发起 IO 操作,若当前不具备 IO 条件,则等待直到条件满足完成 IO 后返回

    在这里插入图片描述
    优点:流程简单
    缺点:资源利用率低,效率相对低下

    非阻塞 IO

    发起 IO 操作,若当前能够 IO ,则执行完 IO 操作后返回;若当前不具备 IO 条件则报错返回

    在这里插入图片描述
    优点:对资源利用率提高了
    缺点:程序流程复杂–因为要进行循环操作;不是实时的

    信号驱动 IO

    定义 IO 信号处理方式,IO 就绪通过信号通知进程,然后发起 IO 调用

    在这里插入图片描述
    优点:对资源利用率更充分;IO 操作更实时
    缺点:操作流程更加复杂–添加信号通知部分

    异步 IO

    发起异步 IO 操作,IO 的等待以及数据的拷贝都由系统完成,完成后通知进程

    在这里插入图片描述
    优点:对资源利用率提升;
    确定:程序流程也更复杂了

    常见问题

    阻塞 vs 非阻塞
    阻塞:发起一个操作,若当前操作条件不满足则一直等待
    非阻塞:发起一个操作,若当前操作条件不满足则报错返回

    阻塞与非阻塞关联:通常都是操作接口特性
    阻塞与非阻塞区别:发起一个接口调用后,接口是否会立即返回

    同步 vs 异步
    同步:功能由进程自身来完成,且通常是串行化的
    异步:功能并不由进程自身来完成,而是由系统完成的,完成不一定是串行的

    同步与异步关联:通常用于讨论一个任务的完成流程
    同步与异步区别:功能是否有当前执行流自身完成

    异步阻塞:发起操作后, 功能由系统来完成,进程执行流自身等待系统完成
    异步非阻塞:发起操作后,功能由系统来完成,操作会直接返回,并不会等待

    多路转接模型

    常用于高并发服务器中技术的使用
    作用:针对大量描述符进行 IO 就绪事件监控

    优点:
    1:让进程能够仅针对就绪的描述符进行 IO 操作,提高了任务处理效率
    2:避免进程因为未就绪描述符进行操作而导致阻塞

    具体技术实现:select、poll、epoll

    select 模型

    IO 事件:可读事件、可写事件、异常事件

    流程思想:
    1:定义指定 IO 事件的描述符集合;

    2:将需要对指定事件进行监控的描述符添加到指定集合中;

    3:将事件的描述符集合拷贝到内核中,进行事件监控:
    1)对集合中所有描述符进行遍历,若没有就绪则将描述符挂到内核的 IO 事件队列;
    2)若监控过程中,有某个描述符就绪了所要监控的事件,则会唤醒进程的阻塞;
    3)唤醒后,select 会再次遍历描述符集合,将集合中没有就绪的描述符移除

    4:select 监控返回后,只需要判断哪个描述符还在集合中,哪个描述符就就绪了哪个事件;

    5:进程可以根据就绪的不同事件对描述符进行不同的 IO 操作

    接口介绍

    1、定义集合:
    fd_set set;

    本质上这个集合是一个比特位图,默认拥有 1024 个比特位,取决于 __FD_SETSIZE;因此,select 对描述符进行 IO 事件监控是有最大描述符限制的

    2、先初始化集合,然后将需要监控的描述符添加到集合中

    初始化清空集合:
    void FD_ZERO(fd_set *set);

    将 fd 描述符添加到集合中:
    void FD_SET(int fd, fd_set *set);

    将 fd 描述符从 set 集合移除:
    void FD_CLR(int fd, fd_set *set);

    3、开始监控:
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

    nfds:将所有需要监控的集合中,最大描述符+1,提高监控遍历效率
    readfds:可读事件的描述符集合
    writefds:可写事件的描述符集合
    exceptfds:异常事件的描述符集合
    timeout:设置本次监控的阻塞时长; NULL-一直阻塞,直到描述符就绪或被信号打断; 0-非阻塞
    返回值:返回实际就绪的描述符事件个数;出错返回 -1;0-监控超时

    select 接口一旦返回,就意味着三个集合中,就只保留了就绪了指定事件的描述符

    4、判断哪个描述符还在集合中,哪个描述符就绪了哪个事件
    int FD_ISSET(int fd, fd_set *set);
    返回值:非0-描述符还在集合中;0-描述符不在集合中

    Demo:

      1 #include<stdio.h>
      2 #include<unistd.h>
      3 #include<stdlib.h>
      4 #include<time.h>
      5 #include<sys/select.h>
      6 
      7 /*对标准输入进行可读事件监控,有数据则读取,没有数据则阻塞*/
      8 
      9 int main()
     10 {
     11   fd_set rfds;    //定义可读事件集合
     12   while(1){
     13      int maxfd=0;    //集合中最大描述符个数 --- 因为只对标准输入进行可读事件监控,因此最大描述符 1 个
     14      struct timeval tv;    //定义时间结构体
     15      tv.tv_sec=3;        //阻塞 3s;因为 select每次监控都会重置阻塞时间为0,所有每次循环都需要重新设置
     16      tv.tv_usec=0;                                                                                                      
     17      
     18      FD_ZERO(&rfds);    //初始化集合        因为select每次监控都会重置描述符集合,因此每次循环都需要重新添加描述符到集合中
     19      FD_SET(0,&rfds); //将标准输入-0,添加到可读集合
     20      
     21      int nfds=select(maxfd+1,&rfds,NULL,NULL,&tv);   //开始监控,只有可读集合 rfds 
     22      if(nfds<0){        //监控失败
     23         perror("select error~!\n");
     24         return -1;
     25      }else if(nfds==0){        //返回值为0,表示没有描述符存在
     26         perror("select timeout!\n");        //等待超时
     27         continue;
     28      }
     29         
     30      //for 循环遍历集合
     31      for(int i=0;i<=maxfd;++i){
     32          if(FD_ISSET(i,&rfds)!=0){
     33              //判断 i 号描述符在可读集合中,说明就绪了可读事件
     34              char buf[1024]={0};
     35              read(i,buf,1024);          //从 i 号描述符中读取数据放入 buf
     36              printf("buf:[%s]\n",buf);
     37          }   
     38          //else if(FD_ISSET(i,&efds))  {
     39                //判断 i 号描述符在可写事件中,说明就绪了可写事件
     40          //} 
     41      }   
     42   }            
     43   return 0;
     44 }    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    运行结果:
    在这里插入图片描述

    将 select 应用在 TCP 服务器搭建上

    搭建一个 TCP 服务器,会涉及到服务器为每一个客户端都建立一个新的套接字进行通信,需要对大量的描述符进行 IO 操作;之前介绍 TCP 服务器时候的解决方案是:使用多执行流来处理 – 为每一个客户端的通讯都创建执行流

    这里,我们可以使用 多路转接模型+线程池 进行应用

    思想:
    封装一个 Select 类,每一个实例化的对象,都是一个能够针对大量描述符进行 IO 事件监控的对象 添加链接描述

    在这里插入图片描述

    封装 TCP:添加链接描述

    客户端代码:添加链接描述

    服务端代码:添加链接描述

    在这里插入图片描述

    select 总结:
    优点:遵循 posix 标准,跨平台移植性好; 监控超时可以细微到微妙
    缺点:
    1.能够监控的描述符数量是有上线限制的 — 取决于 _FD_SETSIZE,默认1024;
    2.监控过程中需要多次遍历描述符集合,因此监控的描述符越多,性能就越低;
    3.因为每次监控都会修改描述符集合,因此每次监控都需要重新条件描述符到集合中;
    4.监控返回的是就绪的描述符集合(位图),因此监控调用返回后,无法直接针对就绪的描述符进行操作,需要遍历一遍描述符看哪个还在集合中才能确定是否就绪了事件

    高并发服务器中,有一种并发模型 :reactor 模型

    思想:使用多路转接模型对大量描述符进行事件监控,谁触发了事件就处理谁

    分类:
    1.单 reactor 单线程 :在一个线程中,进行事件监控以及事件处理;
    2.单 reactor 多线程 :在一个线程中进行 reactor 事件监控,触发事件后交给其他线程进行事件处理;
    3.多 reactor 多线程 :在一个线程中进行新连接到来事件监控,有事件触发则获取新建连接,将新建连接分发给其他 reactor 线程,其他的 reactor 进行描述符的事件监控以及 IO 操作

    在这里插入图片描述

    poll 模型

    操作流程:

    1.定义一个事件结构体数组

    struct poollfd{
    	int fd;     //要监控的文件描述符
    	short events;    //想要监控的事件,POLLIN-可读,POLLOUT-可写
    	short revents;   //监控返回后,存储实际就绪的事件
    }
    
    //定义事件结构体数组
    struct pollfd fds[MAX];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.若哪个描述符需要监控什么事件,就在数组中进行设置

    fds[0].fd=0;
    fds[0].events=POLLIN;      //对标准输入描述符进行可读事件监控
    
    • 1
    • 2
    fds[1].fd=1;
    fds[1].events=POLLOUT;  //对标准输出描述符进行可写事件监控
    
    • 1
    • 2

    3.开始监控
    原理:将数组中有效数据拷贝到内核中,进行多次轮询遍历

    第一次遍历:判断有没有就绪的事件,没有则挂起到监控队列中;
    第二次遍历:进程的阻塞被唤醒后进行遍历,对每个元素的 revents 设置实际就绪的事件

    int poll(struct pollfd*fds,nfds_t maxevents,int timeout);
    
    fds:定义的时间结构体数组首地址
    maxevents:数组中有效元素个数
    timeout:监控阻塞的超时时间,以毫秒为单位
    
    返回值:>0 表示实际就绪的事件个数; ==0 表示超时; <0 表示出错了
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.调用返回后,遍历事件结构体数组,根据 revents 成员确定描述符是否就绪了某个事件,进而对描述符进行操作

    Demon:

    
    #include
    #include
    #include
    #include
    
    #define MAX_POLL_SIZE 10
    
    int main()
    {
      //定义事件结构体数组
      struct pollfd fds[MAX_POLL_SIZE];
    
      //添加要监控的描述符事件信息
      fds[0].fd=0;      //标准输入描述符
      fds[0].events=POLLIN;  //监控可读事件
    
      while(1){
        int ret=poll(fds,1,3000);     //超时时间为 3000 毫秒 --- 3s
        if(ret<0){
          perror("poll error!\n");
          continue;
        }
        else if(ret==0){
          printf("poll timeout!\n");
          continue;
        }
        int i=0,valid_count=1;       //只监控可读事件,因此有效监控个数 valid_count=1
        for(i=0;i<valid_count;++i){
          if(fds[i].revents & POLLIN){    //就绪可读事件
             char buf[1024]={0};
             read(fds[i].fd,buf,1023);     //从 fds[i].fd 描述符中读取数据到 buf 中
             printf("buf:[%s]\n",buf);
          }else if(fds[i].revents & POLLOUT)  {
           //就绪可写事件
             printf("POLLOUT EVENTS!\n");  
          }
        }
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    运行结果:

    在这里插入图片描述

    poll 总结:

    优点:
    1.使用事件结构体替代了事件集合,相较于 select 操作,简便性提高了很多;
    2.所能监控的描述符数量不在上限限制;

    缺点:
    1.每次监控需要将信息拷贝到内核;
    2.监控原理涉及到多次对事件数组的遍历,因此性能会随着描述符的增多而下降;
    3.每次监控完毕后,依然需要遍历整个事件数组才能确定哪个描述符就绪了哪个事件

    events = POLLIN | POLLOUT; 对两个事件同时监控采用 |

    epoll 模型

    操作流程:
    1.在内核中创建 epoll 句柄 eventpoll 结构

    int epoll_create(int size);
    
    size: 所能监控的描述符上限 ,在Linux 2.6.8 后被忽略,但必须大于 0
    返回值:返回 epoll 描述符;出错返回 -1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    struct eventpoll{
    	...
    	list_head rdllist;   //双向链表
    	rbtree rbr;     //红黑树
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.向内核的句柄中,添加/移除/修改所要监控的描述符及其对应的事件结构

    int epoll_ctl(int epfd,int op,int fd,struct epoll_event* ev);
    
    epfd: epoll_create 返回的 epoll 描述符
    op: 对 epoll 要进行的操作:EPOLL_CTL_ADD / EPOLL_CTL_DEL / EPOLL_CTL_MOD
    fd: 要操作的描述符,对 fd 描述符进行 op 操作
    
    struck epoll_event* ev;   //对描述符要进行操作的详细信息
    
    
    struct epoll_event{
    	uint32_t events;  //想要监控的事件以及监控后存放实际就绪的事件
    	union{        //可以监控的事件 : EPOLLIN-可读,EPOLLOUT-可写
          void* ptr;
          int fd; 
        }data;        //额外信息
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.开始监控

    epoll 的监控是一个异步阻塞操作

    发起监控调用是为了告诉系统,可以开始监控了,监控由系统完成 (而系统内部为 epoll 的每个描述的就绪事件挂了一个回调函数)
    回调函数功能:描述符一旦就绪了指定事件,将事件信息拷贝一份到 rdllist 中,其实 rdllist 双向链表的作用:存放就绪的描述符对应的事件结构

    一旦系统监控有描述符就绪了,则唤醒进程的阻塞,进程一旦被唤醒,查看 rellist 双向链表中是否有数据就可以确定是否有描述符就绪

    监控调用返回的数据就是一个事件结构体数组 – 就绪的描述符对应的事件

    int epoll_wait(int epfd,struct epoll_event* evs,int maxevents,int timeout);
    
    epfd: epoll 描述符
    evs: epoll_event 结构体数组的空间首地址,接收就绪事件
    maxevents: 数组的最大元素个数,也表示了当前想要获取的最大事件个数
    timeout: 要设置的监控超时时间 -- 以毫秒为单位
    返回值: >0 实际就绪的事件个数;==0 超时 ; <0 出错了
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    封装一个 Epoll 类

    在这里插入图片描述
    将封装的 epoll 应用于 TCP 通信的操作与 select 应用相同,感兴趣可以自己琢磨琢磨,这里就不放代码咯~

    epoll 事件触发方式

    水平触发:select 与 poll 只有水平触发,epoll 默认水平触发
    可读:缓冲区中数据大小小于高水平标记 (默认1字节)就会触发可读事件
    可写:缓冲区中剩余空间大小小于高水平标记,就会触发可写事件
    思想:只要满足触发条件就会触发对应事件

    边缘触发: EPOLLET
    可读:每当套接字有新数据到来时,则会触发一次事件
    可写:缓冲区剩余空间从无到有的时候,才会触发一次事件
    思想:尽量让用户在一次事件触发中,将能处理的数据都处理完毕,尽量减少事件触发次数,减少运行态切换次数

    因为有新数据到来才触发一次事件,因此若一次事件触发后的处理中没有将所有数据进行处理,则在下一次新数据到来前,这些剩余数据都得不到处理

    场景:http请求接收 – 在一次请求的接收处理中,发现缓冲区中数据不足以进行一次处理,取出来则需要额外存储,不取出来则水平触发就会一直触发可读事件,这种情况下希望能够在有新数据到来时再去进行数据处理,则使用边缘触发。

    Q:如何将数据在一次处理中全部取出进行处理?

    若想要将缓冲区中数据全部取出就只能循环取出
    但循环读取数据,在套接字 recv 时候,有可能因为 socket 没有数据而阻塞

    解决:将套接字的阻塞属性设置为非阻塞
    将属性进行设置之后,则套接字的所有操作将变为非阻塞操作

    int fcntl(int fd,int op,int arg);
    
    op:F_GETFL 获取文件访问属性以及状态标志
        F_SETFL 设置文件的访问属性或状态标志 -- O_NONBLOCK--非阻塞
    
    
    int flag=fcntl(fd,F_GETFL,0);  //F_GETFL 获取文件原有属性--第三个参数被忽略
    fcntl(fd,F_SETFL,flag|O_NONBLOCK); //在原有属性基础上添加非阻塞属性
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    epoll 总结:

    优点:
    1.所能监控的描述符没有数量上限
    2.监控性能并不会随着描述符的增多而下降
    3.直接返回就绪描述符对应的事件结构,减少外界空遍历
    4.描述符监控的事件信息,只需要向内核中添加一次,不需要每次监控都添加

    缺点:
    跨平台移植性不好,只能在类unix平台下使用

    Q:select 、poll、epoll 哪个好?

    不管是哪种模型,多路转接模型针对的都是对大量描述符进行 IO 事件监控,但是同一时间少量活跃的场景
    若活跃连接也较多,则一定要搭配多执行流进行处理,充分利用系统资源

    相较之下,select & poll 比较适用于单个描述符的事件监控以及超时管理,而 epoll 适用于大量描述符的事件监控场景。

    在这里插入图片描述

  • 相关阅读:
    [manjaro]更新后内核文件加载失败
    QPS\TPS指的是什么?怎样测试一个接口得QPS
    软件工程-第4章结构化编码和测试
    智能化“竞赛下半场”,什么才是可量产的最佳实践?
    ES6中对象的扩展
    第二十六篇 组件通信 - 事件中心bus
    【SA8295P 源码分析 (三)】125 - MAX96712 解串器 start_stream、stop_stream 寄存器配置 过程详细解析
    iperf 工具使用总结
    分布式模式之Broker模式
    Linux60个小时速成
  • 原文地址:https://blog.csdn.net/weixin_46655027/article/details/133215525