在说IO前,我们来看一下操作系统进行读写的流程
例如:
Java客户端通过write系统调用将数据从用户缓冲区复制到内核缓存区,内核通过客户机网卡将数据发送出去;
服务器通过网卡接收数据至内核缓冲区,Java用户程序通过read系统调用将内核缓冲区中的数据读取至Java进程缓冲区
这是一个典型的系统调用流程
对于服务器IO编程来讲,常见的IO模型有4种
1. 同步阻塞IO
Blocking IO是指用户空间主动发起,需要等内核IO完成后才返回用户空间的IO操作,IO过程中用户进程处于阻塞状态
优点:应用程序开发简单
缺点:需要为每个连接配备一个独立的线程,一个线程维护一个连接IO操作;在高并发的情况下,需要大量的线程来维护连接,内存和线程切换的开销也会增大,性能低下
2. 同步非阻塞IO
Non-Blocking IO指用户进程主动发起,不需要等内核IO完成就能立即返回用户空间的IO操作,IO过程中用户进程处于非阻塞状态
注意这里的NIO不是Java的NIO,Java NIO表示New IO
同步非阻塞在调用过程中,如果内核数据没数据的情况下回返回失败,有数据的情况下系统调用阻塞,直至数据复制完成;所以在内核数据没有准备好的情况下,用户线程需要不断的发起IO系统调用,如果准备好了,用户线程阻塞,复制数据至完成(简而言之,用户需要尝试N(N>=1)次后才能读到完整的数据)
优点:内核在等待数据的过程中,系统IO调用能立即返回,用户线程不会阻塞
缺陷:需要不断轮询内核,占用CPU时间,所以高并发场景下,又是一个凉凉的方案,但是非阻塞这一点还是有价值的,可以基于这一点为基础做一些文章。
3. IO多路复用
IO Multiplexing属于经典的Reactor模式实现,有时也称为异步阻塞IO
此模型有硬性的前提条件:即操作系统必须在硬件上支持多路分离的系统调用select、epoll
IO多路复用的流程如下:
1. 选择器注册
2. 就绪状态轮询
3. 用户线程获取就绪列表后,发起read系统调用
4. 复制完成
相比前两种,IO多路复用涉及两种系统调用:
a. IO操作的系统调用
b. select就绪查询系统调用 (这一点和NIO模型类似需要轮询)
JDK中Java NIO使用的就是多路复用模型
优点:一个选择器查询线程可以处理成千上万连接(解决了NIO模型的问题),用户线程无需创建大量的线程
缺点:本质上select/poll调用是阻塞式的,属于同步IO
4. 异步IO
Asynchronous IO指用户胡进程变成被动接收者,内核空间称为主动调用者,当用户进程收到通知时,数据已被内核读取完毕并放在用户缓存区内
异步IO流程如下:
1. 用户线程发起read系统调用,立即可以去做其他事情,用户线程不阻塞
2. 内核IO进行数据准备至用户缓冲区
3. 内核给用户线程发送信号或进行回调方法,告知用户线程read系统调用已经完成
4. 用户线程读取用户缓冲区数据,完成后续业务操作
优点:相比上述三种模型可以真正做到用户线程不阻塞
缺点:应用程序仅做了注册和接收,其余工作都给了操作系统,需要底层内核提供支持(其实事情还是那些事情,异步IO相当于把自己要做的事情外包给了操作系统,如果操作系统本身做的好,性能高于自己实现,那么可以说异步IO的性能更好,操作系统从内部去实现,理论上是性能更好的)
异步IO在Linux2.6版本引入,JDK支持一般的原因,导致目前较多的网络应用程序大多采用IO多路复用(例如Netty)