从零起步学习计算机操作系统: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(使用较少)。
相关推荐
GetcharZp4 小时前
玩转 Linux 机器视觉:手把手带你搞定 Ubuntu 下海康工业相机 C++ SDK
后端
星星在线7 小时前
MusicFree:一个「All in One」的个人音乐服务器,让听歌回归简单
前端·后端
IT_陈寒8 小时前
Redis的SETNX并发问题让我加了三天班
前端·人工智能·后端
demo007x8 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
袋鱼不重9 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
大树889 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
用户8356290780519 小时前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还10 小时前
啥? 前端也要会干Java?🛵🛵🛵
后端
Hommy8810 小时前
【剪映小助手】添加贴纸接口(Add Sticker)
后端·github·剪映小助手·视频剪辑自动化·剪映api
LDR00610 小时前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言