本文最后更新于 2025-03-27T01:25:37+08:00
铺垫
在学IO模型前,我想先讲清楚为什么要有I/O模型,或者说I/O的核心在解决什么问题。
首先,我想大概温习一下操作系统的用户态(user model)和核心态(Kernel Mode)的职责。
操作类型 |
用户态 |
内核态 |
实现机制 |
直接访问硬件 |
❌ |
✅ |
特权指令集限制 |
修改MMU页表 |
❌ |
✅ |
CR3寄存器保护 |
发起系统调用 |
✅ |
➡️ |
软中断门(SYSCALL/SYSENTER) |
处理硬件中断 |
❌ |
✅ |
IDT(中断描述符表)配置 |
修改进程内存空间 |
❌ |
✅ |
页表项特权标志 |
调度线程 |
❌ |
✅ |
任务状态段(TSS) |
大概我们看出来了,一个程序执行超越本程序之外的功能,就会涉及到操作系统用户态到核心态的切换。那什么是本程序范围内的功能呢?例如:计算,内存访问,函数调用等就属于范围内的功能。那么结合本文要说的I/O操作,如果一个程序要进行I/O操作,例如:微信网络传输一个文件,Adobe加载本地一个文件等,都会涉及操作系统用户态到核心态的切换。所以I/O模型的核心确实是内核空间与用户空间之间的数据交互方式问题。 |
|
|
|
既然这样,那么如果我设计一个程序,能否服务端采用AIO的方式,客户端采用NIO方式呢?答案是:当然可以了! 所以I/O模型解决的是在本机系统状态数据交互的问题。
基本概念
BIO 同步阻塞I/O:每个连接由一个独立的用户线程处理。当该线程执行I/O操作(如read/write)时,会通过系统调用进入内核态,如果数据未就绪,内核会将线程置于阻塞状态,直到数据准备好后才唤醒线程继续执行。
NIO 同步非阻塞I/O:用户线程通过Selector(用户态对象)调用select()方法时,会触发系统调用进入内核态,内核通过epoll等机制监控注册的Channel。当有I/O事件就绪时,select()返回,用户线程遍历就绪的Channel集合,然后从对应的用户态Buffer中读取或写入数据。
AIO 异步非阻塞I/O:程序发起I/O操作后立即返回,无需等待操作完成。内核会全权负责I/O操作的执行,包括数据准备和拷贝工作。当操作真正完成时,内核通过回调机制主动通知应用程序处理结果。
内核在三种I/O模型中的职责对比
模型 |
内核职责范围 |
用户线程参与程度 |
典型系统调用 |
BIO |
仅负责数据就绪检测 |
必须全程等待并执行数据拷贝 |
read() /write() |
NIO |
负责就绪事件通知 |
需要主动发起数据拷贝 |
select() +read() |
AIO |
全流程处理(检测+拷贝+通知) |
只需提交请求和接收结果 |
io_submit() (Linux) |
1 2 3 4 5 6 7 8
|
硬件设备 → 内核缓冲区 →(用户线程介入)→ 用户缓冲区
硬件设备 → 内核缓冲区 → 用户缓冲区 (无用户线程介入)
|
NIO
流程:
- 创建Selector
- 创建channel,绑定端口
- Channel注册到Selector
- Selector处理事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| public class NioServer { private static final int BUFFER_SIZE = 1024; private static final int PORT = 8080; public static void main(String[] args) { try { Selector selector = Selector.open(); ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); serverChannel.bind(new InetSocketAddress(PORT)); serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务器启动,监听端口: " + PORT); while (true) { selector.select(); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); if (key.isAcceptable()) { handleAccept(key, selector); } if (key.isReadable()) { handleRead(key); } iter.remove(); } } } catch (IOException e) { e.printStackTrace(); } } private static void handleAccept(SelectionKey key, Selector selector) throws IOException { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = serverChannel.accept(); clientChannel.configureBlocking(false); clientChannel.register(selector, SelectionKey.OP_READ); System.out.println("客户端连接: " + clientChannel.getRemoteAddress()); } private static void handleRead(SelectionKey key) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); try { int bytesRead = clientChannel.read(buffer); if (bytesRead == -1) { System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress()); clientChannel.close(); return; } buffer.flip(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); String message = new String(bytes); System.out.println("收到消息: " + message); ByteBuffer response = ByteBuffer.wrap(("服务器回复: " + message).getBytes()); clientChannel.write(response); buffer.clear(); } catch (IOException e) { System.out.println("客户端异常断开: " + clientChannel.getRemoteAddress()); clientChannel.close(); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| public class NioClient { private static final int BUFFER_SIZE = 1024; private static final String HOST = "localhost"; private static final int PORT = 8080; public static void main(String[] args) { try { SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress(HOST, PORT)); while (!socketChannel.finishConnect()) { System.out.println("等待连接..."); } System.out.println("已连接到服务器"); ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("请输入消息(输入exit退出): "); String message = scanner.nextLine(); if ("exit".equalsIgnoreCase(message)) { break; } buffer.clear(); buffer.put(message.getBytes()); buffer.flip(); while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.clear(); int bytesRead = socketChannel.read(buffer); if (bytesRead > 0) { buffer.flip(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); System.out.println("服务器响应: " + new String(bytes)); } } socketChannel.close(); scanner.close(); } catch (IOException e) { e.printStackTrace(); } } }
|
传统Java NIO的缺点
- 需要手动管理Selector、Channel、Buffer和事件循环,开发效率低
- Java NIO在Linux平台下,空轮询Bug会导致CPU使用率异常升高,严重影响系统性能。(Java11已经修复)
- 断线重连需要手动实现
- 性能瓶颈
- 线程模型单一:通常只有一个Reactor线程处理I/O
- 锁竞争:Selector的selectedKeys()操作需要同步