从操作系统I/O谈起
作为操作系统与外界沟通的“渠道”,I/O在计算机系统中有着很大的作用。在实际应用中,每个程序都免不了通过IO与文件系统进行本地调用,以及与其他应用进行远程调用。
本地调用和远程调用的基础,便是I/O。
I/O的分类
I/O在分类上可以分为同步与非同步,阻塞与非阻塞。这两种分类是完全不同的两个层级,具体如下:
- 阻塞I/O是指在用户进程发起I/O操作后,需等待该操作完成后才会继续执行,这个时候等待I/O操作会阻塞用户线程;非阻塞I/O是指在用户进程发起I/O操作后,不阻塞当前用户进程,但是用户需要主动探测其执行结果。
- 同步I/O与异步I/O的划分不涉及该操作发起的用户,主要区别于内核的行为。同步I/O是指需等待系统内核将数据复制到用户进程才能进行用户进程中的处理;异步I/O是指用户进程在I/O完成后接收由内核发出的通知,不关心内核实际对I/O的处理。
由以上区别可以看出,I/O的四种分类主要可划分成两个维度的分类。关于阻塞和非阻塞,是两个互相通信的进程之间的通信方式的划分;而同步和异步,则是进程和内核之间的通信方式。
I/O复用
I/O多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。I/O多路复用作为非阻塞I/O的基础,由于其不需要进行进程/线程的创建,从而大大减小系统开销。
主流Linux系统中提供了select、poll、epoll三个系统调用支持I/O多路复用。关于三者具体细节,在此不再展开。
从BIO到NIO
Java 中的I/O方式,主要分为 BIO、NIO以及AIO。这三种I/O方式在分类上来看,BIO是阻塞I/O,NIO和AIO是非阻塞IO;同时三者均为同步IO。
BIO-阻塞I/O
BIO是Java中最早的I/O方式。在BIO的实现中,在I/O过程中调用方需阻塞等待被调用方返回结果,才能继续执行。
我们可以把I/O操作相关的业务逻辑,划分为等待就绪和执行处理两部分。
- 等待就绪:线程不占用CPU,仅阻塞等待I/O完成
- 执行处理:I/O操作完成,线程开始使用CPU进行读写逻辑处理
在BIO的线程模型中,单独线程负责进行I/O并进行业务处理,在处理结束后,任务完成,整个任务完成。按照上面的划分逻辑来看,即同一个线程串行经历等待就绪和执行处理两个阶段,由于等待就绪阶段不需要占用CPU,此时CPU就处于空闲状态。
随着高并发场景的逐渐增加,这种业务线程被I/O阻塞吞吐率不高且资源利用率较低的方式显然是不能令我们接收的。虽然可以通过提高线程数的方式来提高CPU的利用率,但是由于线程切换本身会进行上下文以及系统调用过程,故吞吐量无法突破操作系统的限制。
正是因为过度依赖于多线程且目前互联网高并发场景越来越多,所以很多时候BIO并不是一个很好的选择。
我们大概可以总结BIO的特点如下:
- 线程模型简单,易编程
- 并发依赖于线程,可通过线程池简单的管理并发,实现限流等操作
- 由于线程切换成本较高,导致效率不高
- 无法适应大型互联网应用下单机十万/百万以及更高数量级连接的需求
NIO-非阻塞I/O
NIO,全称为Non-Blocking I/O,是一种基于操作系统层面提供的支持I/O复用的系统调用的非阻塞同步I/O方式。
所谓非阻塞,并不代表NIO全链路都是非阻塞的。我们再来看上文说到的I/O操作相关的业务逻辑,即等待就绪和执行处理。
在NIO中,其线程模型将等待就绪和执行真正的I/O处理这两部分进行了分离。这样做的结果便是进行真正I/O操作的线程不必等待就绪状态,可被复用。
自提出以来目前已经广泛用于Java进程间调用。Netty便是基于Java NIO来实现的一套远程通信框架,其目前被广泛应用于各大开源中间件与基础组件中。
由上文中写到,在阻塞I/O场景下,用户操作必须等待I/O结束才能继续进行。为了提高CPU的利用率,往往会采用多线程的方式来使得多个线程来同时接收并处理请求。
而在NIO的场景下,可以通过实际情况下
Reactor模式
什么是Reactor
Reactor与Proactor
总结
本文仅做学习记录,仅针对NIO相关概念及基本线程模型进行介绍,针对于Java NIO的Server及Client端Demo均有很多案例,在此不再进行举例说明。同时在本文中针对许多操作系统级别的系统调用与优缺点暂未深入探讨。
此外,由于AIO这种模式目前在 Java 生态中并没有被主流应用,在此也不做过多对比。目前针对于主流I/O密集应用,采用Netty进行开发以及拓展已经能够满足大多数场景。