【Linux】高并发服务器的起点:五种 IO 模型与非阻塞 IO 本质解析

文章目录

    • [高并发服务器的起点:五种 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 这一位

操作步骤:

  1. F_GETFL:把当前的位图取出来
  2. O_NONBLOCK上去
  3. 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 的基础。如果这篇帮你把这几个概念理清楚了,点个赞再走吧!后面四篇的内容会越来越精彩 💪

相关推荐
ADDDDDD_Trouvaille1 小时前
2026.2.19——OJ89-91题
c++·算法
wangbing11251 小时前
开发指南141-类和字节数组转换
java·服务器·前端
Trouvaille ~1 小时前
【Linux】select 多路转接深度剖析:从位图原理到字典服务器实现
linux·运维·服务器·c++·select·多路转接·io模型
_OP_CHEN1 小时前
【Linux系统编程】(三十五)揭秘 Linux 信号产生:从终端到内核全解析
linux·运维·操作系统·进程·c/c++·信号·信号产生
mzhan0171 小时前
Linux: 重新理解调度
linux·运维·服务器
郝学胜-神的一滴1 小时前
Effective Modern C++ 条款39:一次事件通信的优雅解决方案
开发语言·数据结构·c++·算法·多线程·并发
一路往蓝-Anbo1 小时前
第 4 章:串口驱动进阶——GPDMA + Idle 中断实现变长数据流接收
linux·人工智能·stm32·单片机·嵌入式硬件
SakitamaX1 小时前
haproxy七层代理介绍与实验
linux·运维·服务器
Дерек的学习记录1 小时前
C++:类和对象part2
c语言·开发语言·c++·学习