从零起步学习计算机操作系统:I/O篇

Q1:I/O是什么?

A1:

核心概念:什么是 I/O?🔄

定义

I/O = Input/Output(输入/输出) ,指数据在计算机内部(CPU/内存)与外部设备之间流动的过程。

复制代码
 数据流动方向

输入 (Input):外部 → 内存
• 键盘输入 → 程序
• 磁盘文件 → JVM 堆
• 网络请求 → Socket 缓冲区
• 数据库查询结果 → 应用内存

输出 (Output):内存 → 外部  
• 程序日志 → 磁盘文件
• HTTP 响应 → 网络客户端
• 计算结果 → 数据库
• 监控指标 → Prometheus

I/O 的本质:速度不匹配的桥梁

图片来自https://www.mianshiya.com/


I/O 的分类

按设备类型分类

类型 示例 特点 Java 对应
磁盘 I/O 读写文件、数据库 慢(毫秒级)、顺序/随机访问 FileInputStream, RandomAccessFile
网络 I/O HTTP/RPC/Socket 慢(网络延迟)、流式传输 Socket, HttpClient, Netty
终端 I/O 键盘、屏幕、日志 人机交互、缓冲输出 System.in/out, Logger
设备 I/O GPU、网卡、USB 专用协议、驱动交互 JNI, JNA

Q2:为什么网络I/O会被阻塞?

A2:

核心原因 1:内核缓冲区没数据(Read 阻塞)

这是最常见的阻塞场景。当你调用 socket.read() 时,数据并不是直接从网卡到你的 Java 程序,中间经过了内核缓冲区

数据流动过程

复制代码
 网络线路 →  网卡 →  内核接收缓冲区 (Kernel Receive Buffer) →  用户缓冲区 (Java Heap)

阻塞发生点

  1. Java 应用 调用 read()
  2. 操作系统 检查 内核接收缓冲区
  3. 判断
    • 有数据:拷贝到用户缓冲区,返回数据长度。
    • 无数据阻塞当前线程,将线程放入等待队列,直到网卡收到新数据并唤醒线程。

形象类比

取快递: 你去快递柜(内核缓冲区)取包裹(数据)。

• 如果柜子里有货 → 直接拿走(返回)。

• 如果柜子是空的 → 你只能在旁边干等(阻塞),直到快递员把货放进柜子(网卡收到数据)。

Java 代码体现

java 复制代码
Socket socket = new Socket("server", 8080);
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];

//  如果服务器不发数据,这里永远卡住!
int len = in.read(buf);  // 线程进入 BLOCKED/WAITING 状态(OS 层面)

核心原因 2:内核缓冲区满了(Write 阻塞)

写操作也会阻塞!很多人误以为 write() 只是把数据发到内存,应该很快。但如果网络慢,内存也会满。

阻塞发生点

  1. Java 应用 调用 write()
  2. 操作系统 尝试将数据拷贝到 内核发送缓冲区 (Kernel Send Buffer)
  3. 判断
    • 有空间:拷贝成功,立即返回(数据还在内核,没真正发出去)。
    • 空间满阻塞当前线程,等待 TCP 协议栈把缓冲区的数据发走,腾出空间。

为什么会满?

  • 网络带宽不足:发送速度 > 网卡发送速度。
  • 对端接收慢 :对端的接收窗口(Receive Window)满了,触发 TCP 流控,本端停止发送。
  • 网络拥塞 :TCP 拥塞控制 限制了发送速率。

Java 代码体现

java 复制代码
OutputStream out = socket.getOutputStream();
byte[] data = new byte[10 * 1024 * 1024]; // 10MB 大数据

//  如果网络极慢或对端不读,这里会卡住!
out.write(data);  // 等待内核发送缓冲区腾出空间

核心原因 3:TCP 协议状态等待(Connect/Accept 阻塞)

在连接建立阶段,阻塞是由 TCP 状态机决定的。

1. Connect 阻塞(三次握手)

java 复制代码
// 客户端
Socket socket = new Socket();
//  阻塞直到三次握手完成,或超时
socket.connect(new InetSocketAddress("server", 8080)); 
  • 原因 :必须收到服务端的 SYN+ACK 才能建立连接。如果服务端挂了、防火墙丢了包、或网络延迟高,客户端会一直等(直到 TCP 超时,通常几十秒)。

2. Accept 阻塞(服务端等待连接)

java 复制代码
// 服务端
ServerSocket server = new ServerSocket(8080);
//  阻塞直到有新连接请求到达
Socket client = server.accept(); 
  • 原因:内核监听队列(Backlog)中没有已完成的连接,线程必须等待。

Q3:I/O模型都有哪些?

A3:

5 种经典 I/O 模型(POSIX 定义)

1️⃣ 阻塞 I/O(Blocking I/O - BIO)

机制:两个阶段都阻塞。

  1. 应用发起 recvfrom
  2. 内核等待数据准备(阻塞)。
  3. 数据准备好后,内核拷贝到用户空间(阻塞)。
  4. 返回结果。
java 复制代码
 应用线程:[ 阻塞等待数据 ] → [ 阻塞拷贝数据 ] → 继续执行
 内核:    [ 准备数据...  ] → [ 拷贝数据...  ]
  • 优点:编程简单,逻辑清晰。
  • 缺点:一个线程只能处理一个连接,并发低。
  • Java 对应java.io.Socket, ServerSocket

2️⃣ 非阻塞 I/O(Non-blocking I/O - NIO*)

机制:两个阶段都不阻塞(轮询)。

  1. 应用发起 recvfrom
  2. 内核立即返回(如果数据没准备好,返回 EWOULDBLOCK 错误)。
  3. 应用不断轮询(while 循环),直到数据准备好。
  4. 数据准备好后,再次发起拷贝。
java 复制代码
 应用线程:[ 问:好了吗?❌ ] → [ 问:好了吗?❌ ] → [ 问:好了吗?✅ ] → [ 拷贝数据 ] → 继续
 内核:    [ 准备数据...  ] → [ 准备数据...  ] → [ 数据就绪    ] → [ 拷贝数据 ]
  • 优点:线程不阻塞,可以做别的事。
  • 缺点:轮询浪费 CPU 资源(忙等)。
  • Java 对应SocketChannel.configureBlocking(false)(单纯非阻塞模式,少用)。

注意 :Java 的 "NIO" 包其实主要用的是 模型 3(多路复用),而不是这个纯轮询的非阻塞 I/O。


3️⃣ I/O 多路复用(I/O Multiplexing)⭐ 最常用

机制:阶段 1 阻塞(监听),阶段 2 阻塞(拷贝)。

  1. 应用发起 select/poll/epoll,监听多个 FD(文件描述符)。
  2. 内核阻塞等待,直到任何一个FD 数据准备好。
  3. 返回就绪的 FD 列表。
  4. 应用对就绪的 FD 发起 recvfrom 拷贝数据。
java 复制代码
 应用线程:[ 阻塞监听多个 FD ] → [ 收到就绪通知 ] → [ 拷贝数据 ] → 继续
 内核:    [ 监控多个连接... ] → [ 某个数据就绪 ] → [ 拷贝数据 ]
  • 优点:一个线程可以处理成千上万个连接,CPU 利用率高。
  • 缺点:编程复杂(需要处理就绪事件);数据拷贝阶段仍阻塞。
  • Java 对应java.nio.Selector, Netty, Redis, Nginx

4️⃣ 信号驱动 I/O(Signal Driven I/O)

机制:阶段 1 非阻塞(信号通知),阶段 2 阻塞(拷贝)。

  1. 应用开启信号驱动,发起 recvfrom 后立即返回。
  2. 内核准备数据,准备好后发送 SIGIO 信号给应用。
  3. 应用收到信号,发起 recvfrom 拷贝数据
java 复制代码
 应用线程:[ 注册信号 ] → [ 做别的事... ] → [ 收到信号✅ ] → [ 拷贝数据 ] → 继续
 内核:    [ 准备数据... ] → [ 发送信号    ] → [ 拷贝数据 ]
  • 优点:等待数据时不阻塞。
  • 缺点:信号处理复杂,容易出错,Linux 支持不完善。
  • Java 对应:基本无直接支持。

5️⃣ 异步 I/O(Asynchronous I/O - AIO)⭐ 最理想

机制:两个阶段都不阻塞。

  1. 应用发起 aio_read,提供缓冲区指针和回调函数。
  2. 内核等待数据准备 拷贝到用户缓冲区。
  3. 全部完成后,通知应用(回调/信号)。
java 复制代码
 应用线程:[ 发起请求 ] → [ 做别的事... ] → [ 收到完成通知✅ ] → 继续
 内核:    [ 准备数据... ] → [ 拷贝数据... ] → [ 发送通知    ]
  • 优点:真正的异步,应用完全不参与 I/O 过程。
  • 缺点:实现复杂,Linux 底层 AIO 支持不完善(性能有时不如 epoll)。
  • Java 对应java.nio.channels.AsynchronousSocketChannel(使用较少)。
相关推荐
姓刘的哦2 小时前
Qt实现蚂蚁线
开发语言·qt
布局呆星2 小时前
Python 文件操作教程
开发语言·python
Elnaij2 小时前
从C++开始的编程生活(23)——哈希表
开发语言·c++
跨境海王哥2 小时前
怎么检查一个IP是否干净?IP质量分数检测及如何判断风险?
网络·网络协议·tcp/ip
英英_2 小时前
优化 MATLAB MapReduce 程序性能:从基础调优到进阶提速
开发语言·matlab·mapreduce
nainaire2 小时前
仿muduo库的Tcp服务器以及其应用层Http协议支持
服务器·网络·c++·tcp/ip·http
皙然2 小时前
IPv4与IPv6深度解析:从地址枯竭到下一代网络的演进
网络·智能路由器
法欧特斯卡雷特2 小时前
Kotlin 2.3.20 现已发布,来看看!
android·前端·后端
LSL666_2 小时前
BaseMapper——新增和删除
java·开发语言·mybatis·mybatisplus