IO多路复用

要想学习netty就先要了解:(网络编程模型:BIO、NIO、AIO)

IO

image
上图的工作模式:

  • 开始时应用程序会发一个请求给CPU,CPU得到通知后,此时CPU就需要调用操作系统内核程序(磁盘控制器)。这就是用户态->内核态
  • 磁盘控制器接到通知,使用DMA拷贝技术将数据放到PageCache内核缓冲区中,再由CPU把内核缓冲区中的数据传回用户缓冲区中(buffer)。这就是内核态->用户态

请求数据时,IO操作通常包括两个部分(使用到的IO模型如下)

  • 等待内核缓冲区中的数据准备好
  • 将内核缓冲区中的数据拷贝到用户缓冲区中(需要使用零拷贝进行优化,详情见零拷贝

一、IO模型

阻塞IO:程序请求操作系统IO,如果内核缓冲区中没有准备好数据,进程则会进行等待。
非阻塞IO:程序请求操作系统IO,如果内核缓冲区中没有准备好数据,进程会继续执行,不断进行系统调用直到IO数据准备好。
同步IO:操作系统收到程序请求后,如果内核缓冲区中没有准备好数据,进程不会响应,直到数据准备好才会响应往下执行。
异步IO:操作系统收到程序请求后,如果内核缓冲区中没有准备好数据,会返回一个标记,程序继续往下执行。当数据准备好之后,会以事件的方式通知。

1.1同步IO

1、阻塞式IO(两个步骤都需要等待)

应用进程被阻塞,直到内核缓冲区中的数据复制到应用进程缓冲区中才返回。
image

2、非阻塞式IO(第一个步骤不断询问数据是否准备好不需要等待,第二步需要等待)

应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知I/O是否完成,这种方式称为轮询。(polling)。
image

3、I/O多路复用(两个步骤都需要等待)

1、使用select或者poll等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回,之后再使用recvfrom 把数据从内核复制到进程中。
2、使用select或者poll,可以让单个线程具有处理多个I/O事件的能力
image

IO多路复用的实现(详情查看大佬:IO多路复用

多路复用的实现:select、poll、epoll,这些函数都是系统调用函数。

select(一次select系统调用+N次就绪的客户端的read系统调用)

select工作原理

  • 当通过一个线程调用select,将保存客户端连接的数组拷贝一份给内核态,在内核层遍历该数组看那个客户端已经准备好数据,将准备好的客户端的个数返回给用户态,用户态需要遍历数组,找到准备好数据的客户端,然后进行read系统调用将内核缓冲区中的数据拷贝到用户缓冲区中。

select存在的问题

  1. select 调用需要传入保存客户端连接的数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
  2. select 在内核层仍然是通过遍历的方式检查客户端的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销,只在内核态遍历。(内核层可优化为异步事件通知)
  3. select 仅仅返回就绪的客户端的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的客户端标识,无需用户做无效的遍历)
poll

它和select 的主要区别就是,去掉了select只能监听1024个客户端的限制。

epoll

epoll对select和poll进行了优化:

  1. 只在内核中存储保存客户端连接的数组,无需用户每次都重新传入,只需告诉内核修改的部分即可。
  2. 内核不再通过轮询的方式找到就绪的客户端,而是通过异步IO事件通知用户态已经就绪的客户端。
  3. 内核仅会将有IO事件的客户端标识返回给用户,用户也无需遍历整个文件描述符集合。

由于Linux下没有Windows下的IOCP技术提供真正的异步IO支持,所以Linux下使用epoll模拟异步IO

4、信号驱动IO(第一步不会等待,第二步会等待)

1、应用进程使用sigaction系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。
2、内核在数据到达时向应用进程发送SIGIO信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
image

1.2异步IO(两个步骤都不需要等待)

进行aio_read系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
image

二、IO的发展

image

2.1 BIO、NIO、AIO的区别

Java BIO同步并阻塞,服务器采用线程池为一个客户端连接创建一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

  • 连接数目比较小且固定的架构,对服务器资源要求比较高

Java NIO同步非阻塞(IO多路复用),服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。

  • 连接数目多且连接比较短(轻操作)的架构:聊天服务器,弹幕系统,服务器间通讯

Java AIO(NIO.2)异步非阻塞,AIO 引入异步通道的概念,采用了 Rector/Proactor 模式,当用户态访问内核会立即返回一个标记,由内核会在所有操作完成之后向应用进程发送信号,一般适用于连接数较多且连接时间较长的应用。

  • 连接数目多且连接比较长(重操作)的架构:相册服务器

2.2 NIO的三个核心部分

  • BIO 以的方式处理数据,而 NIO 以的方式处理数据,块 I/O 的效率比流 I/O 高很多。
  • BIO 是阻塞的,NIO 则是非阻塞的。
  • BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。Buffer和Channel之间的数据流向是双向的

channel

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲

channel类

  • FileChannel:用于文件的数据读写
  • DatagramChannel:用于 UDP 的数据读写
  • SocketChannel:用于 TCP 的数据读写
  • ServerSocketChannel:用于UDP和TCP数据的读写

channel类中几个重要的方法

  • public int read(ByteBuffer dst):从通道读取数据并放到缓冲区中(用于读取文件中的数据)
  • public int write(ByteBuffer src):把缓冲区的数据写到通道中(用于把数据写到文件中)
  • public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道中复制数据到当前通道
  • public long transferTo(long position, long count, WritableByteChannel target):把数据从当前通道复制给目标通道

buffer

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。对文件的读取或写入都必须经由Buffer

buffer的子类
image

Buffer缓冲区有两种工作模式

  • 写模式:当通道中的数据读取到buffer中时,buffer为写模式
  • 读模式:当把buffer中的数据写入到通道时,buffer为读模式

buffer缓冲区中的四大属性
需要使用buffer.flip反转读模式和写模式
写模式
image

读模式
image

  • position: 缓存区分配下一个要被读或写的元素的索引,每次读写缓冲区改值会自动加1
  • limit: 缓存区最大可以进行操作的index位置。缓存区的读写状态正式由这个属性控制的,可以改变值。
  • capacity: 缓存区的最大容量。这个容量是在缓存区创建时进行指定的。不可以改变值。
  • mark:在当前缓冲区进行position时设置一个标记mark1,当执行buffer的reset方法时,可以把position复位到mark1

selector

1、Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

2、使用selector检测,当通道中有事件发生时会主动通知selector,可以减少多线程之间的上下文切换(1个selector系统调用+N次就绪的客户端的read系统调用)。
传统做法是需要循环访问所有通道是否有事件发生,这样在数据量大的情况下,会导致消耗内存资源严重

Selector相关方法说明
selector.select(); //阻塞
selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
selector.wakeup(); //唤醒 selector
selector.selectNow(); //不阻塞,立马返还

2.3 Selector、Channel 和 Buffer 关系图

image

  • 每个 Channel 都会对应一个 Buffer。
  • Selector对应一个线程,一个线程对应多个 Channel(连接)。
  • 该图反应了有三个 Channel 注册到该 Selector上程序
  • Selector 会根据不同的事件,在各个通道上切换。程序切换到哪个Channel 是由事件决定的,Event 就是一个重要的概念。
  • Buffer 就是一个内存块,底层是有一个数组。
  • 数据的读取写入是通过Buffer,这个和BIO是不同的,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip方法切换Channel是双向的。
posted @ 2022-03-16 16:11  阿宁你好啊  阅读(1569)  评论(0编辑  收藏  举报