JAVA-IO(一)
JAVA-IO(一)
sschrodinger
2019/05/21
引用
Linux IO 模型
同步/异步通信
同步指的是在发出一个调用请求后,函数在没有得到结果前就不返回,一旦调用就返回,是调用者被动等待的过程。
异步指的是在调用请求发出后,立即返回,调用者不会立即得到结果,而是被调用者通过状态,通知,回调来通知调用者。
同步与异步通信关心的事消息的通知机制。
note
- 所谓的状态的方式,即是在有结果时,改变某一标志位,如 IO 系统中,调用者通过轮询查看标志位。大量的 for 循环导致了程序效率的低下。
- 如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
阻塞/非阻塞调用
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得 CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
进程的阻塞实际上是执行的进程主动让出自己对 CPU 的控制权,直到条件满足,被其他进程唤醒,还给该进程 CPU 的控制权。
缓存 IO
缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
Linux IO 模型
由于 linux 使用缓存 IO 模型,所以,linux 中, 数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个 read 操作发生时,它会经历两个阶段:
- 等待数据准备(数据缓存到内核缓冲区)
- 将数据拷贝到进程的内存空间中
对于 socket 而言,总共分为如下两步:
- 通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
- 把数据从内核缓冲区复制到应用进程缓冲区。
在 linux 中,分为了 5 种 IO 模型:
矩阵
- 同步模型(synchronous IO)
- 阻塞 IO(bloking IO)
- 非阻塞 IO(non-blocking IO)
- 多路复用 IO(multiplexing IO/event driven IO)
- 信号驱动式 IO(signal-driven IO)
- 异步模型(asynchronous IO)
- 异步 IO(asynchronous IO)
同步阻塞 IO
在这个 IO 模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络 IO。
如下图所示:
同步阻塞 IO同步非阻塞式 I/O
进程把一个套接字设置成非阻塞是在通知内核,当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误。
同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
如下所示:
同步非阻塞
IO 多路复用
虽然 I/O 多路复用的函数也是阻塞的,但是其与以上两种还是有不同的,I/O 多路复用是阻塞在 select,epoll 这样的系统调用之上,而没有阻塞在真正的 I/O 系统调用如 recvfrom 之上。
select 函数等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读。
在 IO multiplexing Model 中,对于每一个 socket ,一般都设置成为 non-blocking ,这样可以对每一个 socket 都进行轮询。
如下图所示:
IO 多路复用
信号驱动式 IO
允许 Socket 进行信号驱动 IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。
如下:
信号驱动 I/O<p style="color:red">注意:如上的四种 IO 模型,从整个IO过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。</p>
异步 IO
相对于同步 IO,异步 IO 不是顺序执行。用户进程进行 aio_read 系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到 socket 数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
如下:
异步 IO
Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。
JAVA IO 模型
在 Java 1.4 版本之前,Java 只提供了 BIO 的 IO 模型,即 同步阻塞 IO,基于 Java 的所有 socket 都采用了该模型。在 1.4 版本上, Java 发布了 NIO,即Non-Block IO(New IO),包含了 linux IO 模型的同步非阻塞模型和 IO 多路复用模型,在 1.7 版本之后又发布了 AIO,即异步 IO。
传统的 BIO 模型
BIO 模型是 Java IO 最开始提供的一种 IO 模型,BIO 又可以细分为两种模型,一是传统的同步阻塞模型,二是在对传统 BIO 模型的基本上进行的优化,又称为伪异步 IO 模型。
传统 BIO 中,ServerSocket 负责绑定 IP 地址,启动监听端口;Socket 负责发起连接操作,连接成功后,双方通过输入和输出流进行同步阻塞通信。采用 BIO 通信模型的 Server,通常由一个独立的 Acceptor 线程负责监听 Client 端的连接,它接受到 Client 端连接请求后为每个 Client 创建一个新的线程进行处理,处理完之后,通过输出流返回给 Client 端,线程销毁,过程如下图所示。
传统 IO 模型为了改进这种一对一的连接模型,后来又演进出了一种通过线程池或者消息队列实现 1 个或者多个线程处理所有 Client 请求的模型,由于它底层依然是同步阻塞 IO,所以被称为【伪异步 IO 模型】。相比于传统 BIO 后端不断创建新的线程处理 Client 请求,它在后端使用一个线程池来代替,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程资源耗尽,过程如下图所示:
BIO2.pngNIO 模型
NIO 模型引入了 非阻塞的 IO 模型,其中, select 使用底层的 epoll 函数构成,并可以设置 IO 是否为阻塞,是 Java 一大改进。
AIO 模型
NIO 2.0 中引入异步通道的概念,并提供了异步文件通道和异步套接字导通的实现,它是真正的异步非阻塞I IO,底层是利用事件驱动(AIO)实现,不需要多路复用器(Selector)对注册的通道进行轮组操作即可实现异步读写。