I/O多路复用技术

Posted niuyourou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O多路复用技术相关的知识,希望对你有一定的参考价值。

技术图片

 

  想要理解多路复用技术,首先要了解这个技术出现之前,我们面临的痛点是什么。

  以 JAVA 为例,我们想要写一个 TCP 服务端,接收客户端发来的数据,那么我们会这样写:

 while (true) {
                Socket socket = serverSocket.accept();
                //读取输入缓冲区数据
                //响应数据写入输出缓冲区
}    

  我们的线程阻塞在 socket 的 accept 方法上等待客户端请求的到来。一旦有客户端发起请求,建立一个新的 socket 连接并返回该 socket 对象,我们从本次连接的输入缓冲区读取客户端发来的数据,然后做出响应。响应后本次处理接触,进入下一次 while 循环,继续阻塞在 accept 方法上等待下一次连接的到来。

    这种方式的弊端显而易见,请求的处理是单线程的,我们必须处理完一次请求,才能建立下一次请求需要的连接,没有并发可言。为了解决这个问题,我们可以通过 fork/thread 模型的方式,将建立请求与请求处理分离开来,放在不同的线程中处理。

 while (true) {
                Socket socket = serverSocket.accept();
                Runnable readThread =
                        new Runnable() {
                           //接收请求数据
                           //返回响应
                        }.run();
}    

  每次来新的连接,主线程只负责建立连接并开启一条新的线程来处理这次连接,便可以去处理下一次请求。与第一种方式相比,总算有了点并发能力。但问题还是存在的,那就是每次有新的连接,我们都需要为其分配专门的处理线程,代价非常大。在客户端连接非常多的情况下,我们将会开启非常多的线程来处理这些连接,给系统造成巨大的负担。当然为了防止服务将操作系统搞崩,我们可以将创建新的线程改为将任务提交到线程池,将开启的线程数限制在一个安全的范围内。但这仅仅是保证了系统的安全,并没有增加服务端的并发能力。需要注意的是,以上两种方式虽然使用 JAVA 编写,但都是使用的最基础的 soket 操作,并没有涉及到我们所说的多路复用。

  对于第二种方式来说,主线程开启的处理客户端请求的线程在做两件事:
  1. 阻塞的等待客户端数据的到来。

  2. 回复客户端。

  在阻塞的等待客户端到来上,任务线程一直是处于阻塞状态的,通常情况下任务线程最多的时间也是消耗在了这上面。解决其存在的线程数过多限制了系统的并发能力的问题,我们可以从这点入手。当数据准备就绪时再开启线程处理,而不是连接到来时便开启一个线程傻傻的阻塞在那里等待数据的到来。

  1983年,随着计算机网络的成型,越来越多的人开始使用网络,服务器的并发数开始增加。人们终于意识到了这种问题,所以发明了一种叫做「IO多路复用」的模型,这种模型的好处就是「没必要开那么多条线程和进程了」,一个线程一个进程就搞定了。这便是最初的多路复用技术,select 函数的发明。

  使用 select 函数我们可以只使用一个线程来监视所有有效的 socket 连接,当有数据就绪的事件发生时再开启新线程去处理,处理完后线程销毁或放回线程池,并再次将线程放入监视队列。这样不需要将线程与连接绑定,线程只负责处理任务,我们需要维持的线程数将大大减少。

  select 函数的核心在于,主线程需要不断的轮询检查其监视的 socket ,判断每个 socket 是否有事件发生。

  至于 socket 的状态的变更,不同的操作系统有不同的实现。在未使用多路复用技术时,网卡的数据准备就绪会发送中断,OS 通过中断处理程序将数据从网卡拷贝到内核的输入缓冲区,这个拷贝过程如果由DMA 或者通道完成,在数据拷贝完成时 DMA 或 通道 也会向内核发送中断,OS通过中断处理程序将阻塞在这些数据上的线程置为就绪状态。

  OS 可以通过中断处理程序改变线程的状态,也可以做其它事情,比如检查内核空间中某个数据结构确定是否有 select 方法在监视这个缓冲区,如果有则将缓冲区状态映射为事件并同步到用户空间,同时将阻塞在该 select 方法的线程唤醒。这里只是举个例子,具体实现不做深入探讨,但可以确定的是,处理 socket 数据准备就绪时中断的中断处理函数承担了更新事件状态的任务。

  select 函数做到了事件驱动,也就是多路复用,但还存在着一些缺点。比如因为需要一个数组来存储监视的 socket 的引用,其可以监视的 socket 的数量有限。这一点后来的 poll 函数做了改进,将数组结构改为链表结构,使其可以监视的 socket 的数量大大增加。另外一个缺点便是每次有事件发生, select 都需要遍历所有监视的 socket 判断是哪个 soket 发生了事件,在监视的 socket 较多时其效率很低,时间复杂度 O(N)。但总的来说,在当时的并发量下,select 函数的性能已经足够人们使用了。

  直到2002年,互联网时代爆炸,数以千万计的请求在全世界范围内发来发去,服务器大爆炸,人们通过改进「IO多路复用」模型,进一步的优化,发明了一个叫做epoll的方法。epoll 除了维护监视的 socket 的列表,还维护了当前发生事件的 socket 的列表,虽然这样只是给中断处理程序增加了一点小任务:将当前 socket 的引用加入 ready 列表中。但每次进行事件处理时,epoll 只需要遍历 ready 列表即可,相较 select 的全部遍历,并发能力又得到了进一步提升。我们来看一个震撼人心的并发图:

技术图片

  可以看到,epoll 的性能是一条平稳的直线,其性能几乎不受并发数的影响,无敌的并发。

  

  

  

以上是关于I/O多路复用技术的主要内容,如果未能解决你的问题,请参考以下文章

Redis 和 I/O 多路复用

什么是Redis I/O 多路复用?

I/O--多路复用的三种机制Select,Poll和Epoll对比

LinuxI/O多路复用

LinuxI/O多路复用

I/O多路复用技术