文章目录
-
- [高并发服务器的起点:五种 IO 模型与非阻塞 IO 本质解析](#高并发服务器的起点:五种 IO 模型与非阻塞 IO 本质解析)
- [一、从钓鱼说起:五种 IO 模型全景图](#一、从钓鱼说起:五种 IO 模型全景图)
-
- [1.1 IO 的本质:等待 + 拷贝](#1.1 IO 的本质:等待 + 拷贝)
- [1.2 钓鱼故事:五种模型的生动类比](#1.2 钓鱼故事:五种模型的生动类比)
-
- [1. 阻塞 IO(Blocking IO)](#1. 阻塞 IO(Blocking IO))
- [2. 非阻塞 IO(Non-blocking IO)](#2. 非阻塞 IO(Non-blocking IO))
- [3. 信号驱动 IO(Signal-driven IO)](#3. 信号驱动 IO(Signal-driven IO))
- [4. IO 多路转接(IO Multiplexing)](#4. IO 多路转接(IO Multiplexing))
- [5. 异步 IO(Asynchronous IO)](#5. 异步 IO(Asynchronous IO))
- [1.3 五比](#1.3 五比)
- 二、最容易搞混的四个概念
-
- [2.1 同步 vs 异步:关注消息通信机制](#2.1 同步 vs 异步:关注消息通信机制)
- [2.2 阻塞 vs 非阻塞:关注程序等待时的状态](#2.2 阻塞 vs 非阻塞:关注程序等待时的状态)
- [2.3 四者组合:妖怪蒸唐僧的例子](#2.3 四者组合:妖怪蒸唐僧的例子)
- [2.4 从代码角度理解阻塞和非阻塞](#2.4 从代码角度理解阻塞和非阻塞)
- [三、高级 IO 概念梳理](#三、高级 IO 概念梳理)
-
- [3.1 高级 IO 的分类](#3.1 高级 IO 的分类)
- [四、fcntl:实现非阻塞 IO 的利器](#四、fcntl:实现非阻塞 IO 的利器)
-
- [4.1 fcntl 函数概览](#4.1 fcntl 函数概览)
- [4.2 fcntl 的工作原理](#4.2 fcntl 的工作原理)
- [4.3 实现 SetNoBlock 函数](#4.3 实现 SetNoBlock 函数)
- [五、非阻塞 IO 实战:轮询读取标准输入](#五、非阻塞 IO 实战:轮询读取标准输入)
-
- [5.1 场景描述](#5.1 场景描述)
- [5.2 完整代码](#5.2 完整代码)
- [5.3 运行效果分析](#5.3 运行效果分析)
- [六、IO 流程可视化:从系统调用到数据](#六、IO 流程可视化:从系统调用到数据)
-
- [6.1 网络 IO 的完整路径](#6.1 网络 IO 的完整路径)
- [6.2 为什么 IO 多路转接最适合高并发服务器](#6.2 为什么 IO 多路转接最适合高并发服务器)
- 七、常见问题与概念辨析
-
- [7.1 容易混淆的点](#7.1 容易混淆的点)
-
- [1. IO 多路转接 vs 异步 IO,谁更高效?](#1. IO 多路转接 vs 异步 IO,谁更高效?)
- [2. IO 多路转接自身是阻塞的,怎么说它是"非阻塞"?](#2. IO 多路转接自身是阻塞的,怎么说它是"非阻塞"?)
- [3. EAGAIN 和 EWOULDBLOCK 的区别?](#3. EAGAIN 和 EWOULDBLOCK 的区别?)
- [4. 为什么 ET 模式(epoll 边缘触发)必须配非阻塞 IO?](#4. 为什么 ET 模式(epoll 边缘触发)必须配非阻塞 IO?)
- [7.2 典型面试题解析](#7.2 典型面试题解析)
- 八、知识点总结
-
- [8.1 核心要点](#8.1 核心要点)
- [8.2 概念记忆技巧](#8.2 概念记忆技巧)
高并发服务器的起点:五种 IO 模型与非阻塞 IO 本质解析
💬 开篇 :如果你已经掌握了 socket 编程的基础,能写出一个简单的 TCP 服务器,那恭喜你------你已经站在了 Linux 网络编程进阶的门口。打开这扇门,迎接你的是一个关于"等待"和"效率"的哲学命题:当网络数据还没来的时候,程序该干什么?
这篇文章就是回答这个问题的。我们会从五种 IO 模型的本质讲起,深入分析同步/异步、阻塞/非阻塞这四个容易搞混的概念,最后落地到
fcntl实现非阻塞 IO 的完整代码。理解了这篇,后面的 select、poll、epoll 才能真正学进去。👍 点赞、收藏与分享:IO 模型是 Linux 后端开发和高性能服务器的必考点,搞懂了这里,面试官问你"说说 epoll 的工作原理"时,你就能从根上讲清楚,而不只是背几个结论。
🚀 循序渐进:我们先从一个钓鱼的故事开始,把五种 IO 模型讲透彻;再辨析同步/异步、阻塞/非阻塞这四个概念;最后动手实现非阻塞 IO,为后续的多路复用打好基础。
一、从钓鱼说起:五种 IO 模型全景图
1.1 IO 的本质:等待 + 拷贝
在展开五种模型之前,我们先搞清楚一件事:一次 IO 操作,到底发生了什么?
无论是读取网络数据、读文件,还是读标准输入,本质上都分两个阶段:
bash
阶段一:等待数据就绪
数据在网卡/磁盘/键盘上还没到达,内核在等
↓
阶段二:数据拷贝
内核把就绪的数据从内核缓冲区拷贝到用户缓冲区
这两个阶段缺一不可。五种 IO 模型的区别,就在于这两个阶段是怎么处理的。
一个关键结论先记住:等待消耗的时间,往往远远大于拷贝消耗的时间。 所以让 IO 更高效的核心办法,就是让等待的时间尽量少。五种模型的演化,都是围绕这一点展开的。
1.2 钓鱼故事:五种模型的生动类比
假设你要去钓鱼。鱼什么时候咬钩,你不知道(就像网络数据什么时候来,你也不知道)。
1. 阻塞 IO(Blocking IO)
你把鱼竿插进水里,然后就坐在那里等,哪都不去,直到鱼咬钩。
这就是阻塞 IO:调用 read() 之后,如果数据还没来,进程/线程就被挂起(阻塞),什么也干不了,直到内核把数据准备好,拷贝完成,read() 才返回。
bash
你的程序 内核
| |
|--- read() 系统调用 ----->|
| | 等待数据到来...
| (程序阻塞在这) |
| | 数据到了!开始拷贝
| |
|<--- 拷贝完成,返回 -------|
| |
特点:
- 所有套接字默认都是阻塞模式
- 最简单、最常见
- 缺点是一个线程只能盯着一个 IO,效率低下
2. 非阻塞 IO(Non-blocking IO)
你把鱼竿插进水里,每隔几秒就过来看一眼,没咬钩就去旁边玩,再回来看,周而复始。
这就是非阻塞 IO:调用 read() 时如果数据还没准备好,内核直接返回一个错误码 EWOULDBLOCK,而不是让你等。程序可以继续干别的事,但需要不断地重复调用(轮询,polling)。
bash
你的程序 内核
| |
|--- read() -----------> |
|<-- EWOULDBLOCK ------ | 数据还没来
| |
|--- read() -----------> |
|<-- EWOULDBLOCK ------ | 数据还没来
| |
|--- read() -----------> |
| | 数据来了!拷贝中...
|<-- 返回数据 ---------- |
特点:
- 需要程序主动轮询,消耗 CPU(CPU 一直在问"好了吗?好了吗?")
- 一般只在特定场景下使用,不推荐单独使用
3. 信号驱动 IO(Signal-driven IO)
你在鱼竿上装一个铃铛,鱼咬钩时铃铛会响,你才过来处理。
这就是信号驱动 IO:程序向内核注册一个信号处理函数(SIGIO),然后继续干别的事。当数据准备好时,内核发送 SIGIO 信号通知程序,程序收到信号后再去调用 read() 把数据取走。
bash
你的程序 内核
| |
| 注册 SIGIO 信号处理函数 |
|---告诉我数据来了--------->|
| |
| (程序继续干其他事) | 等待数据...
| |
| | 数据来了!发信号
|<== SIGIO 信号 ===========|
| |
|--- read() -----------> |
|<-- 拷贝完成,返回 ------ |
特点:
- 等待阶段不阻塞,比较灵活
- 但在 TCP 中,信号触发情况复杂(各种事件都会触发),实际使用较少
4. IO 多路转接(IO Multiplexing)
你同时放了 100 根鱼竿,然后雇了一个管理员(select哪根咬钩了就通知你去处理哪根。)
这就是 IO 多路转接,也叫 IO 多路复用(multiplexing)。这才是高性能服务器的核心技术。
bash
你的程序 内核
| |
| select/epoll_wait() |
|--- 监控 fd1,fd2...fd100->|
| | 等待任一 fd 就绪
| (程序阻塞在 select) |
| | fd5 就绪了!
|<--- 返回就绪的 fd --------|
| |
| 对 fd5 调用 read() |
|--- read(fd5) ---------->|
|<-- 拷贝完成,返回 --------|
特点:
- 一个进程能处理大量连接(这就是 C10K 问题的解决思路)
- select/poll/epoll 是三种实现,后面我们会详细讲
- 注意:select 本身是阻塞的,但它能同时监控多个 fd,解决了"一次只能等一个"的问题
5. 异步 IO(Asynchronous IO)
你委托一个全职助手去钓鱼:鱼咬钩、收杆、处理、放进冰箱,全流程都由助手完成,最后只通知你"鱼已经在冰箱里了"。
程序发起 IO 请求后立刻返回 ,内核负责等待数据就绪 + 完成数据拷贝到用户缓冲区 ,全部完成后再通过信号/回调/事件通知程序:数据已经可以直接用了。
与信号驱动 IO 的关键区别:
- 信号驱动:内核告诉你"可以开始读了 ",你自己
read()完成拷贝 - 异步 IO:内核告诉你"已经拷贝好了",你直接用缓冲区数据
bash
你的程序 内核
| |
| aio_read() |
|--- 发出 IO 请求,立即返回->|
| |
| (程序继续干其他事) | 等待数据 + 拷贝...
| |
| | 全部完成!通知程序
|<== 回调/信号 ============|
| |
| 直接使用缓冲区里的数据 |
异步 IO 是最彻底的非阻塞,两现复杂,Linux 下的 aio 支持有限。
1.3 五比
| IO 模型 | 等待阶段 | 拷贝阶段 | 典型接口 | 适用场景 |
|---|---|---|---|---|
| 阻塞 IO | 阻塞 | 阻塞 | read/write |
简单场景,连接数少 |
| 非阻塞 IO | 不阻塞(轮询) | 阻塞 | read + O_NONBLOCK |
配合 IO 多路复用使用 |
| 信号驱动 IO | 不阻塞(信号通知) | 阻塞 | SIGIO |
使用较少 |
| IO 多路转接 | 阻塞(但监控多个) | 阻塞 | select/poll/epoll |
高并发服务器首选 |
| 异步 IO | 不阻塞 | 不阻塞 | aio_read/aio_write |
最彻底,但实现复杂 |
bash
五种 IO 模型的本质区别:
等待阶段 拷贝阶段
阻塞 IO ████████████████ ████
非阻塞 IO ✓轮询✓轮询✓就绪! ████
信号驱动 IO ---干其他---SIGIO! ████
IO 多路转接 ████(多个fd)█████ ████
异步 IO ---干其他---完成! (内核帮你拷)
二、最容易搞混的四个概念
2.1 同步 vs 异步:关注消息通信机制
这两个概念在不同语境下有不同含义,一定要先搞清楚背景。在 IO 模型中:
同步(Synchronous):调用者发出调用后,在没有得到结果之前,调用不返回。调用者主动等待这个调用的结果。
异步(Asynchronous):调用者发出调用后,调用立刻返回,没有返回结果。被调用者通过状态通知或回调函数来告知调用者结果。
重要:这里的"同步/异步"是 IO 通信机制的概念,跟多线程中"同步与互斥"的"同步"完全不是一回事!多线程的同步指的是线程间的协作次序控制,尤其是访问临界资源时的协调。以后看到"同步"这个词,一定先搞清楚大背景!
2.2 阻塞 vs 非阻塞:关注程序等待时的状态
阻塞 :调用结果返回之前,当前线程会被挂起。调用线程只有得到结果之后才会返回。
非阻塞 :不能立刻得到结果之前,该调用不会阻塞当前线程,直接返回(可能带错误码)。
2.3 四者组合:妖怪蒸唐僧的例子
我们用一个生动的例子来理解这四个概念的组合关系。
妖怪抓到了唐僧,想蒸来吃,锅得烧热。妖怪有几种等法:
阻塞 + 同步 (傻等型):
妖怪盯着锅,一直等到锅热,啥也不干。
bash
妖怪:(盯着锅)
妖怪:(还在盯)
锅:我热了!
妖怪:好,下一步
非阻塞 + 同步 (轮询型):
妖怪每隔一会问一次"热了吗",没热就去玩会儿,再来问。自己主动轮询。
bash
妖怪:热了吗?(没热)去玩
妖怪:热了吗?(没热)去玩
锅:我热了!
妖怪:(下次来问时)好,下一步
非阻塞 + 异步 (信号通知型):
妖怪干其他事,锅热了让小妖怪来通知它。妖怪收到通知后,自己去检查、处理。
bash
妖怪:去干别的事了
锅:(通过小妖怪)我热了!
妖怪:(收到通知)我来了,开始下一步
完全异步 (全托管型):
妖怪干别的事,不只是等通知,而是全程托管------连唐僧也让别人处理,做好了通知妖怪来吃。
2.4 从代码角度理解阻塞和非阻塞
来看一个直观的对比:
cpp
// 阻塞读取:如果没有数据,程序卡在 read() 这一行
ssize_t n = read(fd, buf, sizeof(buf));
// 只有数据来了,才能到达下一行
// 非阻塞读取:如果没有数据,立即返回 -1,errno = EWOULDBLOCK
ssize_t n = read(fd, buf, sizeof(buf)); // fd 设置了 O_NONBLOCK
if (n < 0 && errno == EWOULDBLOCK) {
// 数据还没来,稍后重试
continue;
}
// 程序不会被卡住
阻塞就像"你不给我,我就不走";非阻塞就像"你没有就告诉我一声,我先去忙别的"。
三、高级 IO 概念梳理
3.1 高级 IO 的分类
Linux 中,除了基础的阻塞 IO,以下都属于高级 IO:
| 高级 IO 类型 | 说明 |
|---|---|
| 非阻塞 IO | fcntl 设置 O_NONBLOCK |
| 记录锁 | fcntl 的锁功能 |
| IO 多路转接 | select / poll / epoll |
readv / writev |
分散读 / 聚集写 |
| 存储映射 IO | mmap |
| 系统 V 流机制 | 较旧的机制 |
我们这个系列的重点:IO 多路转接(select → poll → epoll → Reactor 模式)
四、fcntl:实现非阻塞 IO 的利器
4.1 fcntl 函数概览
fcntl 是 Linux 下操控文件描述符属性的瑞士军刀,函数原型如下:
c
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
根据 cmd 的不同,它有五种功能:
| cmd | 功能 |
|---|---|
F_DUPFD |
复制文件描述符 |
F_GETFD / F_SETFD |
获取/设置文件描述符标记 |
F_GETFL / F_SETFL |
获取/设置文件状态标记 ← 我们用这个 |
F_GETOWN / F_SETOWN |
获取/设置异步 IO 所有权 |
F_GETLK / F_SETLK / F_SETLKW |
获取/设置记录锁 |
我们要实现非阻塞 IO,用的是第三种:获取/设置文件状态标记。
4.2 fcntl 的工作原理
文件描述符有一套状态标记位图(flags),记录着这个 fd 的各种属性,比如:
O_RDONLY:只读O_WRONLY:只写O_RDWR:读写O_NONBLOCK:非阻塞 ← 我们要加的O_APPEND:追加写入O_ASYNC:异步 IO
想让一个 fd 变成非阻塞,就是在它的状态位图上加上 O_NONBLOCK 这一位。
操作步骤:
F_GETFL:把当前的位图取出来- 把
O_NONBLOCK位或上去 F_SETFL:把修改后的位图写回去
就像是给 fd 贴一张标签:"以后操作我,不要等,没数据就直接告诉我 EWOULDBLOCK"。
4.3 实现 SetNoBlock 函数
cpp
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
/**
* 将文件描述符设置为非阻塞模式
* @param fd 需要设置的文件描述符
*/
void SetNoBlock(int fd) {
// 第一步:获取当前文件状态标记(这是一个位图)
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl F_GETFL");
return;
}
// 第二步:将 O_NONBLOCK 位添加进去(用按位或,不影响其他标志位)
// 错误示范:fcntl(fd, F_SETFL, O_NONBLOCK); // 这会清除其他所有标志!
int ret = fcntl(fd, F_SETFL, fl | O_NONBLOCK);
if (ret < 0) {
perror("fcntl F_SETFL");
return;
}
}
警告 :
F_SETFL时一定要先F_GETFL取出原有标志,再|上O_NONBLOCK!不能直接
fcntl(fd, F_SETFL, O_NONBLOCK),那样会清除fd上所有其他的标志位(比如O_RDWR),后果不可预料。
五、非阻塞 IO 实战:轮询读取标准输入
5.1 场景描述
我们来做一个小实验:把标准输入(fd = 0)设置为非阻塞,然后用循环轮询读取。这能帮我们直观感受非阻塞 IO 的行为。
5.2 完整代码
c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
/**
* 将指定文件描述符设置为非阻塞模式
*/
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl F_GETFL");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main() {
// 把标准输入 (fd=0) 设为非阻塞
SetNoBlock(0);
while (1) {
char buf[1024] = {0};
// 非阻塞 read:没有输入时立即返回 -1
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
// errno == EWOULDBLOCK 或 EAGAIN 表示"暂时没有数据,稍后重试"
// 这是非阻塞 IO 的"正常"情况,不是真正的错误
if (errno == EWOULDBLOCK || errno == EAGAIN) {
printf("[轮询] 没有输入,等 1 秒后重试...\n");
sleep(1);
continue;
}
// 其他错误才是真正的错误
perror("read");
sleep(1);
continue;
}
if (read_size == 0) {
// 返回 0 表示 EOF(对于标准输入,就是 Ctrl+D)
printf("检测到 EOF,退出\n");
break;
}
buf[read_size] = '\0';
printf("[输入] %s\n", buf);
}
return 0;
}
5.3 运行效果分析
编译并运行:
bash
gcc nonblock_stdin.c -o nonblock_stdin
./nonblock_stdin
运行后的行为:
- 如果你不输入任何内容,程序每秒打印一次"没有输入,等 1 秒后重试..."
- 你输入
hello并回车,程序立刻打印[输入] hello - 按
Ctrl+D,程序退出
这个例子揭示了非阻塞 IO 的两个关键点:
1. EWOULDBLOCK / EAGAIN 不是真正的错误
bash
EWOULDBLOCK 和 EAGAIN 在现代 Linux 上是同一个值
含义:操作会阻塞,但 fd 是非阻塞的,所以直接返回了
处理方式:重试!这是预期内的情况
2. 非阻塞轮询非常浪费 CPU
如果你把 sleep(1) 去掉,这个程序会以 CPU 100% 的速度狂转,不停地询问"有数据了吗"。这就是非阻塞 IO 单独使用的问题------轮询会浪费大量 CPU。
所以,非阻塞 IO 通常不单独使用,而是配合 IO 多路转接(select/poll/epoll)一起用。后者告诉你哪个 fd 有数据了,你再去读------既不浪费 CPU 等待,也不浪费 CPU 轮询。
注意:在终端下,标准输入默认按"行"提交(回车才会把这一行交给程序),所以你看到的是回车后 read 才读到数据;如果改成 raw模式(
termios),才可能按键级别返回。
六、IO 流程可视化:从系统调用到数据
6.1 网络 IO 的完整路径
理解 IO 模型,还需要知道数据是如何从网卡到达你的程序的:
bash
网络数据到达的完整路径
远端主机
|
| 发送数据包
↓
[网卡 NIC]
|
| 硬件中断 / DMA
↓
[内核接收缓冲区] ← 数据"就绪"就是指到这里
|
| 系统调用(read/recv)
↓
[用户空间缓冲区] ← 你的 buf[1024]
|
| 程序处理
↓
[你的业务逻辑]
理解了这个路径,五种 IO 模型的区别就更清晰了:
- 等待阶段:等数据从网卡到达内核缓冲区
- 拷贝阶段:把数据从内核缓冲区拷贝到用户缓冲区
6.2 为什么 IO 多路转接最适合高并发服务器
假设你的服务器有 10000 个客户端连接:
传统方法(一连接一线程):
bash
线程 1:阻塞等 客户端1 的数据
线程 2:阻塞等 客户端2 的数据
...
线程 10000:阻塞等 客户端10000 的数据
- 10000 个线程!内存开销巨大,上下文切换开销大
- 大部分时候只有少数客户端在活跃,其他线程都在白白睡觉
IO 多路转接(单线程/少量线程 + epoll):
bash
1 个线程:
epoll_wait() 监控 10000 个 fd
↓ 某几个 fd 就绪了
处理这几个 fd
↓
回到 epoll_wait() 继续监控
- 只用 1 个(或少量)线程
- 只处理真正有数据的连接,不浪费
- 这就是 Nginx、Redis 的核心原理
七、常见问题与概念辨析
7.1 容易混淆的点
1. IO 多路转接 vs 异步 IO,谁更高效?
很多人以为异步 IO 最好,其实不一定。
- IO 多路转接(特别是 epoll)在 Linux 上非常成熟,性能极好
- 异步 IO(
aio)在 Linux 上的支持相对有限,且编程复杂度更高 - Nginx、Redis、Node.js 都基于 IO 多路转接,没有用异步 IO
结论 :epoll + 非阻塞 IO 是目前 Linux 高性能服务器的主流方案。
非常适合:连接数多、但同一时刻真正活跃的连接比例不高的场景
2. IO 多路转接自身是阻塞的,怎么说它是"非阻塞"?
select/poll/epoll_wait 本身是阻塞的(如果没有 fd 就绪,会等待)。
但它的价值不在于"不阻塞",而在于"一次监控多个 fd,只要有一个就绪就返回"。
这让一个线程能服务大量连接,而不是为每个连接开一个线程阻塞等待。
3. EAGAIN 和 EWOULDBLOCK 的区别?
c
// 在 Linux 上,两者通常是同一个值
// /usr/include/asm-generic/errno-base.h:
#define EWOULDBLOCK EAGAIN // 通常 EWOULDBLOCK = EAGAIN = 11
// 所以处理时,两个都判断就行:
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 暂时无数据,重试
}
4. 为什么 ET 模式(epoll 边缘触发)必须配非阻塞 IO?
这个问题我们在 epoll 那篇会深入讲,这里先埋个伏笔。
简单来说:ET 模式下,数据就绪只通知你一次。如果你用阻塞 read,一次 read 可能没读完所有数据,剩下的数据就再也等不到通知了(ET 不会再次触发)。
所以 ET 模式下,必须用非阻塞 read,循环读直到 EAGAIN,确保把缓冲区的数据一次性读完。
7.2 典型面试题解析
Q:说说 select、poll、epoll 的区别。
(先别急着说,从这篇的基础开始引入)
首先,它们都是 IO 多路转接的实现,核心目的是让一个线程监控多个 fd。区别在于:
- select:基于位图,有 fd 数量上限(1024/4096),每次都要拷贝整个位图到内核,返回后还要遍历找就绪的 fd
- poll :解决了 select 的数量限制,用
pollfd数组,但还是每次都要全量拷贝和遍历 - epoll :内核维护红黑树和就绪队列,只拷贝变化的 fd,
epoll_wait直接返回就绪的 fd,性能最好
这些我们后面三篇会详细展开。
八、知识点总结
8.1 核心要点
| # | 要点 | 说明 | |
|---|---|---|---|
| 1 | IO 两阶段 | 等待就绪 + 数据拷贝,等待占大头 | |
| 2 | 五种模型本质 | 对这两个阶段的不同处理策略 | |
| 3 | 同步≠阻塞 | 同步/异步关注通信机制,阻塞/非阻塞关注等待状态 | |
| 4 | fcntl 设置非阻塞 | F_GETFL 取出 + O_NONBLOCK+F_SETFL 写回 |
|
| 5 | EAGAIN 不是错误 | 非阻塞 IO 下"暂无数据"的正常返回,应重试 |
8.2 概念记忆技巧
记住这张表,面试被问到直接不慌:
bash
同步/异步 → 谁来通知结果(调用者主动等 / 被调用者来通知)
阻塞/非阻塞 → 调用者等待时的状态(挂起 / 继续执行)
五种模型的区别 → 钓鱼故事:
阻塞 = 呆看
信号驱动 = 装铃铛
IO多路转接 = 雇管家同时盯100根竿
异步IO = 全托管,搞好了叫你
同时:在IO模型里面,同步异步的本质区别还有:
- 同步 IO:拷贝阶段由用户线程触发(read/recv 去拷贝)
- 异步 IO:等待+拷贝都由内核完成,完成后通知用户线程
💬 总结 :这篇文章我们从最底层的 IO 两阶段模型出发,走通了五种 IO 模型的演进逻辑,厘清了同步/异步、阻塞/非阻塞这四个关键概念,并通过
fcntl实现了第一个非阻塞 IO 的实战代码。现在你应该能清楚地说出:为什么 IO 多路转接是高并发服务器的基础,以及为什么非阻塞 IO 通常要和多路复用配合使用。下一篇,我们开始深入 select ------历史最悠久的多路转接实现,从它的位图原理到字典服务器的完整实现,一步步拆解。
👍 点赞、收藏与分享:IO 模型是面试高频知识点,也是理解 epoll、Reactor 的基础。如果这篇帮你把这几个概念理清楚了,点个赞再走吧!后面四篇的内容会越来越精彩 💪