IO 多路复用

一、DMA 简介

      I/O 主要是磁盘的读写,网络的数据传输,音视频的输入输出等。

      倘若CPU要从磁盘中读取文件,我们可能会这样认为,首先CPU对磁盘进行直接进行读取,然后将读取到的数据放入到内存中,此时流程图如下所示:

在这里插入图片描述

      如果上述这种情况下,我们对CPU的分片或者不分片,效率应该是差不多的。但是实际上,操作系统选择的IO并不是这种方式,而是选择下面这种方式。

      CPU直接对DMA下达指令(指令中含有IO设备的信息,以及要读取文件的内容),然后DMA告知磁盘进行文件读取,在文件读取的过程中将数据加载到内存中,加载完毕后,磁盘会给硬盘一个反馈,DMA最终以中断的形式通知CPU,此时CPU再去内存中读取数据。

在这里插入图片描述

      我们可以看到,CPU 再给 DMA 发送一条指令后,便一直处于空闲状态了,在当前状态下,CPU可以去处理其他的请求。

      那么DMA可不可以被复用呢(也就是说一个线程在进行IO时,另一个线程是不是需要等待上一个线程IO结束)? 答案是可以被复用的(即不需要等待上一个线程IO结束,当前线程便可以进行IO读取),在CPU的总线有多条线路,而总线便是信息传输的通道。

二、I\O多路复用模型的引入

      考虑这样一种场景,我们要设计一个高性能的网络服务器,如果有大量的客户端进行连接,并且可以处理客户端的请求。
在这里插入图片描述
      通常情况下,我们可能选择为每个连接创建一个线程,由每一个线程去处理相应请求(记得学习 Socket 通信时,客户端给服务器上传图片时便采用的这种方式)。但是在有大量客户端进行连接的情况下,由于线程太多,造成频繁的CPU上下文切换,导致效率低下,所以此时我们只能选择单线程进行处理。

      如果选择单线程的话,不知道你有没有这样的困惑?即倘若服务器在处理客户端A的请求,此时客户端B的请求也来了,那么服务器是怎么处理客户端B的请求,会不会丢弃掉客户端B的请求?

      而在上面对DMA的介绍中,DMA 保证了在单线程的环境下,数据的不丢失

      在 Linux 系统中,一切皆文件。每一个网络连接在内核中都是以文件描述符(FD)的形式表示

      所以,单线程环境下,我们可以遍历文件描述符的集合,来进行客户端请求的处理,伪代码如下所示:

while(1){
    // 遍历文件描述符的集合
    for(fdx in fds){
        if(fdx 有数据){
         读 fdx
         请求处理    
        }
    }
}

三、I\O多路复用的具体实现

      I/O多路复用的具体实现有以下三种方式:select、 poll、 以及epoll。

select

// 创建 socket 服务器端
sockfd = socket(AF_INET,SOCK_STREAM,0);
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
listen(sockfd,5);

// 创建 5 个文件描述符
for(i=0;i<5;i++){
    memset(&client,0,sizeof(client))
    addrlen = sizeof(client);
    fds[i] = accept(sockfd,(struct sockaddr*)&client,&addrlen);
    if(fds[i] > max)
        // 求出文件描述符的最大值
        max = fds[i];
}

================================================================
  
while(1){
    FD_ZERO(&rset);
    for(i=0; i<5;i++){
        // rset 的类型是bitmap,用来表示哪一个文件描述符被启用(监听)
    
       
        // bitmap 默认是1024位,如果需要监听的位,则置1,否则置0
        // 假设文件描述符中存储的值为 1、2、5、7、9
        // 则bitmap 中存储的值为 0 1 1 0 0 1 0 1 0 1 0 0 0 ...
        FD_SET(fds[i],&rset);
    }
    
    puts("round again");
    
    // max+1 的作用是对 bitmap 进行截取,只需要前面的部分,后面的不需要判断,因为是0不需要监听
    // 第二个参数:读文件描述符集合
    // 第三个参数:写文件描述符集合
    // 第四个参数:异常文件描述符集合
    // 第五个参数: 超时时间    
    select(max+1,&rset,NULL,NULL,NULL);
    
    
    for(i=0;i<5;i++){
        // 判断哪一个 FD 被置 1 了
        if(FD_ISSET(fds[i],&rset)){
            memset(buffer,0,MAXBUF);
            // 读取数据
            read(fds[i],buffer,MAXBUF);
            // 进行处理
            purs(buffer);
        }
    }
}    

在这里插入图片描述
      select 函数,与我们开始的伪代码相比区别在于:判断是否有数据的行为由用户态变为了内核态

      select函数的缺点

  1. bitmap 默认为1024,虽然可以调整,但是依然由上限
  2. 每次循环都需要给 rset 赋初值,不可重用
FD_ZERO(&rset);
for(i=0; i<5;i++){
    FD_SET(fds[i],&rset);
}
  1. 从用户态拷贝到内核态,仍然需要一定开销
  2. select 返回时,并不能直接返回哪几个是有数据的,需要再遍历一篇。

      select函数的优点

  1. select 整体是在内核态进行运行
  2. 由内核监听 FD

poll

struct pollfd{
    int fd;
    // 监听的事件
    short events;
    // 对监听的事件回馈
    short revents;
}
for(i=0;i<5;i++){
    memset(&client,0,sizeof(client));
    addrlen = sizeof(client);
    
    // 直接将 fd 赋值给 pollfd 对象
    pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client,&addrlen);
    
    // 只在意读事件
    pollfds[i].events = POLLIN;
}
sleep(1);

================================================================

while1(1){
    puts("round again");
    
    // 第一个参数:传入一个 pollfd 数组
    // 第二个参数:数组长度
    // 第三个参数:超时时间
    poll(pollfds,5,50000);
    
    for(i=0;i<5;i++){
        
        // 如果 revents 被置位
        if(pollfds[i].revents & POLLIN){
            // 重新置0,这样便可重用
            pollfds[i].revents = 0;
            memset(buffer,0,MAXBUF);
            // 读取数据
            read(pollfds[i].fd,buffer,MAXBUF);
            // 处理请求
            puts(buffer);
        }
    }
}    

在这里插入图片描述

      poll解决了 select 的哪些缺点:

  1. 解决了 bitmap 1024 大小的限制,数组远远不止1024的大小
  2. 每次只需要恢复 revents 即可,而 select 则是需要恢复整个 reset

epoll

struct epoll_event events[5];

// epfd 相当于一个白板
int epfd = epoll_create(10);
...
...
for(i=0;i<5;i++){
    static struct epoll_event ev;
    memset(&client,0,sizeof(client));
    addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr *)&client,&addrlen);
    ev.events = EPOLLIN;
    
    // 相当于在白板上写字
    // 写入ev.data.fd以及监听的事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
}    
  
================================================================
    
while(1){
    puts("round again");
    nfds = epoll_wait(epfd,events,5,10000);
    
    fo(i=0;i<nfds;i++){
        memset(buffer,0,MAXBUF);
        read(events[i].data.fd,buffer,MAXBUF);
        puts(buffer);
    }
}    

在这里插入图片描述

在这里插入图片描述
      epoll解决了 select 的缺点:

  1. 解决了用户态拷贝到内核态的开销
  2. 判断哪一个fd有数据的时间复杂度由 O(n) 变为 O(1)
  • 2
    点赞
  • 7
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论

打赏作者

baburwang

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值