文章目录
- 五种IO模型
- Linux设计哲学
- BIO
- NIO
- AIO
- SIO
- IO多路复用
五种IO模型
Linux设计哲学
在linux系统中,实际上所有的I/O设备都被抽象为了文件这个概念,一切皆文件,磁盘、网络数据、终端,甚至进程间通信工具管道pipe等都被当做文件对待。
在了解多路复用
select、poll、epoll实现之前,我们先简单回忆复习以下两个概念:
多路: 指的是多个socket网络连接;
复用: 指的是复用一个线程、使用一个线程来检查多个文件描述符(Socket)的就绪状态
多路复用主要有三种技术:select,poll,epoll。epoll是最新的, 也是目前最好的多路复用技术;
- blockingIO - 阻塞IO
- nonblockingIO - 非阻塞IO
- signaldrivenIO - 信号驱动IO
- asynchronousIO - 异步IO
- IOmultiplexing - IO多路复用
其中Java实现了BIO、NIO和AIO
BIO
BIO 是 Java 最早提供的 IO 模型,属于同步阻塞式 IO。其特点是,在进行读写操作时,线程会被阻塞,直到操作完成。
工作原理:每处理一个连接,就需要创建一个独立的线程。当有大量连接时,会消耗大量的系统资源,导致性能下降。
适用场景:适用于连接数目较少且固定的场景。
示例:
// 服务器端示例
ServerSocket serverSocket = new ServerSocket(8080);
while(true) {// 阻塞等待客户端连接Socket socket = serverSocket.accept();// 为每个连接创建一个新线程处理new Thread(() -> {// 处理输入流和输出流// ...}).start();
}
进程/线程在从调用recvfrom开始到它返回的整段时间内是被阻塞的
recvfrom成功返回后,应用进程/线程开始处理数据报。
主要特点是进程阻塞挂起不消耗CPU资源,能及时响应每个操作;
实现难度低,适用并发量小的网络应用开发,不适用并发量大的应用,因为一个请求IO会阻塞进程,所以每请求分配一个处理进程(线程)去响应,系统开销大。
NIO
NIO 是 Java 1.4 引入的新 IO 模型,属于同步非阻塞式 IO。它的核心组件有 Channel(通道)、Buffer(缓冲区)和 Selector(选择器)。
工作原理:
Channel:可以进行双向数据传输,类似于传统 IO 中的流,但功能更强大。
Buffer:数据的读写都要通过缓冲区进行,这是一种双向操作。
Selector:单个线程可以通过选择器监控多个通道的事件,从而实现非阻塞 IO。
适用场景:适用于连接数目多且连接比较短(轻操作)的场景,例如聊天服务器。
代码示例:
// 服务器端示例
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {// 非阻塞等待事件int readyChannels = selector.selectNow();if (readyChannels > 0) {Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 处理不同事件...keyIterator.remove();}}
}
进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
进程轮询(重复)调用,消耗CPU的资源;
实现难度低、开发应用相对阻塞IO模式较难;
适用并发量较小、且不需要及时响应的网络应用开发;
AIO
AIO 是 Java 7 引入的异步 IO 模型,也被称为 NIO 2.0。它基于事件和回调机制,实现了真正的异步非阻塞。
工作原理:应用程序只需发起 IO 操作,然后继续执行其他任务。当 IO 操作完成后,系统会通过回调函数通知应用程序。
适用场景:适用于连接数目多且连接比较长(重操作)的场景,例如相册服务器。
代码示例
// 服务器端示例
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {@Overridepublic void completed(AsynchronousSocketChannel client, Void attachment) {// 接受下一个连接serverChannel.accept(null, this);// 处理客户端连接...}@Overridepublic void failed(Throwable exc, Void attachment) {// 处理失败情况}
});
当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。
特点:
不阻塞,数据一步到位;Proactor模式;
需要操作系统的底层支持,LINUX 2.5 版本内核首现,2.6 版本产品的内核标准特性;
实现、开发应用难度大;
非常适合高性能高并发应用;
SIO
signaldrivenIO - 信号驱动IO
当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
特点:回调机制,实现、开发应用难度大;
IO多路复用
大多数文件系统的默认IO操作都是缓存IO。在Linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存
(page cache)。也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间中。
这种做法的缺点就是,需要在应用程序地址空间和内核进行多次拷贝,这些拷贝动作所带来的CPU以及内存开销是非常大的。
至于为什么不能直接让磁盘控制器把数据送到应用程序的地址空间中呢?最简单的一个原因就是应用程序不能直接操作底层硬件。
总的来说,IO分两阶段:
1)数据准备阶段
2)内核空间复制回用户进程缓冲区阶段