云计算百科
云计算领域专业知识百科平台

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

文章目录

    • 高并发服务器的起点:五种 IO 模型与非阻塞 IO 本质解析
    • 一、从钓鱼说起:五种 IO 模型全景图
      • 1.1 IO 的本质:等待 + 拷贝
      • 1.2 钓鱼故事:五种模型的生动类比
        • 1. 阻塞 IO(Blocking IO)
        • 2. 非阻塞 IO(Non-blocking IO)
        • 3. 信号驱动 IO(Signal-driven IO)
        • 4. IO 多路转接(IO Multiplexing)
        • 5. 异步 IO(Asynchronous IO)
      • 1.3 五比
    • 二、最容易搞混的四个概念
      • 2.1 同步 vs 异步:关注消息通信机制
      • 2.2 阻塞 vs 非阻塞:关注程序等待时的状态
      • 2.3 四者组合:妖怪蒸唐僧的例子
      • 2.4 从代码角度理解阻塞和非阻塞
    • 三、高级 IO 概念梳理
      • 3.1 高级 IO 的分类
    • 四、fcntl:实现非阻塞 IO 的利器
      • 4.1 fcntl 函数概览
      • 4.2 fcntl 的工作原理
      • 4.3 实现 SetNoBlock 函数
    • 五、非阻塞 IO 实战:轮询读取标准输入
      • 5.1 场景描述
      • 5.2 完整代码
      • 5.3 运行效果分析
    • 六、IO 流程可视化:从系统调用到数据
      • 6.1 网络 IO 的完整路径
      • 6.2 为什么 IO 多路转接最适合高并发服务器
    • 七、常见问题与概念辨析
      • 7.1 容易混淆的点
        • 1. IO 多路转接 vs 异步 IO,谁更高效?
        • 2. IO 多路转接自身是阻塞的,怎么说它是"非阻塞"?
        • 3. EAGAIN 和 EWOULDBLOCK 的区别?
        • 4. 为什么 ET 模式(epoll 边缘触发)必须配非阻塞 IO?
      • 7.2 典型面试题解析
    • 八、知识点总结
      • 8.1 核心要点
      • 8.2 概念记忆技巧

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

💬 开篇:如果你已经掌握了 socket 编程的基础,能写出一个简单的 TCP 服务器,那恭喜你——你已经站在了 Linux 网络编程进阶的门口。打开这扇门,迎接你的是一个关于"等待"和"效率"的哲学命题:当网络数据还没来的时候,程序该干什么? 这篇文章就是回答这个问题的。我们会从五种 IO 模型的本质讲起,深入分析同步/异步、阻塞/非阻塞这四个容易搞混的概念,最后落地到 fcntl 实现非阻塞 IO 的完整代码。理解了这篇,后面的 select、poll、epoll 才能真正学进去。

👍 点赞、收藏与分享:IO 模型是 Linux 后端开发和高性能服务器的必考点,搞懂了这里,面试官问你"说说 epoll 的工作原理"时,你就能从根上讲清楚,而不只是背几个结论。

🚀 循序渐进:我们先从一个钓鱼的故事开始,把五种 IO 模型讲透彻;再辨析同步/异步、阻塞/非阻塞这四个概念;最后动手实现非阻塞 IO,为后续的多路复用打好基础。


一、从钓鱼说起:五种 IO 模型全景图

1.1 IO 的本质:等待 + 拷贝

在展开五种模型之前,我们先搞清楚一件事:一次 IO 操作,到底发生了什么?

无论是读取网络数据、读文件,还是读标准输入,本质上都分两个阶段:

阶段一:等待数据就绪
数据在网卡/磁盘/键盘上还没到达,内核在等

阶段二:数据拷贝
内核把就绪的数据从内核缓冲区拷贝到用户缓冲区

这两个阶段缺一不可。五种 IO 模型的区别,就在于这两个阶段是怎么处理的。

一个关键结论先记住:等待消耗的时间,往往远远大于拷贝消耗的时间。 所以让 IO 更高效的核心办法,就是让等待的时间尽量少。五种模型的演化,都是围绕这一点展开的。


1.2 钓鱼故事:五种模型的生动类比

假设你要去钓鱼。鱼什么时候咬钩,你不知道(就像网络数据什么时候来,你也不知道)。

1. 阻塞 IO(Blocking IO)

你把鱼竿插进水里,然后就坐在那里等,哪都不去,直到鱼咬钩。

这就是阻塞 IO:调用 read() 之后,如果数据还没来,进程/线程就被挂起(阻塞),什么也干不了,直到内核把数据准备好,拷贝完成,read() 才返回。

你的程序 内核
| |
|— read() 系统调用 —–>|
| | 等待数据到来...
| (程序阻塞在这) |
| | 数据到了!开始拷贝
| |
|<— 拷贝完成,返回 ——-|
| |

特点:

  • 所有套接字默认都是阻塞模式
  • 最简单、最常见
  • 缺点是一个线程只能盯着一个 IO,效率低下

2. 非阻塞 IO(Non-blocking IO)

你把鱼竿插进水里,每隔几秒就过来看一眼,没咬钩就去旁边玩,再回来看,周而复始。

这就是非阻塞 IO:调用 read() 时如果数据还没准备好,内核直接返回一个错误码 EWOULDBLOCK,而不是让你等。程序可以继续干别的事,但需要不断地重复调用(轮询,polling)。

你的程序 内核
| |
|— read() ———–> |
|<— EWOULDBLOCK —— | 数据还没来
| |
|— read() ———–> |
|<— EWOULDBLOCK —— | 数据还没来
| |
|— read() ———–> |
| | 数据来了!拷贝中...
|<— 返回数据 ———- |

特点:

  • 需要程序主动轮询,消耗 CPU(CPU 一直在问"好了吗?好了吗?")
  • 一般只在特定场景下使用,不推荐单独使用

3. 信号驱动 IO(Signal-driven IO)

你在鱼竿上装一个铃铛,鱼咬钩时铃铛会响,你才过来处理。

这就是信号驱动 IO:程序向内核注册一个信号处理函数(SIGIO),然后继续干别的事。当数据准备好时,内核发送 SIGIO 信号通知程序,程序收到信号后再去调用 read() 把数据取走。

你的程序 内核
| |
| 注册 SIGIO 信号处理函数 |
|—告诉我数据来了———>|
| |
| (程序继续干其他事) | 等待数据...
| |
| | 数据来了!发信号
|<== SIGIO 信号 ===========|
| |
|— read() ———–> |
|<— 拷贝完成,返回 —— |

特点:

  • 等待阶段不阻塞,比较灵活
  • 但在 TCP 中,信号触发情况复杂(各种事件都会触发),实际使用较少

4. IO 多路转接(IO Multiplexing)

你同时放了 100 根鱼竿,然后雇了一个管理员(select哪根咬钩了就通知你去处理哪根。)

这就是 IO 多路转接,也叫 IO 多路复用(multiplexing)。这才是高性能服务器的核心技术。

你的程序 内核
| |
| 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:内核告诉你“已经拷贝好了”,你直接用缓冲区数据

你的程序 内核
| |
| 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 最彻底,但实现复杂

五种 IO 模型的本质区别:
等待阶段 拷贝阶段
阻塞 IO ████████████████ ████
非阻塞 IO ✓轮询✓轮询✓就绪! ████
信号驱动 IO —干其他—SIGIO! ████
IO 多路转接 ████(多个fd)█████ ████
异步 IO —干其他—完成! (内核帮你拷)


二、最容易搞混的四个概念

2.1 同步 vs 异步:关注消息通信机制

这两个概念在不同语境下有不同含义,一定要先搞清楚背景。在 IO 模型中:

同步(Synchronous):调用者发出调用后,在没有得到结果之前,调用不返回。调用者主动等待这个调用的结果。

异步(Asynchronous):调用者发出调用后,调用立刻返回,没有返回结果。被调用者通过状态通知或回调函数来告知调用者结果。

重要:这里的"同步/异步"是 IO 通信机制的概念,跟多线程中"同步与互斥"的"同步"完全不是一回事!多线程的同步指的是线程间的协作次序控制,尤其是访问临界资源时的协调。以后看到"同步"这个词,一定先搞清楚大背景!


2.2 阻塞 vs 非阻塞:关注程序等待时的状态

阻塞:调用结果返回之前,当前线程会被挂起。调用线程只有得到结果之后才会返回。

非阻塞:不能立刻得到结果之前,该调用不会阻塞当前线程,直接返回(可能带错误码)。


2.3 四者组合:妖怪蒸唐僧的例子

我们用一个生动的例子来理解这四个概念的组合关系。

妖怪抓到了唐僧,想蒸来吃,锅得烧热。妖怪有几种等法:

阻塞 + 同步(傻等型): 妖怪盯着锅,一直等到锅热,啥也不干。

妖怪:(盯着锅)
妖怪:(还在盯)
锅:我热了!
妖怪:好,下一步

非阻塞 + 同步(轮询型): 妖怪每隔一会问一次"热了吗",没热就去玩会儿,再来问。自己主动轮询。

妖怪:热了吗?(没热)去玩
妖怪:热了吗?(没热)去玩
锅:我热了!
妖怪:(下次来问时)好,下一步

非阻塞 + 异步(信号通知型): 妖怪干其他事,锅热了让小妖怪来通知它。妖怪收到通知后,自己去检查、处理。

妖怪:去干别的事了
锅:(通过小妖怪)我热了!
妖怪:(收到通知)我来了,开始下一步

完全异步(全托管型): 妖怪干别的事,不只是等通知,而是全程托管——连唐僧也让别人处理,做好了通知妖怪来吃。


2.4 从代码角度理解阻塞和非阻塞

来看一个直观的对比:

// 阻塞读取:如果没有数据,程序卡在 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 下操控文件描述符属性的瑞士军刀,函数原型如下:

#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 函数

    #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 完整代码

    #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 运行效果分析

    编译并运行:

    gcc nonblock_stdin.c -o nonblock_stdin
    ./nonblock_stdin

    运行后的行为:

    • 如果你不输入任何内容,程序每秒打印一次"没有输入,等 1 秒后重试…"
    • 你输入 hello 并回车,程序立刻打印 [输入] hello
    • 按 Ctrl+D,程序退出

    这个例子揭示了非阻塞 IO 的两个关键点:

    1. EWOULDBLOCK / EAGAIN 不是真正的错误

    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 模型,还需要知道数据是如何从网卡到达你的程序的:

    网络数据到达的完整路径

    远端主机
    |
    | 发送数据包

    [网卡 NIC]
    |
    | 硬件中断 / DMA

    [内核接收缓冲区] ← 数据"就绪"就是指到这里
    |
    | 系统调用(read/recv)

    [用户空间缓冲区] ← 你的 buf[1024]
    |
    | 程序处理

    [你的业务逻辑]

    理解了这个路径,五种 IO 模型的区别就更清晰了:

    • 等待阶段:等数据从网卡到达内核缓冲区
    • 拷贝阶段:把数据从内核缓冲区拷贝到用户缓冲区

    6.2 为什么 IO 多路转接最适合高并发服务器

    假设你的服务器有 10000 个客户端连接:

    传统方法(一连接一线程):

    线程 1:阻塞等 客户端1 的数据
    线程 2:阻塞等 客户端2 的数据
    ...
    线程 10000:阻塞等 客户端10000 的数据

    • 10000 个线程!内存开销巨大,上下文切换开销大
    • 大部分时候只有少数客户端在活跃,其他线程都在白白睡觉

    IO 多路转接(单线程/少量线程 + epoll):

    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 的区别?

    // 在 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 概念记忆技巧

    记住这张表,面试被问到直接不慌:

    同步/异步 → 谁来通知结果(调用者主动等 / 被调用者来通知)
    阻塞/非阻塞 → 调用者等待时的状态(挂起 / 继续执行)

    五种模型的区别 → 钓鱼故事:
    阻塞 = 呆看
    信号驱动 = 装铃铛
    IO多路转接 = 雇管家同时盯100根竿
    异步IO = 全托管,搞好了叫你

    同时:在IO模型里面,同步异步的本质区别还有:

    • 同步 IO:拷贝阶段由用户线程触发(read/recv 去拷贝)
    • 异步 IO:等待+拷贝都由内核完成,完成后通知用户线程

    💬 总结:这篇文章我们从最底层的 IO 两阶段模型出发,走通了五种 IO 模型的演进逻辑,厘清了同步/异步、阻塞/非阻塞这四个关键概念,并通过 fcntl 实现了第一个非阻塞 IO 的实战代码。现在你应该能清楚地说出:为什么 IO 多路转接是高并发服务器的基础,以及为什么非阻塞 IO 通常要和多路复用配合使用。下一篇,我们开始深入 select ——历史最悠久的多路转接实现,从它的位图原理到字典服务器的完整实现,一步步拆解。

    👍 点赞、收藏与分享:IO 模型是面试高频知识点,也是理解 epoll、Reactor 的基础。如果这篇帮你把这几个概念理清楚了,点个赞再走吧!后面四篇的内容会越来越精彩 💪

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux】高并发服务器的起点:五种 IO 模型与非阻塞 IO 本质解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!