先明确核心前提:IO 的两个必经步骤
不管哪种 IO 模型,完成一次数据读写(比如从网卡读数据、从文件读数据)都要走两步:
- 等待(Waiting):内核等待数据准备好(比如网卡接收到完整的网络数据包、硬盘把数据加载到内存);
- 拷贝(Copying):内核把准备好的数据拷贝到应用程序的内存空间(比如把网卡数据从内核缓冲区复制到你的代码能访问的内存)。
实际场景中,等待占了 99% 的时间(比如网卡等数据包可能要等 100ms,拷贝只需要 0.1ms),所以 IO 优化的核心就是 "怎么减少等待时间"。
阻塞 IO
你去奶茶店点单后,站在柜台前一直等,不做任何事,直到奶茶做好(等待),然后亲手接过奶茶(拷贝)------ 全程被 "买奶茶" 这件事阻塞,没法干别的。

- 应用程序发起 IO 系统调用(比如
read())后,内核数据没准备好时,系统调用会一直挂起,应用程序被阻塞(CPU 不会给它分配时间片); - 直到内核数据准备好,内核把数据拷贝到应用程序内存,系统调用才返回,应用程序恢复运行;所有套接字(socket)默认都是阻塞模式
非阻塞 IO
你去奶茶店点单后,店员告诉你 "还没好,你先走走",你没闲着 ------每隔 1 分钟就回来问一次 "好了吗?"(轮询),直到问的时候奶茶做好了,你接过奶茶。

技术本质
- 先把文件描述符(比如 socket)设置为非阻塞模式(最后会讲解方法);
- 应用程序发起 IO 系统调用后,内核数据没准备好时,系统调用会立刻返回
EWOULDBLOCK错误码(表示 "暂时没数据,你别等了"),不会阻塞程序; - 程序员需要写循环(轮询),反复调用 IO 接口,直到内核数据准备好,完成拷贝。
特点
✅ 优点
- 等待期间程序可以干其他事:比如在
sleep(1)前加一行printf("轮询中,干其他事...\n");,就能在等待输入时执行其他逻辑; - 不会被单个 IO 请求卡住:比如如果是网络 socket,非阻塞模式下不会因为一个 socket 没数据就卡住整个程序。
❌ 缺点
- CPU 空耗:即使加了
sleep(1),轮询还是会占用 CPU(如果去掉 sleep,CPU 会 100% 占用); - 代码逻辑复杂:需要处理
read返回值的各种情况,比阻塞 IO 多很多判断。
📌 适用场景
- 极特殊的低延迟需求(比如高频次小数据读写);
- 一般不单独使用,常和 IO 多路转接(epoll/select)结合(比如 epoll 监控非阻塞 socket,避免轮询空耗)。
信号驱动 IO
你去奶茶店点单后,留了手机号,告诉店员 "做好了给我发个短信",然后你去逛街(干别的事)------等收到短信(信号),再回来取奶茶。

2. 技术本质
- 应用程序先给文件描述符注册一个
SIGIO信号处理函数(告诉内核:数据准备好时给我发这个信号); - 注册后应用程序可以正常运行,不用等待;
- 内核数据准备好后,会给应用程序发送
SIGIO信号,应用程序收到信号后,再发起 IO 系统调用完成数据拷贝。
3. 关键区别
信号驱动 IO 只通知 "数据准备好了,可以开始拷贝了",拷贝过程仍然是阻塞的 (比如收到信号后调用read(),拷贝时程序还是会卡住)。
4. 特点
- ✅ 优点:等待期间 CPU 完全解放,不用轮询,利用率高;
- ❌ 缺点:信号处理逻辑复杂(比如多个 IO 请求的信号可能混淆),且拷贝阶段仍阻塞;
- 📌 适用场景:少量 IO 请求、对 CPU 利用率要求高的场景(比如嵌入式设备)。
IO 多路转接
你开了一家奶茶店雇了一个 "前台"------ 所有顾客点单后,都把订单交给前台,前台同时盯着所有订单的进度,哪个订单的奶茶做好了,就喊对应的顾客来取。你不用挨个问,只等前台通知。

2. 技术本质
- 核心是
select/poll/epoll(Linux)、kqueue(BSD)等多路转接函数,它们能同时监控多个文件描述符的就绪状态(比如 "是否有数据可读"); - 应用程序调用
epoll_wait()后,内核会阻塞这个调用,但会同时监控所有注册的文件描述符; - 只要有任意一个文件描述符的数据准备好,
epoll_wait()就会返回,告诉应用程序 "哪个描述符就绪了"; - 应用程序再针对就绪的描述符发起 IO 调用,完成拷贝。
3. 关键区别
- 看似和阻塞 IO 一样 "等",但阻塞 IO 是 "等一个",多路转接是 "等多个"------ 比如一个进程用
epoll可以监控 1000 个 socket,只要有一个有数据,就处理哪个; - 等待阶段是内核帮你批量等,CPU 利用率远高于阻塞 IO。
4. 特点
- ✅ 优点:能高效处理高并发(比如单进程处理上万的 socket 连接),CPU 利用率高;
- ❌ 缺点:拷贝阶段仍然是阻塞的;
- 📌 适用场景:网络编程的核心场景(比如 Web 服务器、游戏服务器),是目前高并发 IO 的主流方案(Nginx、Redis 都用
epoll)。
异步 IO
1. 通俗类比
你在奶茶店点单后,直接告诉店员 "做好了帮我送到家",然后你该干啥干啥 ------既不用等,也不用自己取,全程不用管,直到奶茶送到家(你负责在拷贝完成后直接使用,而不需要关系拷贝本身)。

2. 技术本质
- 应用程序发起异步 IO 调用(比如 Linux 的
aio_read()),告诉内核 "我要读数据,读完了通知我",然后立刻返回,完全不阻塞; - 内核会自己完成 "等待数据准备好 + 拷贝数据到应用程序内存" 的全部过程;
- 等所有步骤都完成后,内核才会通知应用程序(比如通过信号或回调函数)。
3. 关键区别
和信号驱动 IO 的核心差异:
- 信号驱动 IO:内核通知 "可以开始拷贝了"(只完成等待);
- 异步 IO:内核通知 "拷贝已经完成了"(等待 + 拷贝都完成)。
4. 特点
- ✅ 优点:IO 全程不阻塞应用程序,CPU 利用率最高;
- ❌ 缺点:实现复杂,操作系统支持有限(如 Linux 的异步 IO 对文件 / 网络的支持不完全)
- 📌 适用场景:超大规模并发、对响应时间要求极高的场景(比如分布式存储系统)。
关于异步IO的具体实现等我们复习到了c++11的异步关键字了再说
非阻塞 IO 的实现
非阻塞 IO 的本质是:文件描述符(fd)被设置为非阻塞后,调用read()/write()等 IO 函数时,内核若未准备好数据,函数会立刻返回(而非阻塞等待),并返回错误码(通常是EWOULDBLOCK或EAGAIN)。
- 标准输入(fd=0)、socket 等默认都是阻塞模式:比如阻塞模式下
read(0, ...)会一直等你输入,程序卡着不动; - 设为非阻塞后:
read(0, ...)会立刻返回 ------ 有输入就读数据,没输入就返回错误,程序不会卡。
fcntl 函数
fcntl是 Linux 下操作文件描述符属性的多功能函数,我们只关注它的获取 / 设置文件状态标记功能(对应cmd=F_GETFL/F_SETFL):
F_GETFL:读取当前文件描述符的 "状态标记"(比如是否是阻塞、是否是只读 / 只写等,本质是一个位图);F_SETFL:修改文件描述符的 "状态标记";O_NONBLOCK:非阻塞标志 ------ 给状态标记加上这个值,文件描述符就变成非阻塞模式。
- 为什么不能直接设置,要 "先读再改"?
文件描述符的状态标记是位图(比如阻塞 = 0、非阻塞 = 1,只读 = 2、只写 = 4...),如果直接用F_SETFL设置O_NONBLOCK,会覆盖原有属性(比如原本是 "只读 + 阻塞",直接设成O_NONBLOCK会丢失 "只读" 属性)。所以正确做法是:先读取原有属性 → 加上非阻塞标志 → 再写回去。
SetNoBlock 函数
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h> // 补充perror需要的头文件
void SetNoBlock(int fd) {
// 步骤1:读取fd当前的状态标记
int fl = fcntl(fd, F_GETFL);
if (fl < 0) { // 读取失败(比如fd无效)
perror("fcntl"); // 打印错误原因
return;
}
// 步骤2:修改状态标记------保留原有属性,新增O_NONBLOCK
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
-
int fl = fcntl(fd, F_GETFL);- 调用
fcntl,传入cmd=F_GETFL,表示 "读取 fd 的状态标记"; - 返回值
fl是一个整数,每一位代表一个属性(比如第 0 位 = 1 表示非阻塞,第 1 位 = 1 表示只读...)。
- 调用
-
if (fl < 0) { perror("fcntl"); return; }- 如果返回值 < 0,说明读取失败(比如 fd 是无效的,或者没有权限);
perror("fcntl")会打印具体错误(比如 "fcntl: Bad file descriptor" 表示 fd 无效)。
-
fcntl(fd, F_SETFL, fl | O_NONBLOCK);fl | O_NONBLOCK:按位或操作 ------ 保留fl原有所有属性,同时把 "非阻塞位" 设为 1;- 传入
cmd=F_SETFL,把修改后的状态标记写回 fd,此时 fd 就变成非阻塞模式。
轮询读取标准输入:非阻塞 IO 的实战
标准输入的 fd 是 0,下面的代码把 fd=0 设为非阻塞,然后用循环 "轮询" 读取输入(非阻塞 IO 的典型用法):
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h> // 补充memset需要的头文件
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() {
// 步骤1:将标准输入(fd=0)设为非阻塞
SetNoBlock(0);
// 步骤2:无限循环轮询读取输入
while (1) {
char buf[1024] = {0}; // 初始化缓冲区
// 步骤3:非阻塞读取标准输入
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
// 步骤4:处理read的返回值
if (read_size < 0) {
// 读取失败:非阻塞模式下,无数据时会返回-1,错误码是EWOULDBLOCK/EAGAIN
perror("read");
sleep(1); // 轮询间隔,避免CPU空耗
continue; // 继续下一次循环
}
// 步骤5:读取成功,打印输入内容
printf("input:%s\n", buf);
// 清空缓冲区(可选,避免残留数据)
memset(buf, 0, sizeof(buf));
}
return 0;
}