IO多路复用
要想学习netty就先要了解:(网络编程模型:BIO、NIO、AIO)
IO
上图的工作模式:
- 开始时应用程序会发一个请求给CPU,CPU得到通知后,此时CPU就需要调用操作系统内核程序(磁盘控制器)。这就是用户态->内核态。
- 磁盘控制器接到通知,使用DMA拷贝技术将数据放到PageCache内核缓冲区中,再由CPU把内核缓冲区中的数据传回用户缓冲区中(buffer)。这就是内核态->用户态。
请求数据时,IO操作通常包括两个部分(使用到的IO模型如下)
- 等待内核缓冲区中的数据准备好
- 将内核缓冲区中的数据拷贝到用户缓冲区中(需要使用零拷贝进行优化,详情见零拷贝)
一、IO模型
阻塞IO:程序请求操作系统IO,如果内核缓冲区中没有准备好数据,进程则会进行等待。
非阻塞IO:程序请求操作系统IO,如果内核缓冲区中没有准备好数据,进程会继续执行,不断进行系统调用直到IO数据准备好。
同步IO:操作系统收到程序请求后,如果内核缓冲区中没有准备好数据,进程不会响应,直到数据准备好才会响应往下执行。
异步IO:操作系统收到程序请求后,如果内核缓冲区中没有准备好数据,会返回一个标记,程序继续往下执行。当数据准备好之后,会以事件的方式通知。
1.1同步IO
1、阻塞式IO(两个步骤都需要等待)
应用进程被阻塞,直到内核缓冲区中的数据复制到应用进程缓冲区中才返回。
2、非阻塞式IO(第一个步骤不断询问数据是否准备好不需要等待
,第二步需要等待)
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知I/O是否完成,这种方式称为轮询。(polling)。
3、I/O多路复用(两个步骤都需要等待)
1、使用select或者poll等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回,之后再使用recvfrom 把数据从内核复制到进程中。
2、使用select或者poll,可以让单个线程具有处理多个I/O事件的能力。
IO多路复用的实现(详情查看大佬:IO多路复用)
多路复用的实现:select、poll、epoll,这些函数都是系统调用函数。
select(一次select系统调用+N次就绪的客户端的read系统调用)
select工作原理:
- 当通过一个线程调用select,将保存客户端连接的数组
拷贝
一份给内核态,在内核层遍历该数组看那个客户端已经准备好数据,将准备好的客户端的个数
返回给用户态,用户态需要遍历
数组,找到准备好数据的客户端,然后进行read系统调用将内核缓冲区中的数据拷贝到用户缓冲区中。
select存在的问题:
- select 调用需要传入保存客户端连接的数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
- select 在内核层仍然是通过遍历的方式检查客户端的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销,只在内核态遍历。(内核层可优化为异步事件通知)
- select 仅仅返回就绪的客户端的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的客户端标识,无需用户做无效的遍历)
poll
它和select 的主要区别就是,去掉了select只能监听1024个客户端的限制。
epoll
epoll对select和poll进行了优化:
- 只在内核中存储保存客户端连接的数组,无需用户每次都重新传入,只需告诉内核修改的部分即可。
- 内核不再通过轮询的方式找到就绪的客户端,而是通过异步IO事件通知用户态已经就绪的客户端。
- 内核仅会将有IO事件的客户端标识返回给用户,用户也无需遍历整个文件描述符集合。
由于Linux下没有Windows下的IOCP技术提供真正的异步IO支持,所以Linux下使用epoll模拟异步IO
4、信号驱动IO(第一步不会等待,第二步会等待)
1、应用进程使用sigaction系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。
2、内核在数据到达时向应用进程发送SIGIO信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
1.2异步IO(两个步骤都不需要等待)
进行aio_read系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
二、IO的发展
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的子类
Buffer缓冲区有两种工作模式
- 写模式:当通道中的数据读取到buffer中时,buffer为写模式
- 读模式:当把buffer中的数据写入到通道时,buffer为读模式
buffer缓冲区中的四大属性
需要使用buffer.flip反转读模式和写模式
写模式
读模式
- 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 关系图
- 每个 Channel 都会对应一个 Buffer。
- Selector对应一个线程,一个线程对应多个 Channel(连接)。
- 该图反应了有三个 Channel 注册到该 Selector上程序
- Selector 会根据不同的事件,在各个通道上切换。程序切换到哪个Channel 是由事件决定的,Event 就是一个重要的概念。
- Buffer 就是一个内存块,底层是有一个数组。
- 数据的读取写入是通过Buffer,这个和BIO是不同的,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip方法切换Channel是双向的。