同步阻塞IO
上下文切换过程:
- 用户态 → 内核态:调用
read()系统调用 - 内核态:等待数据(进程挂起,不占用 CPU)
- 内核态:数据从网卡复制到内核缓冲区(DMA 直接内存访问)
- 内核态:数据从内核缓冲区复制到用户缓冲区
- 内核态 → 用户态:返回调用结果
特点:2 次上下文切换 + 2 次数据复制
用户进程 内核
| |
| 调用read() |
|----------------->|
| |
| 等待数据准备 |
| (进程阻塞) |--- 等待数据包到达 ---
| | ↓
| | 数据到达网卡
| | ↓
| | 数据复制到内核缓冲区
| | ↓
| 数据复制完成 |
|<-----------------|
| |
| 复制数据到用户空间|
| (内核到用户复制) |
| |
| 返回结果 |
|<-----------------|
同步非阻塞IO
上下文切换过程:
- 用户态 → 内核态:发起系统调用
- 内核态 → 用户态:立即返回(无数据)
- 重复步骤 1-2 进行轮询(多次上下文切换)
- 用户态 → 内核态:数据就绪时调用
- 内核态:数据从内核复制到用户空间
- 内核态 → 用户态:返回结果
特点:多次上下文切换 + 2 次数据复制,CPU 消耗高
用户进程 内核
| |
| 调用recv() |
|----------------->|
| |
| 立即返回EWOULDBLOCK|
|<-----------------|
| |
| 不断轮询recv() |
|----------------->|
| 返回EWOULDBLOCK |
|<-----------------|
| ... |
| 第N次调用recv() |
|----------------->|
| |
| 数据已准备好 |
| 开始复制到用户空间 |
|<-----------------|
| 复制完成 |
|<-----------------|
多路复用
NIO+Selector对路复用器
用户进程 内核
| |
| 调用select() |
|----------------->|
| |
| 监视多个fd |
| (进程阻塞) |--- 等待任意fd就绪 ---
| | ↓
| | 有fd就绪(如socket1)
|<-----------------|
| |
| 遍历就绪fd |
| 调用recv(socket1) |
|----------------->|
| |
| 复制数据到用户空间 |
|<-----------------|
上下文切换过程(以 select 为例):
应用程序 内核
| |
| select() 系统调用 | 1. 用户态→内核态
|------------------>|
| |
| 遍历所有fd | 2. 在内核遍历所有fd
| |
| 有fd就绪时返回 | 3. 内核态→用户态
|<------------------|
| |
| 遍历就绪fd | 4. 在用户空间遍历
| |
| 对每个就绪fd: |
| recv()系统调用 | 5. 用户态→内核态
|------------------>|
| 数据复制 | 6. 内核到用户复制
|<------------------| 7. 内核态→用户态
epoll 的改进:
应用程序 内核
| |
| epoll_create() |
|------------------>|
| |
| epoll_ctl(ADD) | 添加fd到红黑树
|------------------>|
| |
| epoll_wait() | 1. 用户态→内核态
|------------------>|
| |
| 等待就绪事件 | 2. 进程阻塞
| | ↓
| | 事件就绪队列
|<------------------| 3. 返回就绪事件
| | (只返回就绪的fd)
| |
| 处理就绪事件 |
| recv(就绪fd) | 4. 用户态→内核态
|------------------>|
| | 5. 数据复制
|<------------------| 6. 内核态→用户态
特点:
- select/poll:每次调用都要传递所有 fd,内核遍历所有 fd
- epoll:内核维护事件表,只返回就绪的 fd
异步阻塞IO(少用)
AIO,linux中对的异步实际上是假的AIO,底层实现还是多路复用模拟的AIO
在windows中实现了真正的异步AIO(IOCP)。
上下文切换:
类似同步阻塞,但操作发起是非阻塞的,等待完成是阻塞的。
用户进程 内核
| |
| 发起异步操作 |
|----------------->|
| |
| 立即返回 |--- 异步处理 ---
|<-----------------| ↓
| | 等待操作完成
| | ↓
| 调用阻塞等待 | 完成时设置状态
|----------------->|
| |
| 等待完成 | 阻塞等待信号
|<-----------------|
异步非阻塞IO
数据流程(Linux AIO):
用户进程 内核
| |
| io_submit() | 1. 用户态→内核态
|----------------->|
| |
| 立即返回 | 2. 内核态→用户态
|<-----------------|
| |
| 继续执行其他任务 |--- 内核异步处理 ---
| | ↓
| | 等待数据到达
| | ↓
| | 数据复制到用户缓冲区
| | ↓
| 收到信号/回调 | 3. 内核发送信号
|<-----------------|
| |
| 处理完成事件 |
Windows IOCP 流程:
应用程序 内核
| |
| WSARecv() | 1. 提交异步请求
|----------------->|
| |
| 立即返回 | 2. 返回正在处理
|<-----------------|
| |
| 继续执行 |--- 内核处理 ---
| | ↓
| GetQueuedCompletionStatus()|
|----------------->| 3. 等待完成端口
| | ↓
| | 操作完成时
| | 数据已复制到用户缓冲区
|<-----------------| 4. 返回完成状态
数据复制差异:
- 同步 I/O:用户进程调用时,数据才从内核复制到用户空间
- 异步 I/O:内核自动完成数据复制,完成后通知用户进程
信号驱动
不完善,基本不用
对比总结
| 模型 | 用户态→内核态切换次数 | 数据复制时机 | 等待方式 |
|---|---|---|---|
| 同步阻塞 | 1次(等待+复制) | 调用时复制 | 进程阻塞 |
| 同步非阻塞 | 多次轮询 | 数据就绪时复制 | 轮询 |
| 多路复用 | 2次(select+recv) | 就绪后复制 | 单线程阻塞等待多个 |
| 异步阻塞 | 2次(提交+等待) | 完成时已复制 | 阻塞等待完成 |
| 异步非阻塞 | 1次(提交) | 内核自动复制 | 回调/信号 |
数据复制过程:
- DMA 复制:网卡数据 → 内核缓冲区(由 DMA 控制器完成,不占用 CPU)
- CPU 复制:内核缓冲区 → 用户缓冲区(需要 CPU 参与)