为您推荐:
精华内容
最热下载
问答
  • 5星
    3KB weixin_44410704 2021-06-07 11:35:12
  • 5星
    108.34MB guoruibin123 2021-04-27 06:03:44
  • 5星
    3.16MB Johnho130 2021-06-30 11:30:26
  • 5星
    143KB z1332291552 2021-06-21 17:38:29
  • 所谓的redis多路复用原理 他是把IO操作再细分成多个事件去处理 比如IO涉及连接 读取 输入 把这三种当成三种事件分别起三个线程处理 如果一个连接来了显示被读取线程处理了,然后再执行写入,那么之前的读取...

    首先你得了解redis是单线程的,然后你接着会有个疑问,单线程怎么会有高性能呢(据悉,在普通的笔记本上redis吞吐量亦能达到每秒几十W次),带着疑问看看下面转载的帖子吧。

    先个人总结:
    所谓的redis的多路复用原理
    他是把IO操作再细分成多个事件去处理
    比如IO涉及连接 读取  输入
    把这三种当成三种事件分别起三个线程处理
    如果一个连接来了显示被读取线程处理了,然后再执行写入,那么之前的读取就可以被后面的请求复用,吞吐量就提高了

    原文:https://www.cnblogs.com/doit8791/p/7461479.html

    高性能IO之Reactor模式

    讲到高性能IO绕不开Reactor模式,它是大多数IO相关组件如Netty、Redis在使用的IO模式,为什么需要这种模式,它是如何设计来解决高性能并发的呢?

    最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新的套接字连接,如果有,那么就调用一个处理函数处理,类似:
    while(true){
    socket = accept();
    handle(socket)
    }
    这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。
    之后,想到了使用多线程,也就是很经典的connection per thread,每一个连接用一个线程处理,类似:
    while(true){
    socket = accept();
    new thread(socket);
    }
    tomcat服务器的早期版本确实是这样实现的。多线程的方式确实一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线程只能对应一个socket”的原因。最开始对这句话很不理解,线程中创建多个socket不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个socket被阻塞了,后面的是无法被执行到的。
    缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数太高,系统无法承受,而且,线程的反复创建-销毁也需要代价。
    线程池本身可以缓解线程创建-销毁的代价,这样优化确实会好很多,不过还是存在一些问题的,就是线程的粒度太大。每一个线程把一次交互的事情全部做了,包括读取和返回,甚至连接,表面上似乎连接不在线程里,但是如果线程不够,有了新的连接,也无法得到处理,所以,目前的方案线程里可以看成要做三件事,连接,读取和写入。
    线程同步的粒度太大了,限制了吞吐量。应该把一次连接的操作分为更细的粒度或者过程,这些更细的粒度是更小的线程。整个线程池的数目会翻倍,但是线程更简单,任务更加单一。这其实就是Reactor出现的原因,在Reactor中,这些被拆分的小线程或者子过程对应的是handler,每一种handler会出处理一种event。这里会有一个全局的管理者selector,我们需要把channel注册感兴趣的事件,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么主线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。典型的事件有连接,读取和写入,当然我们就需要为这些事件分别提供处理器,每一个处理器可以采用线程的方式实现。一个连接来了,显示被读取线程或者handler处理了,然后再执行写入,那么之前的读取就可以被后面的请求复用,吞吐量就提高了。

    【Java】Reactor模式

    几乎所有的网络连接都会经过读请求内容——》解码——》计算处理——》编码回复——》回复的过程,Reactor模式的的演化过程如下:

    这种模型由于IO在阻塞时会一直等待,因此在用户负载增加时,性能下降的非常快。

    server导致阻塞的原因:

    1、serversocket的accept方法,阻塞等待client连接,直到client连接成功。

    2、线程从socket inputstream读入数据,会进入阻塞状态,直到全部数据读完。

    3、线程向socket outputstream写入数据,会阻塞直到全部数据写完。

     

    改进:采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行数据处理。

    Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的Handler去处理。

    Handler:负责处理非阻塞的行为,标识系统管理的资源;同时将handler与事件绑定。

    Reactor为单个线程,需要处理accept连接,同时发送请求到处理器中。

    由于只有单个线程,所以处理器中的业务需要能够快速处理完。

    改进:使用多线程处理业务逻辑。

    将处理器的执行放入线程池,多线程进行业务处理。但Reactor仍为单个线程。

    继续改进:对于多个CPU的机器,为充分利用系统资源,将Reactor拆分为两部分。

    Using Multiple Reactors

    mainReactor负责监听连接,accept连接给subReactor处理,为什么要单独分一个Reactor来处理监听呢?因为像TCP这样需要经过3次握手才能建立连接,这个建立连接的过程也是要耗时间和资源的,单独分一个Reactor来处理,可以提高性能。

    《Scalable IO in Java》笔记

     Reactor模式是什么,有哪些优缺点?

    Wikipedia上说:“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。从这个描述中,我们知道Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。如果用图来表达:

    从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理。

    Reactor模式结构

    在解决了什么是Reactor模式后,我们来看看Reactor模式是由什么模块构成。图是一种比较简洁形象的表现方式,因而先上一张图来表达各个模块的名称和他们之间的关系:

    Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接(Connection,在Java NIO中的Channel)。这个Channel注册到Synchronous Event Demultiplexer中,以监听Handle中发生的事件,对ServerSocketChannnel可以是CONNECT事件,对SocketChannel可以是READ、WRITE、CLOSE事件等。
    Synchronous Event Demultiplexer:阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。在Java NIO中用Selector来封装,当Selector.select()返回时,可以调用Selector的selectedKeys()方法获取Set<SelectionKey>,一个SelectionKey表达一个有事件发生的Channel以及该Channel上的事件类型。上图的“Synchronous Event Demultiplexer ---notifies--> Handle”的流程如果是对的,那内部实现应该是select()方法在事件到来后会先设置Handle的状态,然后返回。不了解内部实现机制,因而保留原图。
    Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。
    Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。
    Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。

    Reactor模式详解

    优点

    1)响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
    2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
    3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
    4)可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;
    缺点

    1)相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
    2)Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。
    3) Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式。

     

    java高级视频课程资源分享,有需求的添加下方楼主微信

    展开全文
    qq_32077121 2019-06-23 12:25:27
  • 了解Redis底层关于IO多路复用的epoll实现原理前,先介绍关于IO模型,内存与磁盘交互方式、同步IO、异步IO,有助于对多路复用更好的理解。 用户空间与内核空间 User space 是用户程序的运行空间,Kernel space 是...

    前言

    • 了解Redis底层关于IO多路复用的epoll实现原理前,先介绍关于IO模型,内存与磁盘交互方式、同步IO、异步IO,有助于对多路复用更好的理解。

    用户空间与内核空间

    • User space 是用户程序的运行空间,Kernel space 是Linux内核运行的空间。
    • 虚拟内存被 操作系统划分为两块: 内核空间用户空间,内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态。
      在这里插入图片描述
    • Kernel space可以执行任意命令,调用系统的一切资源User space只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发起指令。
    • 通过系统接口,进程可以从用户空间切换到内核空间
    	str= "my zhangsan" //用户空间
    	x+=2;
    	file.write(str); //切换到内核空间
    	y=x+3;//切换会用户空间
    
    • 上面代码中,简单的赋值运算,在User space执行。第三行需要写入文件,就要切换到Kernel spacce,因为用户不能直接写文件,必须通过内核空间 。
    • 查看CPU时间在 User space 与Kernel Space之间的分配情况,可以使用top命令,他的第三行输出就是CPU时间分配统计。
      在这里插入图片描述
    • 这一行8项统计指标
      在这里插入图片描述
      • 第一项24.8 us (user 的缩写)就是CPU消耗在 User space 的时间百分比,第二项 0.5 sy (system的缩写)是消耗在 Kernel space的时间百分比。

    PIO 和 DMA

    PIO

    • 有必要简单说说慢速 I/O设备内存之间的数据传输方式
    • PIO 用磁盘举例,很早之前,磁盘和内存之间的数据传输是需要CPU控制的,意味着读取磁盘文件到内存中,数据要经过CPU存储转发,这种方式成为PIO 。显然,这种方式不合理,需要占用大量的时间读取文件,造成文件访问系统几乎停滞响应。

    DMA

    • DMA, DMA (直接内存访问,Direct Memory Access)取代了PIO,他可以不经过CPU而直接进行磁盘和内存(内核空间)的数据交换 。在DMA模式下,CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传送即可,DMA控制器通过系统总线来传输数据,传送完毕通知CPU,这样可以很大程度上降低CPU占有率,大大节省了系统资源

    缓存I/O和直接I/O

    • 缓存IO : 数据从磁盘先通过 DMA copy到内核空间,在从内核空间通过cpu copy到用户空间。
      • 缓存I/O 被称为标准I/O,大多数文件系统的默认I/O 操作都是缓存I/O,在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区然后从内核空间缓冲区复制到应用程序的地址空间

    缓存I/O的读写操作

    • 读操作
      • 操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回; 否则从磁盘读取,然后缓存在操作系统的缓存中。
    • 写操作
      • 将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候在写到磁盘中有操作系统决定,除非显示地调用了sync同步命令。

    缓存I/O的优点

    • 在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全
    • 可以减少读磁盘的次数,从而提高性能

    缓存I/O的缺点

    • 在缓存I/O 机制中,DMA方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作带来的CPU以及内存开销是非常大的。
    • 直接IO : 数据从磁盘通过DMA copy到用户空间
      • 直接I/O应用程序直接访问磁盘数据,而不经过内核缓冲区,也就是绕过内核缓冲区,自己管理I/O缓冲区,这样的好处减少一次从内核缓冲区到用户程序的数据拷贝。

    直接I/O的优点

    • 对于一些复杂的应用,比如数据库服务器,他们为了充分提高性能,希望绕过内核缓冲区,由自己在用户态空间实现并管理I/O缓冲区,包括缓存机制和写延迟机制,以支持独特的查询机制。绕过内核缓冲区可以减少系统内存的开销,因为内核缓冲区本身就在使用系统内存
      在这里插入图片描述

    IO的访问方式

    磁盘IO

    在这里插入图片描述

    • 应用程序调用read接口时,操作系统检查在内核的高速缓存有没有需要的数据,如果已经缓存了,那么就从缓冲返回,如果没有,则从磁盘中读取,然后缓存在操作系统的缓存中
    • 应用程序调用write接口时,将数据从用户地址空间复制到内核地址空间的缓存中,这时对用户程序来说,写操作已经完成,至于什么时候写到磁盘中,有操作系统决定,除非显示调用了sync同步命令。
      在这里插入图片描述

    网络IO

    • 操作系统将数据从磁盘复制到操作系统内核的页缓存中
    • 应用将数据从内核缓存复制到应用的缓存中
    • 应用将数据写回内核的Socket缓存中
    • 操作系统将数据从Socket缓存区复制到网卡缓存,然后将其通过网络发出。
      在这里插入图片描述
    • 1、当发生read系统调用时,通过DMA(Direct Memory Access)将数据copy到内核模式
    • 2、然后由CPU控制将内核模式数据copy到用户模式下 的buffer
    • 3、read调用完成后,write调用首先将用户模式下buffer中的数据copy到内核模式下的socket buffer中
    • 4、最后通过DMA copy将内核模式下的socket buffer中的数据copy到网卡设备中传送。
    • 总结: 从上面的过程可以看出,数据白白从内核模式到用户模式走了一圈,浪费了两次copy,而这两次copy都是cpu copy,即占用CPU资源

    磁盘IO和网络IO对比

    • 磁盘IO延时是由(以15000rpm的硬盘为例),机械转动延时(机械磁盘的主要性能瓶颈,平均为2ms) + 寻址延时(2~3ms)+ 块传输延时(一般4k每块,40m/s的传输速度,延时一般为0.1ms) 决定。
    • 网络IO延时: 服务器响应延时、带宽限制、网络延时、跳转路由延迟、本地接收延时决定。
    • 所以两者一般来说网络IO延迟要大于磁盘IO的延时

    同步IO和异步IO

    同步IO

    • 同步和异步是针对应用程序和内核的交互而言的,
    • 同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪

    异步IO

    • 异步指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知
    • 内核空间会异步通知用户进程,并把数据直接给到用户空间。

    阻塞IO和非阻塞IO

    阻塞IO

    • 阻塞方式: 读取或者写入函数将一直等待,服务器的响应
      • 阻塞:用户空间通过系统调用(systemcall) 和内核空间发送IO操作时,该调用时阻塞的。

    非阻塞IO

    • 非阻塞方式: 读取或者写入喊出会立即返回一个状态值
      • 非阻塞:用户空间通过系统调用(systemcall)和内核空间发送IO操作时,该调用时不阻塞的,直接返回的。知识返回时,可能没有数据而已。、

    IO设计模式之Reactor pattern

    • 反应器设计模式(Reactor pattern)是一种处理并发服务请求,并将请求提交到一个或多个服务器程序的事件设计模式。当客户端请求抵达后,服务器处理程序使用多路分配策略,由一个非阻塞的线程来接收所有的请求,然后派发这些请求至相关的工作线程进行处理。

    Reactor模式角色分配

    • 初始化事件分配器(Initialization Dispatcher) : 用于管理Event Handler,定义注册,移除EventHandler等。它还作为Reactor模式的入口调用Synchronous Event DemultiPlexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event() 方法。
    • 同步(多路)事件分离器(Synchronous Event Demultilexer): 无限循环等待新事件的到来,一旦发现有新的事件到来,就会通知初始化事件分发器去调取特定的事件处理器。他是由操作系统内核实现的一个函数,用于阻塞等待发生在句柄集合上的一个或多个事件。
    • 系统处理程序 (Handler) : 操作系统中的句柄,是对资源在操作系统层面的一种抽象,他可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handler,即一个网络连接
    • 事件处理器(Event Handler) : 定义事件处理方法,以供Initialization Dispatcher回调使用。
    • Handle 句柄: 用来标识socket连接或是打开文件;
    • Reactor反应器: 定义一个接口,实现以下功能: 1、供应程序注册和删除关注的事件句柄; 2、运行事件循环;3、当有就绪事件来时,分发事件到之前注册的回调函数上处理。
    • Concreate Event HandlerA : 实现应用程序所提供的特定事件处理逻辑。

    为什么使用Reactor模式

    • 并发系统常使用Reactor模式代替常用的多线程的处理方式,节省系统的资源,提高系统的吞吐量。
    • 其实,在高并发的情况下,既可以使用多线程的处理方式,也可以使用Reactor处理方式。
      • 多线程的处理:为每个单独到来的请求,专门启动一条线程,这样的话造成系统的开销很大,并且在单核的机上,多线程并不能提高系统的性能,除非在有一些阻塞的情况发生,否则线程切换的开销会使得处理的速度变慢。
    • Reacotor模式的处理
      • 服务器启动一条单线程,用于轮询IO操作是否就绪,当有就绪的才进行相应的读写操作,这样的话就减少了服务器产生大量的线程,也不会出现线程之间的切换产生的性能消耗。(目前JAVA的NIO就采用的此种模式,这里引申出一个问题,在多核情况下NIO的扩展问题)
    • 以上两种处理方式都是基于同步的,多线程的处理是我们传统模式下对高并发的处理方式,Reactor模式的处理是现今面对高并发和高性能一种主流的处理方式。

    Reactor业务流程时序图

    在这里插入图片描述

    高性能IO模型

    • 服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:
      • 同步阻塞IO (Blocking IO): 即传统的IO模型
      • 同步非阻塞IO( Non-blocking IO): 默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。 注意这里所说的NIO并非Java的NIO库。
      • IO多路复用(IO MultiPlexing): 即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Lnuix中的epoll都是这种模型。
      • 异步IO(Asynchronous IO):即经典的Proactor设计模式,也被称为异步非阻塞IO

    IO模型的举例理解

    • 阻塞IO, 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你不
      会做其他事情, 属于备胎做法.
    • 非阻塞IO, 给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信等待不会
      做其他事情, 属于专一做法.
    • IO多路复用, 是找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺便
      看看其他妹子,玩玩王者荣耀, 上个厕所等等. IO复用又包括 select, poll, epoll 模式. 那么它们
      的区别是什么? 3.1 select大妈 每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要
      一个一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子 3.2 poll大妈不限制盯着
      女生的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神 3.3 epoll大妈不限制盯着女
      生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll大妈会为每个进宿舍楼的女生脸上贴上一
      个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道这个是不是你女神了, 然后大
      妈再通知你.
    • 上面这些同步IO有一个共同点就是, 当女神走出宿舍门口的时候, 你已经站在宿舍门口等着女神的, 此时你属
      于同步等待状态
    • 接下来是异步IO的情况 你告诉女神我来了, 然后你就去王者荣耀了, 一直到女神下楼了, 发现找不见你了,
      女神再给你打电话通知你, 说我下楼了, 你在哪呢? 这时候你才来到宿舍门口. 此时属于逆袭做法

    同步阻塞IO

    • 同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。
      在这里插入图片描述
    • 如上图所示,用户线程通过系统调用read发起IO读操作,有用户空间转到内核空间,内核等到数据包到达后,然后将接收到的数据拷贝到用户空间,完成read操作。
    • 即用户空间需要等待read将socket中的数据读取到buffer后,才继续处理接收到的数据。整个IO请求过程中,用户线程是被阻塞的,这导致后台在发起IO请求时,不能做任何事情,对CPU的资源利用率不高。

    同步非阻塞IO

    • 同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK,这样做用户线程可以发起IO请求后立即返回。
      在这里插入图片描述
    • 如上图所示,由于socket是非阻塞的方式,因此用户发起IO请求时立即返回。 但并读取到任何数据,用户线程需要不断飞起IO请求,直到数据到达后,才真正读取到数据,继续执行。
    • 即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后,立即返回返回,但是为了等到数据,仍需要不断的轮询、重复请求,消耗大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

    IO多路复用

    • IO多路复用模型是建立在内核提供的多路分离函数的select基础之上的,使用select函数可以避免同步非阻塞IO模型中的轮询等待的问题。
      在这里插入图片描述
    • 如上图所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
    • 从流程上看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率很差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个IO请求,用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的,而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
    • 然而,使用select函数的优点并不仅限于此,虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据来到时再进行处理,则可以提高cpu的利用率。
      在这里插入图片描述
    • 如上图所示,通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环处理。用户线程注册事件处理器之后可以继续执行做其他的工作,而Reactor线程负责调用内核的select喊出检查socket状态,当有socket被激活时,则通知相应的用户线程,执行handle_event进行数据读取、处理工作。由于select函数式阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意这里所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达,用户线程一定不会被阻塞。
    • IO多路复用是最常使用的IO模型,但是其异步程度还不够彻底,因为他使用了会阻塞线程的select系统调用。因此,IO多路复用只能称为异步阻塞IO,而非真正的异步IO。

    异步IO

    • "真正"的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知用户线程,有用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
    • 异步IO模型使用了Proactor设计模式实现了这一机制。
      在这里插入图片描述

    Redis IO多路复用技术以及epoll实现原理

    • redis 是一个单线程却性能非常好的内存数据库,主要用来作为缓存系统。redis 采用网络IO多路复用技术来保证在多连接的时候,系统的高吞吐量。

    为什么Redis使用I/O 多路复用呢?

    • 首先,Redis 是跑在单线程中,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以IO操作一般情况下往往不能直接返回,这会导致某一文件的I/O 阻塞导致整个进程无法对其他客户提供服务,而I/O多路复用是为了解决这个问题而出现的。
    • selectpollepoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行响应的操作。
    • redis 的iO模型默认采用epoll实现的。

    epoll实现机制

    • 假设有如下场景:
      • 有100万个客户端同时与同一个服务器进程保持着TCP连接,而每一时刻,通常只有几百上千个TCP连接时活跃的(事实上大部分场景都是这种情况),如何实现这样的高并发 ?
    • 在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千个并发连接。
    • 如果没有I/O 事件产生,我们的程序就会阻塞在select 处,但是依然有个问题,我们从select 哪里仅仅知道了,有I/O事件发生了,但却并不知道是哪几个流(可能有一个,多个,设置全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

    select 和 poll 的缺点

      1. 每次调用select/poll,都需要把句柄从用户态拷贝到内核态,这个开销在句柄很多时会很大
      1. 同时每次调用select/poll都需要在内核遍历传递进来的所有句柄,这个开销在句柄很多时也很大
      1. 针对select支持的文件描述符数量太小了,默认是1024
      1. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
      1. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
    • 相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

    什么是epoll

    • epoll的设计与实现与select 完全不同。 epoll是poll的一种优化,返回后不需要对所有的句柄进行遍历,在内核中维持了连接的列表。select 和 poll是将这个内核列表维持在用户态,然后传递给内核中。 与poll/select不同,epoll不再是单独的系统调用,而是有epoll_create/epoll_ctl/epoll_wait三个系统调用组成,后面将会看到这样做的好处。
    • epoll通过在Linux内核中申请了一个简易的文件系统,把原先的select/epoll调用分成了3个部分:
    • 1、调用epoll_create() 建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
    • 2、调用epoll_ctl向epoll对象中添加这100万个连接的套接字。
    • 3、调用epoll_wait收集发生事件的连接
    • 如此一来,要实现上面说的场景,只需要在进程启动时,建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

    epoll的优点

      1. epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这
        个数目和系统内存关系很大
        ,具体数目可以 cat /proc/sys/fs/file-max 察看。
      1. 效率提升, epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境
        中, epoll 的效率就会远远高于 select 和 poll 。
      1. 内存拷贝, epoll 在这点上使用了“共享内存”,这个内存拷贝也省略了。

    redis epoll底层实现

    • 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体有两个成员与epoll的使用方式密切相关。
    • eventpoll的结构体如下所示:
     struct eventpoll{
        //红黑树的根节点,这棵树中存储着所有添加到epoll中需要监控的事件
    	struct rb_root rbr;
    	//双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
    }
    
    • 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来事件。这些事件都会挂载到红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来。
    • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的时间发生时会调用这个回调方法,这个回调方法在内核中叫ep_poll_callback,他会将发生的时间添加到rdlist双链表中。
     struct epitem {
    	struct rb_node rbn; //红黑树的节点
    	struct list_head ralink; //双向链表的节点
    	struct epoll_fieldfd ffd; //事件句柄的消息
    	struct eventpoll *ep; //指向其所属的eventpoll对象
    	struct epoll_event event; //期待发生的事件类型
    }
    
    • 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表是否有epitem元素即可,如果rdlist不为空,则把发生的时间复制到用户态,同时将事件数量返回给用户。

    优势在哪里

    • 1、不用重复传递,我们调用epoll_wait时就相当于以往调用select/poll。但是这是却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
      -2、 在内核里,一切皆文件,所以,epoll向内核注册了一个文件系统,用于存储上述被监控的socket。当你调用epoll_create时,就会在虚拟的epoll文件系统中创建file结点,当然这个file不是普通文件,他只是服务于epoll。
    • epoll在被内核初始化时,同时会开辟出epoll自己的内核高速缓冲区,用户安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除,这个内核高速cache区,就是建立连续的物理内存也,然后在之上建立slab层,简单来说,就是物理上分配好你想要的size的内存对象,每次使用都是使用空闲的已分配好的对象。
    • 3、高效的原因: 这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储epoll_ctl传来的socket外,还会建立一个list链表,用于存储就绪的事情,当epoll_wait调用时,仅仅观察这个list链表有没有怇,有数据就返回,没有数据就sleep, 等到timeout时间到后,即使链表没有数据页返回,所以,epoll_wiat非常高效。

    准备就绪的list链表是如何维护的?

    • 当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄中断到了,就把他放到准备就绪的list链表里。所以当一个socket有数据到了,内核就把网卡上的数据copy到内核中后就来把socket插入到准备就绪的链表里了(这就是epoll的基础,回调,好好理解一下这句话,非常重要)。
    • 总结: 执行epoll_crate时,创建了红黑树和就绪表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立即返回准备就绪链表中的数据即可。
    • 最后,感谢读者的阅读,文章如有错误,评论或私信我,会及时加以改正,大家共同进步
    展开全文
    weixin_44624375 2021-04-01 21:57:53
  • redis 是一个单线程却性能非常好...LINUX IO多路复用原理 在linux下面, 常见的有5中网络IO方式, 具体可以参考如下的文章, 总结的很清楚, 我们就不再具体介绍: http://blog.csdn.net/lltaoyy/article/details/54

    redis 是一个单线程却性能非常好的内存数据库, 主要用来作为缓存系统。 redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。

    LINUX IO多路复用原理

    在linux下面, 常见的有5中网络IO方式, 具体可以参考如下的文章, 总结的很清楚, 我们就不再具体介绍:
    http://blog.csdn.net/lltaoyy/article/details/54861749

    redis的多路复用, 提供了select, epoll, evport, kqueue几种选择,在编译的时候来选择一种。

    • select是POSIX提供的, 一般的操作系统都有支撑;
    • epoll 是LINUX系统内核提供支持的;
    • evport是Solaris系统内核提供支持的;
    • kqueue是Mac 系统提供支持的;

    我们一般运行的服务器都是LINUX系统上面, 并且我对Solaris和Mac系统不是很了解, 我们这里重点比较一下select、poll和epoll 3种多路复用的差异。

    • select: 单个进程所能打开的最大连接数有FD_SETSIZE宏定义, 其大小为1024或者2048; FD数目剧增后, 会带来性能问题;消息传递从内核到与到用户空间,需要copy数据;

      性能问题:
      (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
      (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

    • poll: 基本上与select一样, 不通点在于没有FD数目的限制, 因为底层实现不是一个数组, 而是链表;

    • epoll: FD连接数虽然有限制, 但是很大几乎可以认为无限制;epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题; 内核和用户通过共享内存来传递消息;

    LINUX IO多路复用的接口

    select 在LINUX的接口:

    #include <sys/select.h>
    
    /* According to earlier standards */
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int select(int nfds, fd_set *readfds, fd_set *writefds,
                      fd_set *exceptfds, struct timeval *timeout);
    
    void FD_CLR(int fd, fd_set *set);
    int  FD_ISSET(int fd, fd_set *set);
    void FD_SET(int fd, fd_set *set);
    void FD_ZERO(fd_set *set);

    select 函数的参数:
    - nfds:fd_set的FD的个数, 采用位图的方式记录fd_set集合的FD状态;
    - readfds: fd_set 集合中来监控有哪些读操作没有被block, 如果有可读,select
    - writefds:fd_set 集合中来监控有哪些写操作没有被block;
    - exceptfds: fd_set 集合中来监控有哪些except操作没有被block;
    - timeout: FD z最小被block的时间, 如果timeout的2个field都是0, 会立刻返回, 如果该参数是NULL, 会一直block;
    select如果有一个或者多个读操作, 写操作, except操作不被block, 返回大于1的数值; 若果没有不被block的FD, 但是某些FD block超时, 返回0; 如果错误出现, 返回-1;
    FD_XXX函数, 是添加、删除、清空以及判断fd_set的工具函数。

    select的pseudo 代码:

    while1){
       int ret = select(streams[]);
       if (ret > 0 ) {
          for i in streams[] {
               if i has data {
                  read or write streams[i];
               }
          }        
       } else if (ret == 0) {
          handle timeout FDs;
       }else {
          handle error
       }
    
    }

    epoll的LINUX的接口:

    #include <sys/epoll.h>
    
    //预定义的EVENT
    enum EPOLL_EVENTS                                                                                                                                                                            
      {
        EPOLLIN = 0x001,
    #define EPOLLIN EPOLLIN
        EPOLLPRI = 0x002,
    #define EPOLLPRI EPOLLPRI
        EPOLLOUT = 0x004,
    #define EPOLLOUT EPOLLOUT
        EPOLLRDNORM = 0x040,
    #define EPOLLRDNORM EPOLLRDNORM
        EPOLLRDBAND = 0x080,
    #define EPOLLRDBAND EPOLLRDBAND
        EPOLLWRNORM = 0x100,
    #define EPOLLWRNORM EPOLLWRNORM
        EPOLLWRBAND = 0x200,
    #define EPOLLWRBAND EPOLLWRBAND
        EPOLLMSG = 0x400,
    #define EPOLLMSG EPOLLMSG
        EPOLLERR = 0x008,
    #define EPOLLERR EPOLLERR
        EPOLLHUP = 0x010,
    #define EPOLLHUP EPOLLHUP
        EPOLLRDHUP = 0x2000,
    #define EPOLLRDHUP EPOLLRDHUP
        EPOLLWAKEUP = 1u << 29,
    #define EPOLLWAKEUP EPOLLWAKEUP
        EPOLLONESHOT = 1u << 30,
    #define EPOLLONESHOT EPOLLONESHOT
        EPOLLET = 1u << 31
    #define EPOLLET EPOLLET
      };
    
    int epoll_create(int size);
    //创建epoll对象并回传其描述符。
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    //将要交由内核管控的文件描述符加入epoll对象并设置触发条件。
    
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    //等待已注册之事件被触发或计时终了。

    epoll提供edge-triggered及level-triggered模式。在edge-trigger模式中,epoll_wait仅会在新的事件首次被加入epoll 对象时返回;于level-triggered模式下,epoll_wait在事件状态未变更前将不断被触发。
    举例来说,倘若有一个已经于epoll注册之管线接获数据,epoll_wait将返回,并发出数据读取的信号。现假设缓冲器的数据仅有部分被读取并处理,在level-triggered模式下,任何对epoll_wait之调用都将即刻返回,直到缓冲器中的数据全部被读取;然而,在edge-triggered的情境下,epoll_wait仅会于再次接收到新数据(亦即,新数据被写入管线)时返回。

    epoll的实现pseudo 代码

    epollfd = epoll_create()
    
    while1) {
        active_stream[] = epoll_wait(epollfd)
        for (i=0; i < len(active_stream[]); i++) {
            read or write active_stream[i]
        }
    }

    redis 多路复用的应用

    接下来我们看一下, redis的多路复用如何实现的。整个redis的main函数包含如下3部分:
    1、初始化Redis Server参数,这部分代码通过initServerConfig实现。
    2、初始化Redis Server,这部分代码在initServer里面。
    3、启动事件轮询器。

    这里第一部分, 就是通过配置文件的参数来初始化server对象的参数, 和本文的主题没有太大关系这里略过。
    第二部分, 包含了创建轮询器, 以及一个时间event队列, 和file event数组。

    void initServer(void) {
        ...
        server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
        if (server.el == NULL) {
            serverLog(LL_WARNING,
                "Failed creating the event loop. Error message: '%s'",
                strerror(errno));
         }
         ...
             /* Create the timer callback, this is our way to process many background
         * operations incrementally, like clients timeout, eviction of unaccessed
         * expired keys and so forth. */
        if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
            serverPanic("Can't create event loop timers.");
            exit(1);
        }
    
        /* Create an event handler for accepting new connections in TCP and Unix
         * domain sockets. */
        for (j = 0; j < server.ipfd_count; j++) {
            if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
                acceptTcpHandler,NULL) == AE_ERR)
                {
                    serverPanic(
                        "Unrecoverable error creating server.ipfd file event.");
                }
        }
        if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
            acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");
    
    
        /* Register a readable event for the pipe used to awake the event loop
         * when a blocked client in a module needs attention. */
        if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
            moduleBlockedClientPipeReadable,NULL) == AE_ERR) {
                serverPanic(
                    "Error registering the readable event for the module "
                    "blocked clients subsystem.");
        }
    

    第三部分, 是整个event loop部分:

    int main() {
        // 第一部分
        // 第二部分入口
        initServer();
        ...
        aeSetBeforeSleepProc(server.el,beforeSleep);                                                                                                                                   
        aeSetAfterSleepProc(server.el,afterSleep);
        aeMain(server.el);
        aeDeleteEventLoop(server.el);
        return 0;
    }
    
    void aeMain(aeEventLoop *eventLoop) {                                                                                                                                              
        eventLoop->stop = 0;
        while (!eventLoop->stop) {
            if (eventLoop->beforesleep != NULL)
                eventLoop->beforesleep(eventLoop);
            aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
        }
    }
    
     * The function returns the number of events processed. */
    int aeProcessEvents(aeEventLoop *eventLoop, int flags)
    {
             * some event fires. */
            numevents = aeApiPoll(eventLoop, tvp);
    
            /* After sleep callback. */
            if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
                eventLoop->aftersleep(eventLoop);
    
            for (j = 0; j < numevents; j++) {
                aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
                int mask = eventLoop->fired[j].mask;
                int fd = eventLoop->fired[j].fd;
                int rfired = 0;
    
            /* note the fe->mask & mask & ... code: maybe an already processed
                 * event removed an element that fired and we still didn't
                 * processed, so we check if the event is still valid. */
                if (fe->mask & mask & AE_READABLE) {
                    rfired = 1;
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                }
                if (fe->mask & mask & AE_WRITABLE) {
                    if (!rfired || fe->wfileProc != fe->rfileProc)
                        fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                }
                processed++;
            }
        }
        /* Check time events */
        if (flags & AE_TIME_EVENTS)
            processed += processTimeEvents(eventLoop);
    
        return processed; /* return the number of processed file/time events */
    }
    
    static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
        aeApiState *state = eventLoop->apidata;
        struct epoll_event ee = {0}; /* avoid valgrind warning */
        int mask = eventLoop->events[fd].mask & (~delmask);
    
        ee.events = 0;
        if (mask & AE_READABLE) ee.events |= EPOLLIN;
        if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
        ee.data.fd = fd;
        if (mask != AE_NONE) {
            epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
        } else {
            /* Note, Kernel < 2.6.9 requires a non null event pointer even for
             * EPOLL_CTL_DEL. */
            epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
        }       
    }   

    上述的代码比较简单, 没有做过多的说明, 只是把调用关系罗列出来, 以便看清楚redis的整个框架。

    展开全文
    baijiwei 2018-02-04 18:38:22
  • 3.IO多路复用技术4.Redis线程模型4.1 I/O多路复用程序、文件事件分派器 前言 redis 是一个单线程却性能非常好的内存数据库, 主要用来作为缓存系统。 redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高...

    前言

    redis 是一个单线程却性能非常好的内存数据库, 主要用来作为缓存系统。 redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。


    1. redis是基于缓存的

    官方文档介绍说,Redis的操作都是基于内存的,CPU不是 Redis性能瓶颈,,Redis的瓶颈是机器内存和网络带宽

    Reds是C语言写的,性能极高。单台redis情况下,官方提供的数据为:读的速度是110000次/s,写的速度是81000次/s 。

    但是redis的线程模型表明,redis是单进程、单线程的。是不是很amazing!!!

    2. Redis为什么这么快?

    先说两个误区:一,高性能的服务器不一定都是多进程、多线程的;二、多线程不一定都不比单线程快,比如单核机器。

    在我们通常的认知中,高性能都是通过多进程、多线程实现的。比如Nginx是多进程单线程的,Memcached是单进程多线程的。

    在计算机的世界中,CPU的速度是远大于内存的速度的,同时内存的速度也是远大于硬盘的速度。redis的操作都是基于内存的,绝大部分请求是纯粹的内存操作,非常迅速,使用单线程可以省去多线程时CPU上下文会切换的时间,也不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。对于内存系统来说,多次读写都是在一个CPU上,没有上下文切换效率就是最高的!既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章的采用单线程的方案了(毕竟采用多线程会有很多麻烦)。

    那么Redis的单进程单线程模型的具体细节是怎样的?

    3.IO多路复用技术

    首先我们了解下IO多路复用技术
    比如,现在我们模拟一个tcp服务器处理30个客户的socket,如何快速的处理掉这30个请求呢?

    在不了解原理的情况下,我们类比一个实例:在课堂上让全班30个人同时做作业,做完后老师检查,30个学生的作业都检查完成才能下课。如何在有限的资源下,以最快的速度下课呢?

    • 第一种:安排一个老师,按顺序逐个检查。先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。这种方式只需要一个老师,但是耗时时间会比较长。

    • 第二种:安排30个老师,每个老师检查一个学生的作业。
      这种类似于为每一个socket创建一个进程或者线程处理连接。这种方式需要30个老师(最消耗资源),但是速度最快。

    • 第三种:安排一个老师,站在讲台上,谁解答完谁举手。这时C、D举手,表示他们作业做完了,老师下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。这种方式可以在最小的资源消耗的情况下,最快的处理完任务。

      第三种就是IO复用模型(Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor模式。)

    4.Redis线程模型

    Redis基于Reactor模式开发了自己的网络事件处理器,被称为文件事件处理器,由套接字、I/O多路复用程序、文件事件分派器(dispatcher),事件处理器四部分组成。
    在这里插入图片描述

    4.1 I/O多路复用程序、文件事件分派器

    • I/O多路复用程序会同时监听多个套接字,当被监听的套接字准备好执行accept、read、write、close等操作时,与操作相对应的文件事件就会产生,I/O多路复用程序会将所有产生事件的套接字都压入一个队列,然后以有序地每次仅一个套接字的方式传送给文件事件分派器,文件事件分派器接收到套接字后会根据套接字产生的事件类型调用对应的事件处理器。

    事件的处理器
    在这里插入图片描述
    (1)连接应答处理器:

    当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AEREADABLE事件关联起来,当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候,套接字就会产生AEREADABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作。

    (2)命令请求处理器:

    当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AEREADABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AEREADABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作;

    在客户端连接服务器的整个过程中,服务器都会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。

    (3)命令回复处理器:

    当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AEWRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AEWRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作。

    当命令发送完毕后,服务器会解除命令回复处理器与客户端套接字的AE_WRITABLE事件之间的关联。

    注意1:只有当上一个套接字产生的事件被所关联的事件处理器执行完毕,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字,所以对每个命令的执行时间是有要求的,如果某个命令执行过长,会造成其他命令的阻塞。所以慎用O(n)命令,Redis是面向快速执行场景的数据库。
    注意2:命令的并发性。Redis是单线程处理命令,命令会被逐个被执行,假如有3个客户端命令同时执行,执行顺序是不确定的,但能确定不会有两条命令被同时执行,所以两条incr命令无论怎么执行最终结果都是2。

    客户端与redis通信过程
    1、假设一个Redis服务器正在运作,那么这个服务器的监听套接字的 AE_READABLE 事件应该正处于监听状态之下, 而该事件所对应的处理器为连接应答处理器。

    2、如果这时有一个Redis客户端向服务器发起连接,那么监听套接字将产生 AEREADABLE事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AEREADABLE事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。

    3、之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生 AE_READABLE 事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。

    4、执行命令将产生相应的命令回复, 为了将这些命令回复传送回客户端, 服务器会将客户端套接字的 AEWRITABLE 事件与命令回复处理器进行关联。当客户端尝试读取命令回复的时候, 客户端套接字将产生 AEWRITABLE 事件, 触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。

    参考:https://github.com/doocs/advanced-java/blob/main/docs/high-concurrency/redis-single-thread-model.md

    展开全文
    b1303110335 2021-01-07 20:39:02
  • longgeqiaojie304 2019-09-17 22:09:27
  • zbajie001 2020-03-21 17:34:18
  • qq_40989769 2021-04-24 16:40:21
  • java_cjx 2021-03-23 19:55:28
  • cj_eryue 2021-06-30 14:53:56
  • Seky_fei 2020-06-14 19:29:42
  • xmt1139057136 2021-07-07 00:13:52
  • qq_36986015 2020-06-14 21:07:38
  • qq_40989769 2020-09-07 17:44:52
  • Muscleape 2020-04-19 23:17:06
  • weixin_44204751 2021-11-13 06:17:38
  • weixin_45784328 2020-04-09 13:58:11
  • qq_28018283 2020-09-04 17:03:38
  • sun_tantan 2021-08-17 09:22:49
  • weixin_52622200 2020-12-17 17:56:40
  • ajianyingxiaoqinghan 2020-07-07 23:49:27
  • lvxinchun 2020-08-14 17:32:47
  • sinat_33087001 2020-11-30 13:10:29
  • With_Her 2020-01-05 22:37:41

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,527
精华内容 4,210
关键字:

redisio多路复用原理

redis 订阅