likes
comments
collection
share

Netty专栏(1)--学习Netty 一定要明白的IO的模型

作者站长头像
站长
· 阅读数 48

在讲解高并发IO的底层原理之前,我们需要了解几个概念

用户空间和内核空间

在计算机操作系统中,用户空间和内核空间指的是不同的内存空间。用户空间是指应用程序可以访问的内存空间,而内核空间是指操作系统内核可以访问的内存空间。用户空间和内核空间之间的访问必须通过系统调用来完成,通过系统调用可以将数据传递到内核中进行处理,然后再将结果传递回来。这样做的好处是可以提高系统的安全性,防止应用程序对系统资源的滥用。

内核缓冲区

内核缓冲区是指内核中的一块缓冲区,用于存放从设备中读取的数据或将数据写入设备。当应用程序需要读写设备时,它并不直接与设备交互,而是通过系统调用将数据传递给内核,然后内核将数据从设备中读取或写入设备,最后再将数据传递给应用程序。这样做的好处是可以减少用户态和内核态之间的切换次数,提高系统的性能。

进程缓冲区

进程缓冲区是指应用程序中的一块缓冲区,用于存放从内核中读取的数据或将数据写入内核。当应用程序需要读写设备时,它并不直接与设备交互,而是通过系统调用将数据传递给内核,然后内核将数据从设备中读取或写入设备,最后再将数据传递给应用程序的进程缓冲区中。这样做的好处是可以减少内核态和用户态之间的切换次数,提高系统的性能。

为什么设置缓冲区?

为了提高系统的性能,减少用户态和内核态之间的切换次数,应用程序不直接与设备交互,而是通过系统调用将数据传递给内核,然后内核将数据从设备中读取或写入设备,最后再将数据传递给应用程序。内核缓冲区和进程缓冲区的作用就是在应用程序和设备之间起到缓冲的作用,减少数据传输的次数,从而提高系统的性能。

应用程序接收网络传输数据的底层逻辑

Netty专栏(1)--学习Netty 一定要明白的IO的模型

当应用程序等待数据通过网络中到达网卡时,操作系统会自动将数据从网络传输层复制到内核缓冲区中。这个过程对用户程序是透明的。然后,内核会将数据从内核缓冲区复制到应用程序的用户缓冲区中。

常见的IO模型有以下几种:

  • 同步阻塞IO模型(Blocking IO Model)
  • 同步非阻塞IO模型(Non-blocking IO Model)
  • IO复用模型(IO Multiplexing Model)
  • 信号驱动IO模型(Signal Driven IO Model)
  • 异步IO模型(Asynchronous IO Model)

同步与异步

同步与异步可以看成是发起IO请求的两种方式。同步IO是指用户空间(进程或者线程)是主动发起IO请求的一方,系统内核是被动接 受方。异步IO则反过来,系统内核主动发起IO请求的一方,用户空间是被动接受方。

阻塞与非阻塞

阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间执行用户程序的操作指令,阻塞一词所指的是用户程序(发起IO请求的进程或者线程)的执行状态是阻塞的。可以说传统的IO模型都是阻塞IO模型,并且在Java中,默认创建的socket都属于阻塞IO模型。

*阻塞和非阻塞的区别是什么呢? *阻塞是指用户进程(或者线程)一直在等待,而不能干别的事情;非阻塞是指用户进程(或者线程)拿到内核返回的状态值就返回自己的空间,可以去干别的事情。在Java中,非阻塞IO的socket套接字,要求被设置为NONBLOCK模式。

同步阻塞模型(Blocking IO Model)

是指应用程序在执行IO操作时,必须等待IO操作完成才能继续执行下一步操作。在这种模型下,应用程序调用系统调用后,系统会一直等待IO操作完成,直到数据被读取或写入到缓冲区中才会返回结果,期间应用程序会被阻塞。这种模型适用于一些需要数据完整性和可靠性的场景,但是会导致系统资源浪费和性能瓶颈。

Netty专栏(1)--学习Netty 一定要明白的IO的模型

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

阻塞IO的优点是

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

阻塞IO的缺点是

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

同步非阻塞模型(Non-blocking IO Model)

是指应用程序在执行IO操作时,无需等待IO操作完成就可以继续执行下一步操作。在这种模型下,应用程序通过系统调用向内核请求IO操作,如果内核缓冲区中没有数据,内核会立即返回一个错误码,应用程序可以继续执行其他操作。应用程序需要不断轮询内核缓冲区,直到数据被读取或写入到缓冲区中,然后再处理数据。这种模型适用于一些需要高并发、高吞吐量的场景,但是会导致CPU资源浪费,因为应用程序需要不断轮询内核缓冲区,直到数据到达缓冲区中。

在NIO模型中,应用程序一旦开始IO系统调用,会出现以下两种情况: (1)在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的 信息。 (2)在内核缓冲区中有数据的情况下,在数据的复制过程中系统调用是阻塞的,直到 完成数据从内核缓冲复制到用户缓冲。复制完成后,系统调用返回成功,用户进程(或者线 程)可以开始处理用户空间的缓存数据。

Netty专栏(1)--学习Netty 一定要明白的IO的模型

同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已 经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。 同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。 用户线程不会阻塞,实时性较好。 同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。

同步非阻塞 IO 也可以简称为 NIO,但是,它不是 Java 编程中的 NIO,虽然它们的英文缩写一样,但是不能混淆。Java 的 NIO(New IO)类库组件,所归属的不是基础 IO 模型中的 NIO(None Blocking IO)模型,而是另外的一种模型,叫做 IO 多路复用模型(IOMultiplexing)。

**IO多路复用模型(IO Multiplexing Model)**是指应用程序可以同时监视多个文件描述符的IO状态。在这种模型下,应用程序可以将多个文件描述符注册到内核中,然后通过系统调用等待这些文件描述符中的任意一个或多个文件描述符变为可读或可写状态。当有文件描述符变为可读或可写状态时,内核会通知应用程序,应用程序就可以进行读写操作。这种模型适用于一些需要同时监视多个文件描述符的场景,可以提高系统的并发性能。

在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。在Linux系统中,对应的系统调用为select/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符(包括socket连接),一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。

举个例子来说明IO多路复用模型的流程

发起一个多路复用IO的sys_read读操作的系统调用,流程如下:

(1)选择器注册。在这种模式中,首先,将需要sys_read操作的目标文件描述符(socket连接),提前注册到Linux的select/epoll选择器中,在Java中所对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程。

(2)就绪状态的轮询。通过选择器的查询方法,查询所有的提前注册过的目标文件描述符(socket连接)的IO就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好或者就绪了,就是内核缓冲区有数据了,内核就将该socket加入到就绪的列表中,并且返回就绪事件。

(3)用户线程获得了就绪状态的列表后,根据其中的socket连接,发起sys_read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。

(4)复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

Netty专栏(1)--学习Netty 一定要明白的IO的模型

IO多路复用模型的特点

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

注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。只是这一点对于用户程序而言,是无感知的。

IO多路复用模型的优点

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

IO多路复用模型的缺点

本质上,select/epoll系统调用是阻塞式的,属于同步阻塞IO。 都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个事件的查询过程是 阻塞的。

信号驱动IO模型(Signal Driven IO Model)

是指应用程序通过系统调用向内核请求IO操作,并注册一个信号处理函数,当IO操作完成时,内核会发送一个信号给应用程序,应用程序在信号处理函数中处理数据。这种模型适用于一些需要高并发、高吞吐量的场景,因为应用程序不需要不断轮询内核缓冲区,而是可以继续执行其他操作,当数据到达内核缓冲区时,内核会发送一个信号给应用程序,应用程序在信号处理函数中处理数据。这种模型可以避免CPU资源的浪费,提高系统的并发性能。

具体的做法是,用户进程预先在内核中设置一个回调函数,当某个事件发生时,内核使 用信号(SIGIO)通知进程运行回调函数。 然后用户线程会继续执行,在信号回调函数中 调用IO读写操作来进行实际的IO请求操作。 信号驱动IO的基本流程是:用户进程通过系统调用,向内核注册SIGIO信号的owner进 程和以及进程内的回调函数。内核IO事件发生后(比如内核缓冲区数据就位)后,通知用 户程序,用户进程通过sys_read系统调用,将数据复制到用户空间,然后执行业务逻辑。

Netty专栏(1)--学习Netty 一定要明白的IO的模型

信号驱动IO模型,每当套接字发生IO事件时,系统内核都会向用户进程发送SIGIO事件,

所以,一般用于UDP传输,在TCP套接字的开发过程中很少使用,原因是SIGIO信号产生得

过于频繁,并且内核发送的SIGIO信号,并没有告诉用户进程发生了什么IO事件。

但是在UDP套接字上,通过SIGIO信号进行下面两个事件的类型判断即可:

1 数据报到达套接字

2 套接字上发生错误

因此,在SIGIO出现的时候,用户进程很容易进行判断和做出对应的处理:如果不是发生错误,那么就是有数据报到达了。

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

(1)设置SIGIO信号的信号处理回调函数。

(2)设置该套接口的属主进程,使得套接字的IO事件发生时,系统能够将SIGIO信号传递给属主进程,也就是当前进程。

(3)开启该套接口的信号驱动I/O机制,通常通过使用fcntl方法的F_SETFL操作命令,使能(enable)套接字的 O_NONBLOCK非阻塞标志和O_ASYNC异步标志完成。

完成以上三步,用户进程就完成了事件回调处理函数的设置。当文件描述符上有事件发

生时,SIGIO 的信号处理函数将被触发,然后便可对目标文件描述符执行 I/O 操作。

信号驱动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 Model)

是指应用程序通过系统调用向内核请求IO操作,并注册一个回调函数,在IO操作完成后,内核会自动调用回调函数,将数据传递给应用程序。这种模型适用于一些需要高并发、高吞吐量的场景,因为应用程序不需要不断轮询内核缓冲区,而是可以继续执行其他操作,当数据到达内核缓冲区时,内核会自动调用回调函数,将数据传递给应用程序。这种模型可以避免CPU资源的浪费,提高系统的并发性能。

在异步IO模型中,在整个内核的数据处理过程中,包括内核将数据从网络物理设备(网 卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。

Netty专栏(1)--学习Netty 一定要明白的IO的模型

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

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

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

异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。就目前而言,Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入,JDK的对其的支持目前并不完善,因此异步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操作主要交给内核去完成,而自己只等待一个完成信号。

转载自:https://juejin.cn/post/7229676998750273597
评论
请登录