[Netty]IO本质,IO简介分类,IO模型发展(一)
Netty是高性能的网络编程框架,基于Netty使我们的业务逻辑代码与网络传输部分的代码,可以进行很好地分离,更容易的进行应用的开发。Netty的编程模型基于NIO,或者说与NIO的思维方式同出一辙,而介绍NIO,就不得不从IO的模型、分类开始说起,这样更具备系统性,也跟容易理解。
> Netty is *an asynchronous event-driven network application framework*
> for rapid development of maintainable high performance protocol servers & clients.
本专栏系列大致路线为从IO介绍开始,然后梳理NIO,最后过度到Netty。
所以接下来对IO简单介绍
### 宏观的IO模型
!(data/attachment/forum/202207/11/101613r3mcyizmuhy1qhrr.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
从宏观的角度看,我们的应用程序,都是运行在操作系统之上的。
现代操作系统,负责管理硬件资源,所有的应用程序都是构建于操作系统之上的,为了保护硬件等资源,操作系统设置了一定的机制和保护机制或者说隔离机制。
##### 内核与用户
资源的访问,需要依赖操作系统,也就是在用户应用程序执行的时候,需要切换到操作系统。
操作系统将内存(虚拟内存)划分为两部分,一部分是内核空间(Kernel-Space),一部分是用户空间(User-Space)。
这就是我们常说的用户程序、内核程序,用户态、内核态,用户空间、内核空间等都是类似的意思。
了解过操作系统的应该对这块都不陌生。
操作系统提供了用户接口以及应用程序接口。
用户接口比如控制台输入命令行进行人机交互;
应用程序接口比如调用系统函数实现某种功能;
对于IO,用户程序进行IO的读写,依赖于底层的IO读写,基本上会用到底层的两大系统调用:
**系统读 sys_read & 系统写 sys_write 。**
##### 缓冲区
为了提高执行效率,均有设置缓存区,有用户缓冲区和内核缓冲区。
这样当系统获得足够的资源后才会进行切换响应,减少频繁地与设备之间的物理交换
计算机的外部物理设备与内存与CPU相比,有着非常大的差距,外部设备的直接读写,涉及操作系统的中断。
发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。
这就是必须要设置缓冲区的原因。
!(data/attachment/forum/202207/11/102028t00kiqf018alf4lq.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
当发生调用时,其实是缓冲区之间的读取和写入。
* 应用程序使用sys_read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(用户缓冲区);
* 上层应用使用sys_write系统调用时,仅仅把数据从应用的用户缓冲区复制到内核缓冲区中;
### 异步 同步 阻塞 非阻塞
日常中经常听到这样几个概念,到底如何理解?
**同步异步,我们指的是两个或者以上的进程之间,一种协作的状态,协作方式,是从全局更高的角度 “进程之间 合作的方式”,关注的是消息的通信机制;**
同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
也就是由调用者主动等待这个调用的结果。
不管是一直卡住不动的等待,还是后期主动地不断地轮训,都是属于同步。
异步就是调用发出之后,这个调用就返回了,是否有结果并不影响继续执行,当有了结果之后,通过事件、状态、回调等方式进行通知调用者
这就是异步。
> 同步需要主动关注结果,异步则是被动的接受通知
**阻塞与非阻塞相对来说比较容易理解,就是调用方在请求时可能会出现的状态。**
指的是单一进程视角观察到的状态,当发起调用时,自身被阻塞不能继续进行,那么称之为阻塞,反之则是非阻塞。
> 发出请求后是自己主动的获取结果还是等通知?决定了是同步还是异步。
> 发布请求后,自身是否会被卡住还是可以继续执行?这决定了是阻塞还是非阻塞。
### POSIX IO分类
* 阻塞式IO(Blocking IO)
* 非阻塞式(IONon-Blocking IO)
* IO多路复用(IO Multiplexing)
* 信号驱动式IO
* 异步IO(Asynchronous IO)
不管是什么类型的IO,基本模型都是请求-响应,这两个阶段。
#### 阻塞式IO(Blocking IO)
除了特指NIO以及AIO,我们平时说的IO通常指的就是阻塞的。
!(data/attachment/forum/202207/11/102703jwziif96cjfpiulj.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
recvfrom视为系统调用
第一阶段应用程序通过recvfrom从系统中获取数据。
最初系统内核中肯定是没有数据的,所以需要准备数据,这需要一个过程。
准备好了之后,在第二个阶段,需要复制到用户空间。
用户空间其实就是应用程序所在的空间,应用程序就是用户程序,区别于系统程序和内核空间。
这就是典型的阻塞模式的IO,在recvfrom 返回数据之前,会一直阻塞,应用程序的进程将会一直卡住,无法做其他的事情。
调用了方法,方法不返回,你不能动。
###### 阻塞IO的特点是:
在内核进行IO执行的两个阶段,发起IO请求的用户进程(或者线程)被阻塞了。
###### 阻塞IO的优点是:
应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起,用户线程基本不会占用CPU资源。
###### 阻塞IO的缺点是:
一般情况下,会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作。
在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。
`本文作者:程序员潇然 疯狂的字节X https://crazybytex.com/`
在高并发应用场景中,阻塞IO模型是性能很低的,基本上是不可用的。也就是说如果是高并发,阻塞IO模型是不行的。
#### 非阻塞式(IONon-Blocking IO)
!(data/attachment/forum/202207/11/102930avfuevwwwhttvyns.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间去执行后续的指令。
即发起IO请求的用户进程(或者线程)处于非阻塞的状态,与此同时,内核会立即返回给用户一个IO的状态值。
第一个阶段,前面几次的recvfrom调用,并没有数据可以返回,所以返回了一个EWOULDBLOCK;
第二个阶段,第四次调用的时候,有数据准备好,被复制到用户空间,于是recvfrom调用成功返回,用户进程可以继续处理数据;
PS:
对于这种对一个非阻塞描述符,循环调用的形式,通常称之为轮训(polling),很显然这会浪费CPU资源,因为一直在空循环做无用功,而且期间如果什么都没做,只是单纯地循环,那么事实上与阻塞式无异了。
阻塞与非阻塞的区别很简单,就是没有拿到结果之前,你有没有机会做其他的事情。
阻塞是指用户进程(或者线程)一直在等待,而不能干别的事情;
非阻塞是指用户进程(或者线程)拿到内核返回的状态值就返回自己的空间,可以去干别的事情;
###### 非阻塞IO的特点:
应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。
###### 非阻塞IO的优点:
每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
###### 非阻塞IO的缺点:
不断地轮询内核,这将占用大量的CPU时间,效率低下。
总体来说,在高并发应用场景中,非阻塞IO也是性能很低的,也是基本不可用的,一般Web服务器都不使用这种IO模型。
在Java的实际开发中,也不会涉及这种IO模型。
但是此模型还是有价值的,其他IO模型中可以使用非阻塞IO模型作为基础,以实现其高性能
**特别注意:**
此处讨论的是IO模型,而不是java类库的NIO。
同步非阻塞 IO 也可以简称为 NIO,但是,它不是 Java 编程中的 NIO,虽然它们的英文缩写一样,但是不能混淆。
Java 的 NIO(New IO)类库组件,所归属的不是基础 IO 模型中的NIO( None Blocking IO)模型,而是另外的一种模型,叫做 IO 多路复用模型( IO Multiplexing)
此处也特别提醒一下,当阐述概念模型时,以及具体类库、方法的名字等,如果有同名现象,并不代表说的是同一件事情。
#### IO多路复用(IO Multiplexing)
!(data/attachment/forum/202207/11/103325rbyfgefz0atlfyff.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
第一个阶段,发起调用时,阻塞在select上,等待数据变为可读。
第二个阶段,当返回可读状态时,就可以通过recvfrom把读取到的数据包复制到用户缓冲区。
select也是阻塞,那有什么优势?
而且,多使用一次调用,本来是通过recvfrom 现在是还需要额外的select。
为了提高性能,操作系统引入了一类新的系统调用,专门用于查询IO文件描述符的(含socket连接)的就绪状态。
> 在Linux系统中,新的系统调用为select/epoll系统调用。
> 通过这种系统调用,解决了轮询的问题,提高了轮询的性能
> 因为一个进程可以监视多个文件描述符(包括socket连接),一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。
> 随后,应用程序根据就绪的状态,进行相应的IO系统调用。
> 只要有可读取的数据,就可以得到进一步的执行!当连接数量增加之后,这一特性将变得非常有用。
> (大量并发情况下效率提升,如果只有一个,并没有什么优势)
注意:
目前支持IO多路复用的系统调用,有select、 epoll等等。
select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。
epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。
在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接的就绪状态,当某个或者某些socket网络连接有IO就绪状态。
就返回这些就绪的状态(或者说就绪事件)。
##### 流程如下:
###### ( 1)选择器注册。
在这种模式中,首先,将需要sys_read操作的目标文件描述符( socket连接),提前注册到Linux的select/epoll选择器中,在Java中所对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程。
###### ( 2)就绪状态的轮询。
通过选择器的查询方法,查询所有的提前注册过的目标文件描述符( socket连接)的IO就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好或者就绪了,就是内核缓冲区有数据了,内核就将该socket加入到就绪的列表中,并且返回就绪事件。
###### (3)发起调用。
用户线程获得了就绪状态的列表后,根据其中的socket连接,发起sys_read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
###### (4)返回结果继续执行。
复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。
###### IO多路复用模型的优点:
一个选择器查询线程,可以同时处理成千上万的网络连接。
所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。
这是一个线程维护一个连接的阻塞IO模式相比,使用多路IO复用模型的最大优势
通过JDK的源码可以看出, Java语言的NIO(New IO)组件,在Linux系统上,是使用的是select系统调用实现的。
所以, Java语言的NIO(New IO)组件所使用的,就是IO多路复用模型。
###### 和NIO模型相似,多路复用IO也需要轮询。
负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。
**但是同时轮询了所有的连接,而不是每一个线程自己轮询。**
###### IO多路复用模型的缺点:
本质上, select/epoll系统调用是阻塞式的,属于同步阻塞IO。
都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个事件的查询过程是阻塞的。
如果彻底地解除线程的阻塞,就必须使用异步IO模型。
### 信号驱动式IO
!(data/attachment/forum/202207/11/103821p76t77tnjjdydlcg.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
在信号驱动IO模型中,第一个阶段,用户线程发起调用时,通过向核心注册IO事件的回调函数,来避免IO时间查询的阻塞。
用户进程预先在内核中设置一个回调函数。
当某个事件发生时,内核使用信号(SIGIO)通知进程运行回调函数。
然后进入IO操作的第二个阶段——执行阶段:
用户线程会继续执行,在信号回调函数中调用IO读写操作来进行实际的IO请求操作
**信号驱动模式,在初始阶段,等待通知,不会被阻塞。**
但是从上图可以看到,**调用recvfrom之后,仍旧是阻塞的,需要等待数据复制完成**
所以信号驱动模式,也被称之为 低配的 异步模式,或者不完全异步模式
信号驱动IO模型,每当套接字发生IO事件时,系统内核都会向用户进程发送SIGIO事件
所以,一般用于UDP传输,在TCP套接字的开发过程中很少使用,原因是SIGIO信号产生得过于频繁,并且内核发送的SIGIO信号,并没有告诉用户进程发生了什么IO事件。
但是在UDP套接字上, 通过SIGIO信号进行下面两个事件的类型判断即可:
1。数据报到达套接字
2 套接字上发生错误
因此,在SIGIO出现的时候, 用户进程很容易进行判断和做出对应的处理: 如果不是发生错误,那么就是有数据报到达了。
##### 信号驱动IO优点:
用户进程在等待数据时,不会被阻塞,能够提高用户进程的效率。
在信号驱动式I/O模型中,应用程序使用套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。
##### 信号驱动IO缺点:
1 在大量IO事件发生时,可能会由于处理不过来,而导致信号队列溢出。
2 对于处理UDP套接字来讲,对于信号驱动I/O是有用的。可是,对于TCP而言,由于致使SIGIO信号通知的条件为数众多,进行IO信号进一步区分的成本太高,信号驱动的I/O方式近乎无用。
3 信号驱动IO可以看成是一种异步IO,可以简单理解为系统进行用户函数的回调。
只是,信号驱动IO的异步特性,又做的不彻底。
信号驱动IO仅仅在IO事件的通知阶段是异步的, 而在第二阶段, 也就是在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程是阻塞的、同步的
### 异步IO(Asynchronous IO)
!(data/attachment/forum/202207/11/105622kxkkhktqzbkkjzkd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
#### AIO的基本流程:
用户线程通过系统调用,向内核注册某个IO操作。
内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
在异步IO模型中,在整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。
用户空间与内核空间的调用方式产生了翻转。
第一阶段,通过调用,直接返回,不会发生阻塞,也不需要等待,可以继续进行下一步的操作。
第二阶段,数据从内核空间复制到用户空间之后,通知用户进程,用户进程无需等待数据的拷贝。
在第二阶段,内核空间成了主动调用者。
异步IO类似于Java中典型的回调模式,用户进程(或者线程)向内核空间注册了各种IO事件的回调函数,由内核去主动调用。
##### 异步IO包含两种:
* 不完全异步的信号驱动IO模型
* 完全的异步IO模型
##### 异步IO模型的特点:
在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。
用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。
正因为如此,异步IO有的时候也被称为信号驱动IO。
##### 异步IO异步模型的缺点:
应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。
理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。
就目前而言, Windows系统下通过IOCP实现了真正的异步IO。
而在Linux系统下,异步IO模型在2.6版本才引入, JDK的对其的支持目前并不完善,因此异步IO在性能上没有明显的优势。
!(data/attachment/forum/202207/11/105833gaberr5maf5v5mn5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
从上面的图中,可以发现,其实前面四种都会有阻塞,只有异步IO模型 才符合POSIX关于异步IO的定义。
注:以上截图部分来自于《UNIX网络编程》
小结:
以上为操作系统视角分析的IO的模型,而Java的NIO是基于IO多路复用的,所以深入理解NIO的模型,对于后面Netty的展开有重大意义。
因为NIO的面向对象的设计模型,就是基于IO多路复用的抽象。
!(data/attachment/forum/202206/16/141330jha7st9soow8772i.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "common_log.png")
`转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-48-1-1.html `
页:
[1]