栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

Linux 网络开发 I/O复用|Epoll|Select基本概念

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Linux 网络开发 I/O复用|Epoll|Select基本概念

游双的书以及网上的很多都不怎么说人话,这里做一个总结,主要针对于epoll、多路复用以及select问题做个归纳总结;

I/O多路复用的含义:

这个词以前在OS中见过,但是只是针对于通信DMA方式进行使用,并没有涉及网络通信;

首先明确一点,IO多路复用针对的是对于socket监听以处理上,衡量的是主机CPU是否有效利用;

对于最原始的socket处理情况,无外乎是:

  1. 创建socket并且绑定一个端口;
  2. listen监听;
  3. accept接受客户端的请求;
  4. 完成请求接受后,通过recv以及send来进行读写;

但是值得注意的是第四部,如果使用recv和send,会导致阻塞,直到读取成功和写入成功,无法再accept其他请求;

此时,如果有并发要求,即多个请求访问,该程序处于阻塞状态,无疑会拖慢时间;

因此,多线程处理应运而生;

当accept一个请求后,就开启一个新的线程进行recv或者send,并发处理,读写请求越多,所需要的线程就越多;

因此,采用上述方案无疑会增大内存空间,并且线程切换也会带来很大的开销;

例如:

// 伪代码描述
while(1) {
  // accept阻塞
  client_fd = accept(listen_fd)
  // 开启线程read数据(fd增多导致线程数增多)
  new Thread func() {
    // recv阻塞(多线程不影响上面的accept)
    if (recv(fd)) {
      // logic
    }
  }  
}

上述方法称之为同步阻塞;

同步说明请求和处理必须要同时进行,阻塞则说明接受和发送必须要阻塞进行;

对于接受和发送消息,可以采用非阻塞设定;

即recv和send不再采用阻塞等待形式,立即尝试发送和读取,如果没成功就报错;

所以也有同步非阻塞的操作,即多轮询尝试发送,每次即时判断,例如:

setNonblocking(listen_fd)
// 伪代码描述
while(1) {
  // accept非阻塞(cpu一直忙轮询)
  client_fd = accept(listen_fd)
  if (client_fd != null) {
    // 有人连接
    fds.append(client_fd)
  } else {
    // 无人连接
  }  
  for (fd in fds) {
    // recv非阻塞
    setNonblocking(client_fd)
    // recv 为非阻塞命令
    if (len = recv(fd) && len > 0) {
      // 有读写数据
      // logic
    } else {
       无读写数据
    }
  }  
}

上述例子可以看到,recv为非阻塞,但是需要对每一个fd来进行判断,看看是否有接收数据;

这样会大量的浪费CPU;

上述问题如果可以做以下优化,就会大有提升:

  1. 是否可以使用某种方式不管是否有无数据,数据来了直接通知接收方即可;
  2. 是否可以不用轮询;

因此,IO多路复用应运而生;

IO多路复用可以使用一个线程监视多个文件句柄(可以简单的理解成socket),如果某个文件句柄就绪(socket缓冲区接收到数据),就通知应用程序进行相应的读写操作。并且没有文件句柄就绪的时候就阻塞应用进程,交出CPU,让他去进行其他的进程;

这里的IO指的就是网络IO,复用则是针对于这个监视进程;

现今使用的epoll以及select都是基于IO复用思想进行实现的,即一个进程管理一切;

关于数据传输的一些前情提要:

这里很多人都没有把数据传输过程系统的结合在一起,看到了一些零散的东西,总结一下;

对于简单的同步阻塞而言,建立socket并且accept建立连接获得fd之后,就可以接收数据;

对于数据传输,有以下途径:

  1. 从链路到网卡;
  2. 数据到达网卡之后,会复制进内存;
  3. 网卡通过中断信号,告诉CPU有数据到达,需要CPU要进行相应的操作;
  4. CPU运行中断程序,讲数据写入socket对应的接受缓冲区内(通过端口和IP的组合寻找),并且唤醒阻塞等待的进程;

之所以能够唤醒等待的进程,主要是因为进程阻塞之后,就会把进程挂在在等待socket下的等待队列中;

等待队列在fd描述符的空间中,以指针形式指向;

当唤醒后,就会把该进程从等待队列取出,并且执行;

但是这是对于一个进程阻塞,挂在在socket对应的等待队列下,如何监控多个socket呢?

select方案:

select方案可以看作最古老的一版本,现在不再用了;

select管理进程会创建一个数组,里面装有所有想要监听的socket的fd(可以通过fd找到所需要的socket);

针对于前情提要的问题,这里就很清楚;

当进程监听多个socket的时候,就会把该进程加在所有的socket等待队列中;

按照之前的前提理论,当一个socket内数据到达,会产生CPU中断,运行中断程序,采用回调函数,唤醒进程;

但是由于数组保存,没有多余的额外信息,进程并不知道是哪个socket来数据;

此时,只能进行O(n)的遍历,找到来数据的socket,进行操作;

往复循环;

因此select的问题显而易见:

  1. 每次调用select都需要把进程加入到每个socket的等待队列,并且唤醒的时候都需要移除,相当于两次O(n)遍历;
  2. 每次都将整个fd数组传递给内核,开销时间很大;
  3. 每次唤醒后,都需要遍历一下fd数组,找到是哪个socket被激活;

缺点显而易见,所以如果可以有以下改进:

  1. 如果可以只知道哪些socket被激活就好了;
  2. 如果只传递被激活的fd就好了;

所以epoll应运而生;

epoll方案:

epoll可以看做select的加强版;

和简单的fd列表相比,epoll增加了eventpoll对象,其中有三个重要的数据结构:

  1. 红黑树;
  2. 双向就绪列表;
  3. eventpoll等待队列;

红黑树主要是用来存储socket集合,通过红黑树的平衡性质保证插入和释放时间等效;

当需要监听一个新的socket,采用epoll_ctl来进行添加,插入到红黑树内;

进程调用epoll_wait方法,如果就绪队列中有socket,直接读取,如果没有,则将进程阻塞,挂在道等待队列中;

和select不同的是,当某个socket传入数据或者处于就绪状态,中断程序会把对应的socket插入双向就绪列表;

因此,双向就绪列表本质上就存有处于可读写状态的socket;

此时,中断唤醒eventpoll等待队列的处理进程,该处理进程直接查双向就绪列表中的socket即可;

所以可以看到,通过引入eventpoll,相当于增加了一个中间层,来管理socket状态;

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/913754.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号