五种IO模型、非阻塞IO
问怎么拿结果→ 同步 / 异步问线程卡不卡→ 阻塞 / 非阻塞提到进程 / 线程同步,和这里调用同步这里的同步 / 异步:面向函数调用、IO、网络通信操作系统里进程 / 线程同步:面向多线程协作、临界资源竞争(配合互斥使用),只是同名概念,语义无关。
1. 五种IO模型
举例:钓鱼
钓鱼 = 等 + 钓,对应IO = 等内核准备数据 + 拷贝到用户缓冲区
| 模型 | 钓鱼特点 | 核心特点 | 等待方式 | 效率 |
|---|---|---|---|---|
| 阻塞 IO | 专注钓鱼,鱼漂不动就一直等 | 全程死等,不做任何事 | 阻塞等待 | 最低 |
| 非阻塞 IO | 不会因为鱼不上钩就卡死在监测鱼漂上 | 不阻塞,轮询检查状态 | 非阻塞轮询 | 比阻塞高,但用户态 CPU 空转 |
| 信号驱动 IO | 鱼上钩后鱼漂会反向通知他 | 不主动等,靠信号通知再处理 | 信号通知 | 高,不轮询,也不阻塞 |
| IO 多路复用(同步) | 架很多支鱼竿(100 只),同时看所有鱼漂 | 一个线程 / 进程管理多个连接,集中等待 | 多路复用(select/poll/epoll) | 高,单线程处理大量连接 |
| 异步 IO | 只爱吃鱼,让别人钓鱼后自己离开,鱼钓好直接送过来 | 全程不参与「等 + 钓」,完成后通知 | 内核全程处理 | 最高,完全不阻塞用户 |
1.1 阻塞IO
在内核将数据准备好之前,系统调用会一直等待,所有的套接字默认都是阻塞方式。阻塞IO是最常见的IO模型
1.2 非阻塞IO
- 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码
- 非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询
- 这对CPU来说是较大的浪费,一般只有特定场景下才使用

1.3 信号驱动IO
内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
1.4 IO多路转接
虽然从流程图上看起来和阻塞IO类似,但核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
1.5 异步IO
由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

2. 高级IO重要概念
2.1 同步通信 vs 异步通信
同步和异步关注的是消息通信机制
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
这里的同步通信和进程之间的同步是完全不相干的概念
- 进程 / 线程同步也是进程 / 线程之间直接的制约关系
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。尤其是在访问临界资源的时候
2.2 阻塞 vs 非阻塞
- 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,调用会立即返回,程序可以继续执行后续操作,之后再主动轮询结果或通过其他方式获取返回信息
2.3 总结
- 问怎么拿结果 → 同步 / 异步
- 问线程卡不卡 → 阻塞 / 非阻塞
提到进程 / 线程同步,和这里调用同步不是一回事:
- 这里的同步 / 异步:面向函数调用、IO、网络通信
- 操作系统里进程 / 线程同步:面向多线程协作、临界资源竞争(配合互斥使用),只是同名概念,语义无关
3. 其他高级 IO
非阻塞 IO,纪录锁,系统 V 流机制,I/O 多路转接(也叫 I/O 多路复用),readv 和 writev 函数以及存储映射 IO(mmap),这些统称为高级 IO
此处重点讨论的是 I/O 多路转接
4. 非阻塞IO
4.1 fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl是文件描述符万能控制函数,cmd 不同,功能完全不同,后面参数也不同
fcntl 5 大功能:
- 复制文件描述符:cmd = F_DUPFD
- 获得 / 设置 文件描述符标记:cmd = F_GETFD / F_SETFD
- 获得 / 设置 文件状态标记:cmd = F_GETFL / F_SETFL,控制的是:文件打开方式 / 状态包含的标志:
O_RDONLY/O_WRONLYO_NONBLOCK非阻塞O_APPEND追加写
- 获得 / 设置 异步 IO 所有权:cmd = F_GETOWN / F_SETOWN
- 获得 / 设置 记录锁:cmd = F_GETLK / F_SETLK / F_SETLKW
此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞
4.2 实现函数SetNoBlock
基于fcntl,实现一个SetNoBlock函数,将文件描述符设置为非阻塞
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h> // perror
// 功能:把文件描述符 fd 设置为非阻塞 IO
void SetNoBlock(int fd) {
// 1. 获取当前文件描述符的 文件状态标记(位图)
int fl = fcntl(fd, F_GETFL);
// 2. 如果获取失败(比如 fd 非法),打印错误并返回
if (fl < 0) {
perror("fcntl F_GETFL failed");
return;
}
// 3. 设置新标记:原有属性 | 非阻塞标记
// 位或运算:只添加 O_NONBLOCK,不修改原来的属性
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
为什么要先获取fd?文件描述符会有很多属性,如果直接使用会覆盖/清空原来的属性!再使用 | 追加
4.3 非阻塞 + 轮询
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
// 设置非阻塞
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main() {
SetNoBlock(0); // 0 代表标准输入 stdin
while (1) {
char buf[1024] = {0};
// 非阻塞 read
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
// 重点:非阻塞没数据,会触发 EAGAIN/EWOULDBLOCK
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("没有数据,轮询等待...\n");
sleep(1);
continue;
}
perror("read");
sleep(1);
continue;
}
// 读到数据
printf("input:%s\n", buf);
}
return 0;
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)