底层原理 | IO模型

本文介绍了Linux下的五种IO模型,包括阻塞式I/O、非阻塞式I/O、I/O多路复用、信号驱动式I/O和异步I/O。阐述了Java对这些模型的支持演进,还分析了网络IO的两个阶段,以及各模型在这两个阶段的不同逻辑,最后区分了同步IO和异步IO、阻塞和非阻塞的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 Linux的五种IO模型

IO 是内存和外部设备 ( 硬盘、终端和网络等 ) 拷贝数据的过程。

在Linux下有5种可用的I/O模型:

  1. 阻塞式I/O:Blocking IO
  2. 非阻塞式I/O: Non-blocking IO
  3. 多路复用I/O:IO multiplexing
  4. 信号驱动式I/O:Signal-driven IO
  5. 异步I/O:Asynchronous IO

对于这五种IO模型,Java并不是一开始就都全部支持,而是有一个逐步演进的过程:

  • 在JDK1.4之前,Java的IO模型只支持阻塞式IO(Blocking IO),简称为BIO;
  • 在JDK1.4时,支持了I/O多路复用模型,相对于之前的IO模型,这是一个新的模型,所以称之为NIO(New IO),有新就有旧,所以有时也把BIO称之为OIO(old IO),其实都是一个意思。到现在为止,JDK1.8都已经出来了,JDK1.4时引入的nio包,也没有什么新鲜的了,所以更多的人愿意把NIO理解为None-Blocking IO,即非阻塞IO;
  • 在JDK1.7时,对NIO包进行了升级,支持了异步I/O(Asynchronous IO),简称为AIO,因为是对nio包的升级,所有有时又称之为NIO2.0;

在这里,我们以一个网络IO来举例,对于一个network IO(以read举例),它会涉及到两个系统对象:一个是调用这个IO的进程,另一个就是系统内核。当一个read操作发生时,它会经历两个阶段:
在这里插入图片描述

  • 阶段1:等待数据准备好;
  • 阶段2:将数据从内核拷贝到进程中;

图中忽略了很多细节,仅显示了涉及到的基本步骤 ,注意图中用户空间和内核空间的概念:

  • 用户空间是常规进程所在区域。 JVM 就是常规进程,驻守于用户空间。用户空间是非特权区域:比如,在该区域执行的代码就不能直接访问硬件设备。
  • 内核空间是操作系统所在区域。内核代码有特别的权力:它能与设备控制器通讯,控制着用户区域进程的运行状态,等等。最重要的是,所有 I/O 都直接(如这里所述)或间接通过内核空间。

当进程请求 I/O 操作的时候,它执行一个系统调用将控制权移交给内核。C/C++程序员所熟知的底层函数 open( )、 read( )、 write( )和 close( )要做的无非就是建立和执行适当的系统调用。当内核以这种方式被调用时,它随即采取必要的步骤,找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进内存。

了解了这两个阶段的作用之后,我们接下来就可以深入讲解五种IO模型了,他们的区别就是在这两个阶段上有着不同的逻辑

2 Blocking IO

在 Linux 中,默认情况下所有的 socket 都是阻塞的,一个典型的读操作流程大概是这样:
在这里插入图片描述

  • 第一步:等待数据从网络中到达。当所有等待数据到达时,它被复制到内核中的某个缓冲区。
  • 第二步:把数据从内核缓冲区复制到应用程序缓冲区。

当用户进程发出 read 操作时,内核就开始了IO的第一个阶段:准备数据。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除阻塞的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

3 非阻塞式I/O

Linux 下,可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子:
Image.png
从图中可以看出,当用户进程发出 read 操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个 error。 从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。

4 I/O多路复用

称为多路IO模型或IO复用,意思是可以检查多个IO等待的状态。有三种IO复用模型:select、poll和epoll。其实它们都是一种函数,用于监控指定文件描述符的数据是否就绪。

就绪指的是对某个系统调用不再阻塞了,可以直接执行IO。例如对于read()来说,数据准备好了就是就绪状态,此时read()可以直接去读取数据且能立即读取到数据。

IO复用同非阻塞IO本质一样,不过利用了新的 select 系统调用,由内核来负责本来是请求进程该做的轮询操作。看似比非阻塞IO还多了一个系统调用开销,不过因为可以支持多路IO,才算提高了效率。

它的基本原理就是 select /epoll 这个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。它的流程如图:
Image.png
当用户进程调用了 select,那么整个进程会被阻塞,而同时,内核会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从内核拷贝到用户进程。

这个图和 blocking IO 的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个 system call(select 和 recvfrom),而 blocking IO 只调用了一个 system call(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。

所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。

select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在 IO multiplexing model 中,实际中对于每一个 socket,一般都设置成为 non-blocking,但是,如上图所示,整个用户的进程其实是一直被 阻塞的。只不过进程是被 select 这个函数阻塞,而不是被 socket IO 给阻塞。

5 信号驱动式I/O

用的很少,就不做讲解了。直接上图
Image.png

6 异步I/O

这类函数的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。如图:
Image.png
用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个 signal,告诉它 read 操作完成了。 在这整个过程中,进程完全没有被阻塞。

7 同步IO和异步IO、阻塞和非阻塞的区分

阻塞和非阻塞,体现在当前进程是否可执行,是否能获取到CPU。

当阻塞和非阻塞的概念体现在IO模型上:

  • 阻塞IO:从开始发起IO操作开始就阻塞,直到IO完成才返回,所以进程会立即进入睡眠态;
  • 非阻塞IO:发起IO操作时,如果当前数据已就绪,则切换到内核态由内核完成数据拷贝(从kernel buffer拷贝到user buffer),此时进程被阻塞,因为它的CPU已经被内核抢走了。如果发起IO操作时数据未就绪,则立即返回而不阻塞,即进程继续享有CPU,可以继续任务。但进程不知道数据何时就绪,所以通常会采用轮循代码(比如while循环)不断判断数据是否就绪,当数据最终就绪后,切换到内核态,进程仍然被阻塞;

同步和异步,考虑的是两边数据是否同步(比如kernel buffer和user buffer之间数据是否同步)。同步和异步的区别体现在两边数据尚未完成同步时的行为:

  • 同步:在保持两边数据同步的过程中,进程被阻塞,由内核抢占其CPU去完成数据同步,直到两边数据同步,进程才被唤醒;
  • 异步:在保持两边数据同步的过程中,由内核默默地在后台完成数据同步,内核不会抢占进程的CPU,所以进程自身不被阻塞,当内核完成两端数据同步时,通知进程已同步完成;

这里阻塞和非阻塞、同步和异步都是广义的概念,上面所做的解释适用于所有使用这些术语的情况,而不仅仅是本文所专注的IO模型。

回到阻塞、非阻塞、同步、异步的IO模型,再对它们啰嗦啰嗦。

阻塞、非阻塞、IO复用、信号驱动都是同步IO模型。需注意,虽然不同IO模型在加载数据到kernel buffer的数据准备过程中可能阻塞、可能不阻塞,但kernel buffer才是 read() 函数读取数据时的对象,同步的意思是让kernel buffer和user buffer数据同步。在保持kernel buffer和user buffer同步的过程中,CPU将从执行 read() 操作的进程切换到内核态,内核获取CPU拷贝数据到user buffer,所以执行 read() 操作的进程在这个同步的阶段中是被阻塞的。

只有异步IO模型才是异步的,因为它调用的是具有【神力】的异步IO函数(如aio_read()),调用这些函数时会请求内核,当数据已经拷贝到user buffer后,通知进程并执行指定的操作。

需要注意的是,无论是哪种IO模型,在将数据从kernel buffer拷贝到user buffer的这个阶段,都是需要CPU参与的。只不过,同步IO模型和异步IO模型中,CPU参与的方式不一样:

  • 同步IO模型中,调用 read() 的进程会切换到内核,由内核占用CPU来执行数据拷贝,所以原进程在此阶段一直被阻塞;
  • 异步IO模型中,由内核在后台默默的执行数据拷贝,所以原进程在此阶段不被阻塞

在这里插入图片描述


参考文章:
  1. Unix五种IO模型
  2. 五种IO模型透彻分析
<think>嗯,用户这次想深入了解Java IO底层工作原理。结合之前的对话历史,用户已经了解字符流和字节流的区别,现在显然是想向更底层的机制探索。从引用内容看,用户提供了关于NIO、线程池和异步IO的参考资料,说明ta可能对高性能IO处理有实际需求,比如开发网络应用或处理大文件。 用户提供的引用中,[1]和[5]提到异步非阻塞IO模型,[4]详细描述了Netty的线程组设计,这些都是Java IO底层的关键补充材料。需要特别注意两点:一是用户明确要求参考站内引用,说明ta可能正在研究特定技术文档;二是ta上次对字符流的提问很精准,这次问题显然经过深思熟虑。 回答时应该分层展开:先解释传统BIO的阻塞本质,再对比NIO的事件驱动机制,最后用Netty的架构示例说明实际应用。特别要强调内核缓冲区与JVM的交互过程,这是理解IO性能的关键。用户可能遇到的困惑点包括:为什么NIO需要Selector?内核空间复制为什么影响性能?这些都需要用通道(Channel)和缓冲(Buffer)的协作关系来解释。 考虑到用户的技术水平(能提出底层原理问题),可以适当深入Linux epoll机制,但必须关联到Java NIO的Selector实现。线程模型部分要结合引用[2][3]的线程池内容,说明IO线程与业务线程的分离设计。最后用Netty的EventLoop例子呼应引用[4],形成闭环。 需要警惕的陷阱是:不要混淆异步IO(AIO)和NIO,Java的AIO在Linux下实际使用epoll模拟,这点在引用[5]有暗示。另外用户可能混淆“非阻塞”在不同层面的含义——API层的非阻塞(NIO)与内核层的真正异步(AIO)是两回事。</think>以下是Java IO底层实现原理的详解,结合操作系统内核交互、JVM实现机制和关键组件进行分层解析: --- ### **一、Java IO核心分层模型** 1. **应用层(Java API)** - `InputStream`/`OutputStream`:字节流基类,提供`read()`/`write()`抽象方法 - `Reader`/`Writer`:字符流基类,内部依赖字节流+编码转换 - 缓冲流(如`BufferedInputStream`)通过内存缓存减少系统调用次数[^2] 2. **JVM层(本地方法接口)** - 调用`native`方法(如`FileInputStream.read0()`)进入JVM的C++代码 - 关键类:`java.io.FileDescriptor`封装操作系统文件描述符(fd) 3. **操作系统层(内核交互)** - **系统调用**:`read()`, `write()`, `open()`等 - **内核缓冲区**:通过Page Cache减少磁盘I/O(预读/延迟写) ```c // Linux系统调用示例 ssize_t read(int fd, void *buf, size_t count); ``` --- ### **二、关键机制详解** #### 1. **阻塞IO(BIO原理** ```mermaid graph LR A[Java线程read调用] --> B[JVM发起read系统调用] B --> C[线程阻塞等待] C --> D[内核数据就绪] D --> E[数据复制到用户空间] E --> F[线程恢复执行] ``` - **阻塞点**:在数据从内核空间复制到用户空间期间,线程被挂起(线程状态:`RUNNABLE`→`BLOCKED`)[^1] - **资源消耗**:每个连接需独立线程,高并发时线程切换开销大(参考线程池问题[^2]) #### 2. **NIO非阻塞模型** ```java // Java NIO核心组件 Selector selector = Selector.open(); // 多路复用器 channel.configureBlocking(false); // 非阻塞模式 channel.register(selector, SelectionKey.OP_READ); ``` - **核心机制**: - **通道(Channel)**:双向数据传输管道(替代传统流) - **缓冲区(Buffer)**:堆外内存(`DirectByteBuffer`)避免JVM堆与本地内存复制 - **多路复用器(Selector)**:基于**epoll(Linux)** 或 **kqueue(Mac)** 实现 - 单线程管理多个Channel事件(`OP_ACCEPT`, `OP_READ`等) - 就绪事件通过`selectedKeys()`返回,避免无效轮询 - **底层交互**: 1. 注册fd到epoll实例:`epoll_ctl(epfd, EPOLL_CTL_ADD, fd, events)` 2. 内核通过回调通知就绪事件(水平触发LT/边缘触发ET) 3. 用户态直接处理就绪fd,无遍历开销[^4][^5] --- ### **三、性能优化机制** 1. **零拷贝技术** - `FileChannel.transferTo()`实现`sendfile`系统调用: ```mermaid graph LR A[磁盘文件] --> B[内核缓冲区] B --> C[网卡缓冲区] ``` 跳开用户空间复制(减少2次上下文切换+1次内存复制) 2. **内存映射文件(MappedByteBuffer)** - `FileChannel.map()`创建虚拟内存映射 - 文件直接映射到进程地址空间,读写操作由内核自动同步 3. **堆外内存(Direct Buffer)** - 避免JVM堆与本地堆间数据复制(GC不管理,需手动释放) --- ### **四、异步IO(AIO原理** - **Proactor模式**: 内核完成IO操作后回调通知(非NIO的就绪通知) ```java AsynchronousFileChannel channel = AsynchronousFileChannel.open(path); channel.read(buffer, position, buffer, new CompletionHandler<>() { @Override public void completed(Integer result, ByteBuffer attachment) { // 数据已就绪在buffer中 } }); ``` - 底层依赖**IOCP(Windows)** 或 **libaio(Linux)** [^1][^5] --- ### **五、与传统BIO对比** | **特性** | BIO(阻塞IO| NIO(非阻塞IO| AIO(异步IO| |------------------|---------------------|----------------------|---------------------| | **线程模型** | 1连接1线程 | 单线程多连接 | 回调驱动 | | **阻塞点** | 读写全程阻塞 | 仅select()可能阻塞 | 全程无阻塞 | | **适用场景** | 低并发连接 | 高并发短连接 | 高并发长连接 | | **复杂度** || 高(需处理半包等) || > **注**:Linux平台AIO成熟度较低,Netty等框架仍以NIO为主[^4][^5] --- ### **六、典型应用案例(Netty)** 1. **Reactor线程模型** - BossGroup:处理连接请求(`OP_ACCEPT`) - WorkerGroup:处理I/O事件(`OP_READ`/`OP_WRITE`) 2. **内存管理** - 基于`ByteBuf`的池化内存分配(减少GC压力) 3. **事件驱动** - 通过`ChannelPipeline`组织处理逻辑(责任链模式)[^4] --- **参考资料** [^1]: 异步IO模型与内核通知机制 [^2]: 线程池对阻塞IO的资源管理优化 [^3]: JMM与并发控制对IO性能的影响 [^4]: Netty的NIO事件处理架构 [^5]: Linux epoll与Java NIO的映射关系
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值