IO Models
I/O Overview
I/O即输入输出(Input/Output),核心目标是实现数据的交换与控制。从本质上来说,IO操作主要涉及两个阶段:
- 数据准备阶段:等待数据准备好(如等待数据从网络到达)
- 数据复制阶段:将数据从kernel buffer复制到user buffer
Kernel Space(内核空间)和User Space(用户空间)是操作系统中两个重要的内存区域。
内核空间是操作系统核心代码运行的区域,具有更高的权限,可以直接访问硬件资源;
而用户空间则是应用程序运行的区域,权限较低,不能直接访问硬件资源。通常需要通过系统调用执行特权操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────┐
│ User Space 0x0000000000000000 - 0x00007FFFFFFFFFFF │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Stack Buffer│ │ Heap Buffer │ │ mmap Buffer │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Kernel Space 0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Socket Buffer│ │ Page Cache │ │ DMA Buffer │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
基于两个阶段的处理方式差异,可以将I/O操作分为以下几种模型:
IO模型 | 数据准备阶段 | 数据复制阶段 |
---|---|---|
阻塞IO | 阻塞等待 | 同步复制 |
非阻塞IO | 非阻塞轮询 | 同步复制 |
IO多路复用 | 阻塞等待多个源 | 同步复制 |
信号驱动IO | 异步通知 | 同步复制 |
异步IO | 异步等待 | 异步复制 |
一次I/O操作各阶段的流程可以参考下图:
View Mermaid diagram code
sequenceDiagram
box User Space
participant APP as Web Application
participant FWK as Web Framework/System Libraries
end
participant SYS as 🔶 System Call Interface
box Kernel Space
participant KER as Kernel
end
participant HW as Physical Hardware
APP->>FWK: 使用Web框架/库函提供的接口,进行IO操作
FWK->>SYS: 发起系统调用
Note over SYS: Context Switch (用户态→内核态)
SYS->>KER: 内核处理逻辑
KER->>HW: 硬件操作
Note over KER: 内核等待I/O设备数据就绪
HW-->>KER: 数据就绪, 存储到内核缓冲区(Kernel Buffer)
Note over KER: 数据由内核缓冲区复制到用户缓冲区(User Buffer)
KER-->>SYS: 系统调用返回
Note over SYS: Context Switch (内核态→用户态)
SYS-->>FWK: 返回I/O结果
FWK-->>APP: 返回业务结果
I/O Models

Blocking & Non Blocking I/O
阻塞 IO 是最通用的 IO 类型。使用这种模型进行数据接收的时候,在数据没有到之前程序会一直等待。
而当执行非阻塞 IO 时,如果数据已经准备好,操作会立即完成;如果未准备好(对于TCP套接字即⾄少有⼀个字节的数据可读,对于UDP套接字即有⼀个完整的数据报可读)
,不要阻塞等待,而是返回EWOULDBLOCK
错误。应用程序可以基于这种错误进行处理,例如继续轮询(polling)等待数据就绪


当一个应用程序使用了非阻塞模式的套接字,应用程序需要不停的 polling(轮询) 内核来检查是否 I/O 操作已经就绪。这将是一个极浪费 CPU 资源的操作。因此这种I/O模型在使用中不是很普遍。
I/O Multiplexing
操作系统提供了一系列系统调用(syscall
),例如 select
、poll
和 epoll
。
它们遵循相似的工作原理:应用程序开一个线程或进程,调用系统调用,例如 select
,来监视多个文件描述符, 之后线程/进程会阻塞在 select
这个调用上,等待某个文件描述符状态改变(比如有数据可读、可写或发生异常),当文件描述符状态变为可读时,调用 recvfrom
把读到的数据复制到用户进程缓冲区
项目 | 阻塞 I/O | I/O 复用(select) |
---|---|---|
系统调用次数 | 1 次,直接调用recvfrom | 2 次先调用select 再调用 recvfrom |
在处理单个文件描述符时,select 反而是“额外绕了一圈”, 但 select
的优势是可以等待多个描述符就绪,与之相似的是[[IO多路复用#多线程/进程 + Blocking I/O|多线程/进程 + Blocking I/O]]

三个函数在实现上有较大差异,此处暂不展开,执行下面命令查看Linux Programmer’s Manual
1
2
3
man select
man poll
man epoll
Signal Driven I/O
信号驱动的 IO 在进程开始的时候注册一个信号处理的回调函数,进程继续执行。当数据到来时,通知目标进程进行 IO 操作(signal handler)

Asynchronous I/O
“异步”这个词在技术讨论中常被用于描述两个不同层面的概念,这容易引起混淆:
操作系统层面的 I/O 模型:例如本文介绍的AIO。在此模型中,从数据准备到数据复制的整个过程都由内核完成,应用只需发起请求,然后等待内核的完成信号即可。
应用程序层面的编程模型:框架提供的API是异步的,但框架所调用的操作系统接口是同步非阻塞。
异步 IO 与前面的信号驱动 IO 相似,其区别在于信号驱动 IO 当数据到来的时候,使用信号通知注册的信号处理函数,而异步 IO 则在数据复制完成的时候才发送信号通知注册的信号处理函数。

Ref
IO Models