一、 IO 的本质与五种 IO 模型
1.1 IO 的核心概念与本质
- 概念解释 :IO (Input/Output) 在计算机科学中指数据在内存与外部设备(如磁盘、网络)之间的流动过程。在操作系统层面,IO = 等待 + 拷贝 。无论是传统的
read/write还是网络的recv/send/recvfrom/sendto,任何 IO 过程都包含这两个步骤:第一步是等待数据准备就绪,第二步是将数据从内核空间拷贝到用户空间(或反之)。 - 笔记:
- 在实际应用场景中,等待消耗的时间往往远远高于拷贝的时间。
- 核心优化结论 :让 IO 更高效,最核心的办法就是让等待的时间尽量少。
- 对于文件描述符 (fd),一般默认情况下:读事件默认是不就绪的(接受缓冲区初始为空),而写事件默认是就绪的(发送缓冲区初始是空的,有空间可写)。
- 报错处理:一旦出错返回
-1并设置errno,最关键的是要查明"为什么出错"。
1.2 阻塞 IO (Blocking IO)
- 概念解释:最常见的 IO 模型。在内核将数据准备好之前,系统调用会一直挂起等待。所有的套接字 (Socket) 默认都是阻塞方式。
- 笔记:
- 执行流程 :应用进程发起
recvfrom系统调用 -> 内核无数据报准备好 -> 进程阻塞于recvfrom调用 -> 数据报准备好 -> 将数据从内核拷贝到用户空间 -> 返回成功指示 -> 处理数据报。 - (类比:专心致志地守在水边钓鱼,不见鱼漂沉下去绝不离开。)
1.3 非阻塞 IO (Non-blocking IO) 与 轮询
- 概念解释 :非阻塞 IO 是指如果内核还未将数据准备好,系统调用不会挂起,而是直接返回一个错误码(通常是
EWOULDBLOCK或EAGAIN)。轮询 (Polling) 则是指程序员需要用循环的方式反复尝试读取或写入数据,直到成功为止。 - 笔记:
- 执行流程 :进程反复调用
recvfrom-> 若无数据则立即返回EWOULDBLOCK-> 循环调用 -> 直到数据准备好 -> 拷贝数据报 -> 返回成功。 - 缺点:轮询对 CPU 资源是较大的浪费,一般只有特定场景下(结合特定的等待机制)才使用。
1.4 信号驱动 IO (Signal-driven IO)
- 概念解释 :内核在数据准备好的时候,主动通过发送
SIGIO信号通知应用程序,应用程序收到信号后再去发起系统调用进行数据拷贝。 - 笔记:
- 执行流程 :建立
SIGIO的信号处理程序(通过sigaction) -> 进程继续执行主逻辑(不阻塞) -> 数据准备好 -> 内核递交SIGIO信号 -> 应用程序调用recvfrom-> 拷贝数据期间进程阻塞 -> 完成后处理数据。
1.5 IO 多路转接 / 多路复用 (IO Multiplexing)
- 概念解释 :通过一种机制(如
select、poll、epoll),让单个进程能够同时等待多个文件描述符 (fd) 的就绪状态。虽然它在数据拷贝时也是阻塞的,但它的核心优势在于一次等待多个 fd。 - 笔记:
- 执行流程 :应用进程受阻于
select调用 -> 等待多个套接口中的任意一个变为可读 -> 返回可读条件 -> 应用程序再调用recvfrom进行无阻塞的数据拷贝。
1.6 异步 IO (Asynchronous IO)
- 概念解释 :由内核负责数据的拷贝工作。内核在数据拷贝完全完成时,才通知应用程序直接去使用数据。
- 笔记:
- 执行流程 :调用
aio_read-> 立即返回,进程继续执行 -> 内核等待数据并主动将数据拷贝到用户空间 -> 拷贝完成后递交指定信号通知进程。 - 与信号驱动的对比 :信号驱动是告诉应用程序"何时可以开始 拷贝数据"(拷贝过程仍由应用程序自己阻塞完成);异步 IO 是告诉应用程序"何时数据已经拷贝完成"。
- (类比总结《妖怪蒸唐僧》:不同小妖看守蒸笼的方式对应不同的 IO 等待模型。)
二、 高级 IO 核心理论解析
2.1 同步通信 vs 异步通信
- 概念解释 :关注的是消息通信机制(注意区分多线程中的同步与互斥概念)。
- 笔记:
- 同步 (Synchronous) :发出调用时,在没有得到结果之前,该调用就不返回。一旦返回,就得到返回值。由调用者主动等待调用的结果。
- 异步 (Asynchronous) :调用发出后直接返回,没有返回结果。被调用者在完成任务后,通过状态、通知或回调函数来通知调用者。
2.2 阻塞 vs 非阻塞
- 概念解释 :关注的是程序在等待调用结果(消息、返回值)时的状态。
- 笔记:
- 阻塞 (Blocking):调用结果返回之前,当前线程会被挂起(休眠)。
- 非阻塞 (Non-blocking):在不能立刻得到结果之前,该调用不会阻塞当前线程,直接返回错误标志。
【发散思考与解答】
问:同步/异步 与 阻塞/非阻塞 常常被混淆,它们到底有何组合关系?
答:同步/异步是"拿结果的方式"(我主动等结果还是你送结果过来),阻塞/非阻塞是"等结果时的状态"(我是睡觉等还是边干活边等)。例如:
- 同步阻塞 :最常见的传统
recv(主动去拿数据,没拿到就死等挂起)。- 同步非阻塞 :设置了非阻塞标志的
recv轮询(主动去拿数据,没拿到直接返回错误,我不挂起,一会再来问)。- 异步非阻塞 :
aio_read(告诉内核把数据准备好并送到缓冲区,我不挂起继续干活,内核弄好了通知我)。
三、 高级 IO 实践:非阻塞 IO (fcntl)
3.1 涉及的核心函数
- 函数名 :
fcntl - 函数原型:
c
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
功能与参数说明 :
控制文件描述符的属性。cmd 决定了函数的具体行为,常用的有 5 种功能:
- 复制现有描述符 (
cmd=F_DUPFD)。 - 获得/设置文件描述符标记 (
cmd=F_GETFD或F_SETFD)。 - 获得/设置文件状态标记 (
cmd=F_GETFL或F_SETFL) ------ 这是实现非阻塞的核心。 - 获得/设置异步 I/O 所有权 (
cmd=F_GETOWN或F_SETOWN)。 - 获得/设置记录锁 (
cmd=F_GETLK,F_SETLK或F_SETLKW)。
3.2 实现 SetNoBlock 非阻塞设置
- 笔记:
- 一个文件描述符默认是阻塞 IO。
- 实现逻辑:先使用
F_GETFL取出当前属性(位图),再附加O_NONBLOCK参数并通过F_SETFL设置回去。
c
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL); // 取出当前文件描述符属性
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 追加非阻塞标记并设置回内核
}
- 应用层非阻塞轮询读取
fd=0(标准输入) 时,如果没有输入,read返回<0,必须结合sleep进行轮询,避免 CPU 100% 占用空转。