IO,英文全称是 Input/Output,翻译过来就是输入/输出。我们听得挺多,就是磁盘 IO,网络 IO 等。IO 即输入/输出,到底谁是输入?谁是输出?IO 如果脱离了主体,会让人疑惑。
计算机角度的 IO
我们常说的输入输出,比较直观的意思就是计算机的输入输出,计算机就是主体。计算机分成分为 5 个部分:运算器、控制器、存储器、输入设备、输出设备。输入设备是向计算机输入数据和信息的设备,键盘,鼠标都属于输入设备;输出设备是计算机硬件系统的终端设备,用于接收计算机数据的输出显示,一般显示器、打印机属于输出设备。
操作系统角度的 IO
我们要将内存中的数据写入到磁盘的话,那么主体就是一个程序。操作系统负责计算机的资源管理和进程的调度,我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。
真正的 IO 是在操作系统执行的。即应用程序的 IO 操作分为两种动作:IO 调用和 IO 执行。IO 调用是由进程(应用程序的运行态)发起,而 IO 执行是操作系统内核的工作。
应用程序发起的一次 IO 操作包含两个阶段:
IO 调用:应用程序进程向操作系统内核发起调用。
IO 执行:操作系统内核完成 IO 操作。
IO 模型
阻塞 IO
假设应用程序的进程发起 IO 调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次 IO 操作,称之为阻塞 IO。
阻塞 IO 比较经典的应用就是阻塞 socket、Java BIO。阻塞 IO 的缺点就是:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞 IO 优化。
非阻塞 IO
如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求,这就是非阻塞 IO。
非阻塞 IO 的流程如下:
1.应用进程向操作系统内核,发起 recvfrom( )读取数据。
2.操作系统内核数据没有准备好,立即返回 EWOULDBLOCK 错误码。
3. 应用程序进程轮询调用,继续向操作系统内核发起 recvfrom 读取数据。
4.操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
5.完成调用,返回成功提示。
recvfrom() 用来接收远程主机经指定的 socket 传来的数据,并把数据传到由参数 buf 指向的内存空间。非阻塞 IO 模型,简称 NIO,Non-Blocking IO。它相对于阻塞 IO,虽然大幅提升了性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的 CPU 资源。
IO 多路复用
既然 NIO 无效的轮询会导致 CPU 资源消耗,我们等到内核数据准备好了,主动通知应用进程再去进行系统调用。
IO 复用模型核心思路:系统给我们提供一类函数(如 select、poll、epoll),它们可以同时监控多个 fd 的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom() 系统调用。
文件描述符 fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符,用来表示相关文件信息。
select
应用进程通过调用 select 函数,可以同时监控多个 fd,在 select 函数监控的 fd中,只要有任何一个数据状态准备就绪了,select 函数就会返回可读状态,这时应用进程再发起 recvfrom( )请求去读取数据。select 的 IO 多路复用模型,只需要发起一次询问就够了,大大优化了性能。
但是,select 有几个缺点:监听的 IO 最大连接数有限,在 Linux 系统上一般为 1024kb。select 函数返回后,是通过遍历 fdset,找到就绪的描述符 fd(仅知道有 I/O 事件发生,却不知是哪几个 FD,所以遍历所有 FD)。因为存在大量遍历,所以会有连接数限制,所以后来又提出了 poll。与 select 相比,poll 解决了连接数限制问题。但是,select 和 poll 一样,还是需要通过遍历 FD 来获取已经就绪的 socket。如果同时连接大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。因此经典的多路复用模型 epoll 诞生。
epoll
为了解决 select/poll 存在的问题,多路复用模型 epoll 诞生,它采用事件驱动来实现。epoll 先通过 epoll_ctl() 来注册一个 fd,一旦基于某个 fd 就绪时,内核会采用回调机制,迅速激活这个 fd,当进程调用 epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是 epoll的亮点。
select、poll、epoll 的区别
select | poll | epoll | |
---|---|---|---|
底层数据结构 | 数组 | 链表 | 红黑树和双链表 |
获取就绪的 fd | 遍历 | 遍历 | 事件回调 |
事件复杂度 | O(n) | O(n) | O(1) |
最大连接数 | 1024 | 无限制 | 无限制 |
fd 数据拷贝 | 每次调用 select,需要将 fd 数据从用户空间拷贝到内核空间 | 每次调用 poll,需要将 fd 数据从用户空间拷贝到内核空间 | 使用内存映射(mmap),不需要从用户空间频繁拷贝 fd 数据到内核空间 |
异步 IO
前面讲的 BIO,NIO,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此 AIO 实现了 IO 全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示类似提交成功的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程 IO 操作执行完毕。
异步 IO 的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景:比如发起一笔批量转账,但是批量转账理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。
一个生活中经典例子:
BIO
小明去吃饭,就这样在那里排队,等了一小时,轮到她了,然后才开始吃饭。
package IO;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;public class BIODemo1 {public static void main(String[] args) throws IOException, InterruptedException {ServerSocket serverSocket = new ServerSocket(9999, 1024);System.out.println("服务器启动");while (true) {Socket socket = serverSocket.accept();}}}
package IO;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class BIODemo2 {public static void main(String[] args) throws IOException, InterruptedException {//创建线程池ThreadPoolExecutor executor = new ThreadPoolExecutor(50,50, 200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(20),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());ServerSocket serverSocket = new ServerSocket(9999, 1024);System.out.println("服务器启动");while (true) {Socket socket = serverSocket.accept();System.out.println(socket + "连接到服务器");executor.execute(() -> {//执行任务});}}}
package IO;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;/*** BIO模拟NIO*/
public class BIODemo3 {public static void main(String[] args) throws IOException, InterruptedException {List<Socket> list = new ArrayList<>();ServerSocket serverSocket = new ServerSocket(9999, 1024);System.out.println("服务器启动");while (true) {// 设置非阻塞 serverSocket.set不阻塞Socket socket = serverSocket.accept(); // 不管接到接不到继续向下执行// 判断有没有接到if (socket != null) { // 没有人连接到服务器// 虽然没有人连接,但是有可能有人发送消息,此时也需要处理.// 循环集合中的socket} else { // 有人连接到服务器System.out.println(socket + "连接到服务器");list.add(socket);// 循环集合中的socket}}}}
NIO
小红也去吃饭,她一看要等挺久的,于是去逛会商场,每次逛一下,就跑回来看看,是不是轮到她了。于是最后她既购了物,又吃上饭了。
package IO;import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class NIODemo1 {public static void main(String[] args) throws IOException {FileInputStream in = new FileInputStream("E:/source.txt");FileOutputStream out = new FileOutputStream("E:/dest.txt");FileChannel inchannel = in.getChannel();FileChannel outchannel = out.getChannel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);while (inchannel.read(byteBuffer) != -1) {byteBuffer.flip();outchannel.write(byteBuffer);byteBuffer.clear();}}}
package IO;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;/*NIO完整写法*/
public class NIODemo2 {public static void main(String[] args) throws InterruptedException, IOException {List<SocketChannel> list = new ArrayList<>();ServerSocketChannel serverSogketChannel = ServerSocketChannel.open();//创建服务器serverSogketChannel.bind(new InetSocketAddress(9999), 1024);serverSogketChannel.configureBlocking(false);//设置非阻塞//注册选择器Selector selector = Selector.open();//创建选择器serverSogketChannel.register(selector, SelectionKey.OP_ACCEPT);//向selector注册管道System.out.println("启动服务器");for (; ; ) {selector.select();Set<SelectionKey> selectionKeys = selector.selectedKeys();//返回所有选择器接收到的操作Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();if (selectionKey.isAcceptable()) {//连接ServerSocketChannel serverSockChannel = (ServerSocketChannel) selectionKey.channel();SocketChannel acceptSocketChannel = serverSockChannel.accept();System.out.println(acceptSocketChannel.getRemoteAddress());acceptSocketChannel.configureBlocking(false);acceptSocketChannel.register(selector, SelectionKey.OP_WRITE);}if (selectionKey.isWritable()) {//写SocketChannel socketChannel = (SocketChannel) selectionKey.channel();String resp = "响应";try {Thread.sleep(500);socketChannel.write(ByteBuffer.wrap(resp.getBytes()));} catch (Exception e) {e.printStackTrace();}}if (selectionKey.isReadable()) {//读SocketChannel channel = (SocketChannel) selectionKey.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int length = channel.read(buffer);String msg = "server receive msg:" + new String(buffer.array(), 0, length);System.out.println(msg);}iterator.remove();}}}
}
AIO
小华一样,去吃饭,由于他是高级会员,所以店长说,你去商场随便逛会吧,等下有位置,我立马打电话给你。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了饭。