文章

高并发IO的底层原理

高并发IO的底层原理

背景

先来感受下Java里边进行OIO操作,代码怎么写?

Desktop View 面向String流的IO模型

这里的OIO实现,需要给每个连接起一个线程,稍微给点流量就能耗尽机器资源,瓶颈是显而易见的。

如果是NIO这种面向缓冲区的模型,就不是套接字了,而是socket通道,拿到通道就可以进行缓冲区的写入,

Desktop View NIO Client

读在server这边,读也需要拿到套接字的传输通道,进行读取工作。

Desktop View NIO Server

BIO、OIO、NIO

术语全称核心含义适用场景
BIOBlocking IO同步阻塞IO连接数少且固定的架构
OIOOld IO传统同步阻塞IONetty框架中区分旧IO的命名场景
NIONew IO / Non-Blocking IO同步非阻塞IOIO多路复用)连接数多且流量小的架构
  • BIO‌:全称Blocking IO,即同步阻塞IO,也被称为Old IO,是JDK1.4之前的唯一IO选择,连接建立后会直接阻塞线程,直到读写操作完成,适合连接数少且固定的场景。
  • OIO‌:全称Old IO,也可指代同步IO模型,和BIO定义完全一致,是Netty框架中常用的命名方式,用来和NIO做明确区分。
  • NIO‌:全称New IO(也可理解为Non-Blocking IO),JDK1.4引入的非阻塞IO模型,核心由缓冲区、通道、选择器组成,通过单线程的选择器管理大量连接,适合连接数多且流量小的场景。

如果要开发高并发的网络应用,‌不要直接使用原生NIO‌,而应该学习Reactor模式‌,比如在Java中,使用Netty‌框架来实现快速开发高性能、高可靠的网络服务器和客户端程序,以获得最佳的性能和开发效率。

Reactor模式由Doug Lea《Scalable IO in Java》中提出,是一种基于‌事件驱动‌的并发处理模型,一种设计模式(架构思想),旨在解决传统BIO在高并发场景下线程资源耗尽的问题,主要关注如何高效地分发和处理IO事件,C++GoJavaPython等均可实现。
Netty是基于该思想实现的高性能异步的、基于事件驱动的Java网络框架(工具),Netty的底层线程模型正是基于多Reactor多线程模式‌实现的。

如果要阐述清楚ReactorNetty,需要专门的篇幅向上看架构思想,本篇对此不做具体讨论,聚焦向下看IO底层原理。

向下看

如果要搞定高并发IO的底层原理,仅仅熟悉这些基础数据的读写操作是远远不够的,这些仅仅是在Java语言虚拟机的维度。实际上,这些IO读写要转换成C语言里边的系统调用函数,转换通过JNI实现。无论是BIO,还是NIO,底层都要用JNI操作,JNI里边有操作系统的IO库函数(readwrite),所以Java语言的IO操作和C里边的面向socket的读写,原理都是类似的。

下面看下C语言里做数据读写,都涉及到哪些库函数,大致流程是什么样的。

库函数readwrite,是提供给用户使用的,这组函数不是系统调用,系统调用属于内核程序,库函数最终还是要调用到系统调用。简单理解,假设读写的系统调用分别是sys_readsys_write,不同操作系统的不同版本,库函数和系统调用的名字不同。不管是Java还是C,最终底层原理都是通过库函数再去进行系统调用。这就涉及到两个核心概念——用户空间和内核空间

用户空间与内核空间

出于安全考虑,出现了用户空间和内核空间的概念。

Desktop View 用户空间和内核空间

CPU指令里边,都是有权限的。为什么要有权限呢?
硬件资源、内存、网卡,这些设备不是所有的用户程序都能直接进行操作,如果用户程序能简单粗暴地直接操作,整个操作系统就会乱套。出于安全考虑,操作系统把用户能访问的空间叫做用户空间,内核程序能访问的空间叫做内核空间。从权利等级的角度来说,内核程序具有最高的权限,用户程序拥有的权限比较小,只能访问自己空间的内存地址。

用户空间如果要访问底层的物理设备,怎么办?毕竟权限不在自己手上,而在内核程序的手上,这时有一种渠道是系统调用,如果要读取网卡或文件里边的数据,必须给内核发指令,通过系统调用的方式来完成。

这里边还有一个概念——用户态和内核态

内核态,是指CPU内核所处的状态,用户态意味着它执行的是用户程序,内核态表示它执行的是内核程序。

系统调用也不是直接去读写设备,读是从内核缓冲区数据写入到用户缓冲区,写是从应用程序进程的用户缓冲区复制到操作系统内核缓冲区。这里的内存指的是虚拟内存,并不是物理内存。简单理解,在做操作时,用户程序的内存地址都是虚拟的,没有物理地址,虚拟地址映射到物理地址是由内核完成的。读写都是内存的复制。

为什么不去和物理设备做交互,而要通过内核空间绕一圈?
原因很简单,是由硬件设备的速度决定的,直接读写网卡太慢了,等待时间划不来。比如一个千兆网卡,每秒能读多少数据,千兆是1K个位,不是1K字节,算下来大约是100M字节,就是说1s只能读100M数据。而内存的速度是多少?比如ddr3能达到每秒10G10G100M,相差100倍,ddr4读写速度大概每秒50G,相差500倍, Desktop View 设备读写速度对比

内核缓冲区的数据,是由谁搬过来的?

是由物理硬件来完成的,DMA复制。网卡的数据来了,内核并不是直接把数据通知用户程序去读,而是默默地做些幕后的工作,把网卡的数据复制到内核缓冲区里。

同样地,写入数据时,系统调用不是直接写网卡,而是写到内核缓冲区缓冲起来,等到写入一定的数据之后,再把它写入网卡里边。

IO系统调用sys_read & sys_write的执行流程

从宏观上,分为两个阶段,不论是读,还是写,都有这两个阶段,

以读为例,

  • 第一阶段,应用程序只要开始系统调用,应用程序就进入阻塞状态。为什么进入阻塞状态?内核空间不一定准备好,比如读数据是从网卡里边来的,网卡的数据还没来得及搬到内核缓冲区,准备工作没做好,用户程序执行系统调用只能阻塞。
  • 第二阶段,内核缓冲区里的数据已经准备好了,系统调用就执行相应的内核程序,这个内核程序主要工作就是执行CPU的数据复制,把数据从内核缓冲区复制到用户缓冲区。完成复制,可以理解为第二阶段的工作。

写也一样,

  • 用户程序执行库函数,走系统调用,这时要看内核缓冲区有没有做好准备。需要做什么准备?每一个套接字(每一个网络连接)在内核缓冲区中有一段写入缓冲区,专门用来写数据的,写入缓冲区有默认大小(默认4KB),超过4KB说明专用的缓冲已经写满了,这时系统调用做复制的时候没办法做数据复制,只能阻塞。等到专用缓冲里边的数据通过DMA搬运完了,内核缓冲区里对应的空间腾出来了,系统调用就可以做CPU复制了。就是等待在内核缓冲区里有没有足够的空间。
  • 第二阶段,就是执行CPU的数据复制,把数据从用户缓冲区复制到内核缓冲区,复制完后,就会解除用户进程的阻塞状态。

CPU的复制工作是由CPU的内核函数(系统调用)来完成的,等到系统调用结束之后,用户进程就会解除阻塞,获得CPU的执行权限,等待被CPU调度(等待时间片)。

Desktop View 一轮读写,发生4次切换

把写的数据准备好,准备好之后,就是一段内存,写套接字,套接字对应到底层,就是一个物理连接,会发生什么?

客户端发送请求,C调用write库函数,底层执行系统调用,通过系统调用把用户数据从用户空间复制到内核空间,内核空间的数据会通过网卡发送出去。用户程序完成系统调用后,就会解除阻塞,干自己的事去,背后的工作由内核完成。

服务端接收数据,服务端系统先干一个幕后的工作,接收数据的过程对用户程序无感知,服务端系统会把数据从网卡里复制到内核缓冲区,服务端程序一直在做数据查询(IO事件查询),只要内核缓冲区过来了数据,就会触发IO事件,用户程序查到IO事件,知道内核缓冲区的数据过来了,用户程序就会执行库函数,底层执行sys_read系统调用,用户程序进入阻塞状态,内核函数进行CPU的数据复制,把接收到的内核缓冲区里边的数据复制到用户空间,复制完之后,系统调用结束,结束后就唤醒用户程序。

这个过程还涉及到用户态和内核态之间的4次切换,一次读要切换2次,一次写也要切换2次。发生一轮读写,就发生4次切换。

5大IO模型

同步和异步相对,同步不好理解,异步好理解一点,类似Java里的异步回调模式,这个异步和异步IO类似。

同步IO,是指用户空间向内核空间发起一次IO请求,从用户空间到内核空间,这种方向的调用,叫做同步;反过来,异步IO,是指内核空间通知用户程序去干活。

比如,同步调用是A调用B;异步调用是,AB设置一个回调函数,B当条件具备了之后,再执行回调函数。

IO层面,方向类似,

  • 同步调用,就是发起方是用户程序,调用内核程序,如果内核程序没有准备好,用户空间只能死等(同步等待);
  • 异步调用,内核空间,当数据准备好了,在执行回调程序,或者发一个通知,告诉你可以做这件事情了。避免了用户空间在那里傻傻等待。

同步IO操作由用户程序发起,

  • 阻塞,从CPU层面来说,任务从CPU的工作队列中移除,不具备调度的机会;
  • 非阻塞,用户进程发起IO操作后,首先查下状态,如果内核IO操作没有完成,就不等了,直接去干别的事情,立即返回到用户空间的IO操作。IO操作还是得做,等其他事干完了,再来查下是否可以执行IO操作,等到能做了再来做这个操作。非阻塞就是不断地做状态的查询,轮询式的试探性的工作。

信号驱动IO模型,是属于异步IO模型的一种,或者说是异步IO模型中没有完全做到异步的,半异步的IO模型。

同步阻塞IO

Desktop View 同步阻塞IO

举个例子,在Java中发起一个socketsys_read读操作的系统调用,流程大致如下,

  • Java进行IO读后发起sys_read系统调用开始,用户线程(或者线程)就进入阻塞状态。
  • 当系统内核收到sys_read系统调用,就开始准备数据。一开始,数据可能还没有到达内核缓冲区(例如,还没有收到一个完整的socket数据包),这个时候内核就要等待。
  • 内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到用户缓冲区中的字节数)。
  • 直到内核返回后,用户线程才会解除阻塞的状态,重新运行起来。

阻塞IO的特点,在内核进行IO执行的两个阶段,发起IO请求的用户进程(或者线程)被阻塞了。

阻塞IO的优点,应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起,用户线程基本不会占用CPU资源。

阻塞IO的缺点,一般情况下,会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。在高并发应用场景中,阻塞IO模型是性能很低的,基本上是不可用的。

同步非阻塞IO

同步阻塞阶段,数据有可能还没来到内核缓冲区,还在网卡里,只有到了内核缓冲区,系统调用才能把内核缓冲区的数据复制到用户缓冲区,在这个过程中,用户进程检查内核有没有把数据准备好这个事件提供给用户线程,用户线程在做IO操作时,先查下内核有没有把数据准备好这个事件提供给用户线程,准备好之后,再来做数据复制的工作。如果没有准备好,就不会在这傻等。

内核把数据有没有准备好这件事和数据复制做解耦,一分为二,有没有做好准备,这个叫IO事件,如果做好准备了,代表发生了IO事件,没做好准备就是没发生IO事件,发生了IO事件才具备了执行数据复制的条件。

非阻塞IO分为两个阶段,第一阶段不用阻塞,之前等待数据从网卡复制到内核空间,现在这个过程不需要等待了,但是数据从内核缓冲区复制到用户缓冲区,还是阻塞的,说明这里的非阻塞不是完全的非阻塞。这个过程相对比较短,DMA复制是从低速设备复制到高速设备,CPU复制是从高速设备复制到高速设备。

Desktop View 同步非阻塞IO

举个例子,发起一个非阻塞socketsys_read读操作的系统调用,流程如下,

  • 在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。所以,为了读取到最终的数据,用户进程(或者线程)需要不断地发起IO系统调用。
  • 内核数据到达后,用户进程(或者线程)发起系统调用,用户进程(或者线程)阻塞(大家一定要注意,此处用户进程的阻塞状态)。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区,然后内核返回结果(例如返回复制到的用户缓冲区的字节数)。
  • 用户进程(或者线程)在读数据时,没有数据会立即返回而不阻塞,用户空间需要经过多次的尝试,才能保证最终真正读到数据,而后继续执行。

同步非阻塞IO的特点,应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。

同步非阻塞IO的优点,每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

同步非阻塞IO的缺点,不断地轮询内核,这将占用大量的CPU时间,效率低下。

总体来说,在高并发应用场景中,同步非阻塞IO是性能很低的,也是基本不可用的,一般Web服务器都不使用这种IO模型。在Java的实际开发中,也不会涉及这种IO模型。但是此模型还是有价值的,其作用在于,其他IO模型中可以使用非阻塞IO模型作为基础,以实现其高性能。

同步非阻塞IO,是多路IO复用的基石。

IO多路复用

目前使用最多的,也是用的最成熟的IO模型——IO多路复用。

建立在同步非阻塞IO基础上,它是同步的,比同步非阻塞IO性能更好。它是怎么做到的呢?

假设在同步非阻塞IO模式下,轮询100w个连接,有没有发生IO事件,轮询的工作就交由线程去做,轮询的效率非常低,所以操作系统帮我们干了这个工作。操作系统增加了一组系统调用——selectepoll。基础版本是select,高级版本是epollselect是所有操作系统都支持的,epollLinux支持的。

这组系统调用的作用,是来查询大量连接的IO事件,一次性可以查询很多连接的IO事件。原来是一个个查,现在是一批批查,效率很大提高。

通过该系统调用,一个进程可以监视很多个文件描述符(包括socket连接)的状态,一旦某个描述符就绪(指的IO事件,一般是内核缓冲区可读/可写,用户线程不用一个个去轮询了,这样效率很低,轮询的工作由操作系统底层做好,底层实现也不一定是轮询,还可以是回调等动作),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。

select/epollIO流程发生了变化,

Desktop View IO多路复用

分为两大步骤,

  • 第一,查询IO事件,select去查数据有没有从设备被复制到内核缓冲区,复制完了之后,IO事件就有了。
  • 第二,执行实际的系统调用。

和同步非阻塞IO不同,select查询事件的时候,第一步可以一次性查询几十万,甚至几百万个Socket连接的IO事件(或者文件描述符的IO事件),所以这里叫IO多路复用。

举个例子,来说明IO多路复用模型的流程。发起一个多路复用IOsys_read读操作的系统调用,流程如下,

  • 选择器注册。因为要做很多连接的IO事件查询,需要把连接注册到选择器里,只需要做一次,后面就是不断地轮询。在这种模式中,首先,将需要sys_read操作的目标文件描述符(socket连接),提前注册到Linuxselect/epoll选择器中,在Java中所对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程。
  • 就绪状态的轮询。通过选择器的查询方法,查询所有的提前注册过的目标文件描述符(socket连接)的IO就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好或者就绪了,就是内核缓冲区有数据了,内核就将该socket加入到就绪的列表中,并且返回就绪事件。
  • 用户线程获得了就绪状态的列表后,根据其中的socket连接,发起sys_read系统调用(比如说这个连接有事件可以读,就可以发起读数据的系统调用),用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
  • 复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

复制数据的线程,和我们数据查询的线程,不是同一个线程。在查询时,一个线程可以查几十万的连接,但在复制时,一个线程做不到几十万量级的数据读取。

IO多路复用模型的特点,IO多路复用模型的IO涉及两种系统调用,一种是IO操作的系统调用,另一种是select/epoll就绪查询系统调用。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll

NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。

IO多路复用模型与同步非阻塞IO模型是有密切关系的,具体来说,注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。只是这一点对于用户程序而言,是无感知的。

IO多路复用模型的优点,一个选择器查询线程,可以同时处理成千上万的网络连接,所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。这是一个线程维护一个连接的阻塞IO模式相比,使用多路IO复用模型的最大优势。

通过JDK的源码可以看出,Java语言的NIONew IO)组件,在Linux系统上,是使用的是select系统调用实现的。所以,Java语言的NIO组件所使用的,就是IO多路复用模型。

IO多路复用模型的缺点,本质上,select/epoll系统调用是阻塞式的,属于同步阻塞IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个事件的查询过程也是阻塞的,尽管是批量查询,但查询的动作是阻塞式的。

如果彻底地解除线程的阻塞(CPU数据复制的阻塞、IO事件查询的阻塞),有两种方案,

  • 一种方案是用异步IO模型(这种异步是一点阻塞都没有)。但是异步IO模型不太成熟,尤其是在Linux系统上,它是基于IO多路复用实现的,Linux上的异步IO模型是个假的,本质上又回到了起点。
  • 还有一种方案是,通过一个比较好的应用架构,应用里边是多线程的,一两个线程局部的阻塞不影响整体,所以可以通过多线程的组件架构解决底层的线程的阻塞问题,也能达到非阻塞的效果。实际上Netty也是这么干的,底层通过Reactor线程模型实现了异步非阻塞效果的框架。

信号驱动IO

异步IO比较原始的版本,就是信号驱动IO

Desktop View 信号驱动IO

类比Java里边的异步回调模式,向内核注册一个回调函数,等到内核缓冲区的数据准备好之后,内核会回调之前注册的回调函数,启动回调函数的执行,回调函数里边再去做数据复制的工作。

前面IO多路复用,IO事件是自己主动去查,异步IO的方向就反过来了,IO事件不需要我们主动查询,回调就行了。

只不过信号驱动IO,是半异步IO,回调工作是内核返过来的,但CPU的复制工作又是同步的,也就是由用户进程发起的。

为什么叫信号驱动?

注册的IO事件,可以理解为一个信号。怎么给这个信号注册一个回调函数?Linux提供了一个系统调用——sigaction。如果要做IO操作,信号就是SIGIO。不管是读写,还是异常,都只有一个信号。只要在这个信号上设置了回调函数,内核就会把套接字上面发生的所有信号都来回调设置的回调函数。

注册了信号的回调函数,还要对套接字做些操作。首先把套接字的拥有者设置为当前进程,信号如果回来了,就会回调进程里边的函数。然后设置套接字的状态,设置为非阻塞异步的状态。

信号驱动IO优势,解决了IO多路复用过程中,查询IO事件的阻塞过程,提升了那么一点点效率。

用户进程在等待数据时,不会被阻塞,能够提高用户进程的效率。具体来说,在信号驱动式IO模型中,应用程序使用套接口进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。

信号驱动IO缺点,做回调函数注册时,只有一个信号可以注册,套接字上面事件有很多类型,只要发生类型里边的一种事件就会执行回调函数,成本就比较高,回调函数里可以做一些判断处理,但不管怎么判断,所有IO事件回调函数都会被回调到,就会产生很多没意义的回调,效率比较低。

  • 在大量IO事件发生时,可能会由于处理不过来,而导致信号队列溢出。
  • 对于处理UDP套接字来讲,对于信号驱动IO是有用的。UDPIO事件比较少,只有两种IO事件,一种是数据过来了,一种是发生错误,相对来说简单点。可是,对于TCP而言,由于致使SIGIO信号通知的条件为数众多,进行IO信号进一步区分的成本太高,信号驱动的IO方式近乎无用。
  • 信号驱动IO可以看成是一种半异步IO,可以简单理解为系统进行用户函数的回调。信号驱动IO的异步特性,又做的不彻底。为什么呢? 信号驱动IO仅仅在IO事件的通知阶段是异步的,而在第二阶段,也就是在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程是阻塞的、同步的。只是不用查询IO事件,但是CPU复制数据还是阻塞的。

如果要做彻底的异步IO,那就需要使用第五种IO模式——异步IO模式。

异步IO(Asynchronous IO)

CPU复制数据工作,也交由内核完成,等到数据复制工作完成后,再来做回调。用户的回调函数里边,当然就不存在数据的CPU复制,用户空间里的数据是准备好了的,直接执行后边的业务操作就可以了。这是真正的异步IO模型。

Desktop View 异步IO

举个例子,发起一个异步IOsys_read读操作的系统调用,流程如下,

  • 当用户线程发起了sys_read系统调用(可以理解为注册一个回调函数),立刻就可以开始去做其他的事,用户线程不阻塞。
  • 内核就开始了IO的第一个阶段:准备数据(DMA复制)。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(CPU复制)。
  • 内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调方法,告诉用户线程,sys_read系统调用(数据搬运工作)已经完成了,数据已经读入到了用户缓冲区。
  • 用户线程读取用户缓冲区的数据(不需要做其他的系统调用),完成后续的业务操作。

异步IO模型的特点,在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO,并没有那么规范。

异步IO异步模型的缺点,应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。

理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。就目前而言,Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入(实际上是对IO多路复用做了封装,支持的不是太好),JDK的对其的支持目前并不完善(JDK不止对异步IO支持不完善,对IO多路复用支持都不完善,只支持select,默认不支持epoll),因此异步IO在性能上没有明显的优势。

大多数的高并发服务器端的程序,一般都是基于Linux系统的。因而,目前这类高并发网络应用程序的开发,大多采用IO多路复用模型。大名鼎鼎的Netty框架,使用的就是IO多路复用模型,而不是异步IO模型。

同步异步、阻塞非阻塞的区别联系

首先,同步和异步是针对应用程序(如Java)与内核的交互过程的方向而言的。

同步类型的IO操作,发起方是应用程序,接收方是内核。同步IO由应用进程发起IO操作,并阻塞等待,或者轮询的IO操作是否完成。异步IO操作,应用程序在提前注册完成回调函数之后去做自己的事情,IO交给内核来处理,在内核完成IO操作以后,启动进程的回调函数。

阻塞与非阻塞,关注的是用户进程在IO过程中的等待状态。前者用户进程需要为IO操作去阻塞等待,而后者用户进程可以不用为IO操作去阻塞等待。同步阻塞型IO、同步非阻塞IO、多路IO复用,都是同步IO,也是阻塞性IO

异步IO必定是非阻塞的,所以不存在异步阻塞和异步非阻塞的说法。真正的异步IO需要内核的深度参与。异步IO中的用户进程时候根本不去考虑IO的执行,IO操作主要交给内核去完成,而自己只等待一个完成信号。

IO多路复用,从操作系统维度来说,可以理解它是同步阻塞性IO(事件查询过程是阻塞的,CPU复制过程也是阻塞的),但是它又是建立在同步非阻塞IO上(对于每个连接来说,它必须是非阻塞的,把连接注册在查询选择器上,由选择器查询事件);但从应用层来说,因为应用都是多线程的,可以做成非阻塞的架构。

本文由作者按照 CC BY 4.0 进行授权

© ManShouyuan. 保留部分权利。

本站总访问量 本站访客数人次

🚩🚩🚩🚩🚩🚩