Java IO模型
# 什么是IO?
冯诺伊曼提出的计算机体系结构由输入和输出、以及运算器、控制器、存储器构成。
实际上IO
就是计算机与外部设备数据交互的过程。为了保证操作系统的稳定性和安全性,操作系统会将空间分为用户空间
和内核空间
。 我们平常所运行的程序都处于用户态,当我们需要进行文件管理、进程通信、内存管理,都需要从用户态转为内核态执行才能执行这些特权指令。并且,用户空间的程序不能直接访问内核空间。当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
所以我们应用程序执行IO
时,会进行以下步骤:
- 应用程序发起
IO
请求,这个特权指令会从用户态转到内核执行。 - 内核等待
IO
准备好数据。 - 内核
IO
数据准备完毕,将数据从内核空间
拷贝用户空间
。
# 深入内核理解网络数据包IO过程
# 网络数据包接收过程
如下图所示,自底向上,首先收到一个网络包后首先会到达网卡,然后通过DMA(直接存储器访问)环形缓冲区
。完成数据拷贝之后,发起一次硬中断
通知CPU
有网络数据来了。然后CPU
将这个数据拷贝到sk_buffer
,再发起一次软中断将数据向上传输。注意,若有多个CPU
则哪个CPU
发起硬中断,则就是哪个CPU
发起软中断。然后就是不断解数据包头的过程,数据会不断解包,以下图为例,我们得知最终的报文是一个TCP
数据包,我们就会根据socket四元组将数据拷贝到socket缓冲区
,反之返回一个目的不可达的icmp包
。 最后数据从socket
缓冲区拷贝到用户数据区read
返回。
性能开销大在这几处:
DMA
拷贝到内存的开销。- 硬中断的开销。
- 软中断的开销。
- 网络数据从内核空间拷贝到用户空间的开销。
- 整个调用过程从用户态转内核态再从内核态转为内核态的过程。
# 网络数据包发送过程
发送过程和接收过程类似,整体如下:
- 应用程序发起
send
请求,将数据从用户空间拷贝到内核空间。 - 封装成
struct msghdr
对象加入socket
队列中 - 需要发送时进行封装处理。
- 然后经过软中断、硬中断到达硬件层发送出去
# Java中常见的IO模型
# BIO (Blocking I/O)
BIO 属于同步阻塞 IO 模型 。同步阻塞 IO 模型中,应用程序发起 read
调用后,会一直阻塞,直到内核把数据拷贝到用户空间
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO
模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
# NIO(Non-blocking/New I/O)
Java 中的 NIO
可以看作是 I/O 多路复用模型,很多人它属于同步非阻塞模型,其实不对,如下说明两者区别
# 同步非阻塞IO模型
同步非阻塞 IO 模型中,应用程序会一直发起 read
调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
# I/O 多路复用模型
IO 多路复用模型中,线程首先发起 select
调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read
调用。read
调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
Java 中的 NIO
,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
# AIO (Asynchronous I/O)
AIO
也就是 NIO 2
。Java 7 中引入了 NIO
的改进版 NIO 2
,它是异步 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。