系列文章目录
1. Netty网络应用基础
2. Java I/O
3. IO/模型
4. 网络应用编解码
5. Netty Pipeline
6. Netty EventLoopGroup&EventLoop
7. Netty ThreadLocal&FastThreadLocal
8. Netty Future&Promise
9. Netty内存管理–(旧)PoolChunk&伙伴分配
10. Netty内存管理–内存池空间规格化SizeClasses
11. Netty内存管理–PoolChunk&PoolSubPage
12. Netty内存管理–内存池PoolArena
13. Netty内存管理–内存分配器PooledByteBufAllocator
14. Netty ObjectPool
前言
关于IO, 想必你听过很多中I/O方式, 有的是OS视角的, 有的是JDK本身支持的, 有的是纯实现视角。但是作为一个developer, 我希望你能先搞清楚上下文之后, 再去理解内容, 否则容易抬杠。这个上下文有横向和纵向两个维度。纵向维度包括JDK底层, JDK上层包装库, 开发框架(如Netty)等。 横向的比如在JDK底层实现中支持BIO和NIO, 在JDK上层包装库维度支持BIO、NIO和AIO。开发框架Netty中包含BIO, NIO和OIO。在不同的上下文, 虽然字母缩写是一致的, 但内容却大相径庭。在这里, 我想聊的是JDK中提供的两种最基础的具体实现BIO和NIO。之所以写在这里, 因为这最基础的IO是上层所有IO方式的基础, 而打好基础非常关键。
一、BIO(Blocked I/O)
1. read过程
- 应用获取InputStream, 可能是FileInputStream或者Socket的InputStream;
- 应用创建user buffer;
- 应用调用InputStream的read(buffer)获取读取的字节数, JVM发起系统调用;
- 内核创建Kernel buffer, 读取目标文件描述符并将结果写入Kernel buffer;
- 内核将Kerne buffer中读取的数据复制到User buffer中。User buffer填充满或者读取结束后应用侧方法返回;
- 应用侧从buffer中获取读取的字节数, 做必要的业务处理;
- 重复3-6, 直到读取完成;
2. write过程
- 应用获取OutputStream, 可能是FileOutputStream或者Socket的OutputStream;
- 应用创建user buffer;
- 应用填充数据到user buffer;
- 应用侧调用OutputStream的write(buffer)写出, JVM发起系统调用;
- 内核创建Kernel buffer, 并复制User buffer中的数据, 然后将Kernel buffer中的数据写入目标文件描述符, 写入完成后系统调用结束, 应用侧方法调用返回;
- 重复3-5直到写入完成
3. 读取文件并发送到网络
这里我们尝试优化下:
- 咱们是读取并转发, 文件从Kernel Buffer复制到User buffer再复制到Kernel buffer, 但实际上应用侧未做任何加工, 因此文件内容不变, 3个buffer的内容是一样的。
- 从逻辑的角度, 应用层不需要参与具体IO, 仅仅是告诉内核从哪里Input然后Output到哪里即可。
- 从空间占用的角度, 重用一个Kernel Buffer, 将文件内容读取然后发送到Socket。由于复用同一个buffer, 又省去了内存拷贝的开销。
其实这就是NIO的实现思路。
二、NIO
1.read(get)过程
- 应用获取Channel;
- 应用通过channel.map创建内存映射, 将文件的某个部分[offset, length] 和buffer对应起来。此外该buffer对应用和内核均可见;
- 应用调用channel.load加载目标内容片段, JVM发起系统调用通知内核加载目标内容。
- 内核加载完成后暴露一个内核管理的但是在应用侧又可操作性的buffer, 内核加载过程除了最终暴露的buffer不同其他与BIO过程一致。在load过程中当前线程也是阻塞的。
- 后续应用侧可以直接通过应用可见的MappedBuffer读取数据(由于该原因, 图中直接将读取描述为逻辑上与内核无关的操作)
- 此外, 对于内存映射文件, 我们不仅可以读取也可以写入, 而后通过force将变更刷新到文件, 相当于BIO中的flush。
2. write(put)过程
- 应用获取Channel;
- 应用创建buffer, 该buffer对内核也是可见的;
- 应用填充数据到buffer;
- 应用调用channel的put(buffer)写出, JVM发起系统调用;
- 内核不再创建自己的buffer, 而是直接把已有的buffer中的数据写入文件, 写入完成后系统调用返回;
3. 读取文件并发送到网络
从图中我们可以看到, 文件数据并没有在应用侧出现, 此外也仅创建了一个Kernel Buffer, 实现了从应用侧仅说明从哪里Input并Output到哪里, 内核完成了整个逻辑过程。
三、两者比较
BIO: 面向Stream, 读写以byte为单位, 需要user buffer和kernel buffer协同(必要时需要两者互相copy)。对内核和JDK版本没要求, 兼容性强, 但效率低。
NIO: 面向Channel, 读写以buffer为单位, user和kernel公用一个buffer(相比于BIO尽可能少copy)。对内核和JDK版本有要求, 兼容性差, 但效率高。
小结
以上就是今天想聊的全部内容, 希望能帮助你对IO的概念,JDK中BIO和NIO的实现基本过程有更深入的理解。