- 网络IO模型也是 Linux 网络编程经常问到的题目,有次面试没答详细,只是简单介绍了一下,这里再写出来复习用
- 原文参考: IO 模型 五种 IO 模型详解 Unix 网络编程例图
太长不看 ?
- 网络 IO 模型 分为 5 种:阻塞 IO、非阻塞 IO、IO 多路复用、信号驱动 IO、异步 IO(AIO)
- 同步 IO 和 异步 IO
- 这俩不是网络 IO 模型, 而是根据消息通信机制对 IO 类型的分类,同步异步强调的是两个操作之间的顺序关系,两个操作之间是有序的还是无序的。同步与异步的核心区别在于内核是否主动将数据准备好并通知应用程序,以及数据拷贝的责任方。
- 所以网络 IO 模型中, 前四个都是同步,最后一个是异步
- 阻塞 IO 和 非阻塞 IO
- 阻塞与非阻塞强调的是一个调用发起后调用发起方的行为,是被动等待还是主动获得执行权,进程 IO 调用会不会阻塞进程自己。阻塞与非阻塞的核心区别在于应用程序在等待数据准备时,是否会被挂起(即能否做其他事)。
- 阻塞 IO 是网络 IO 模型的一种,那么其余四个就都是非阻塞的
| 对比维度 | 阻塞IO | 非阻塞IO | IO多路复用 | 信号驱动IO | 异步IO |
|---|---|---|---|---|---|
| IO类型 | 同步、阻塞 | 同步、非阻塞 | 同步、非阻塞 | 同步、非阻塞 | 异步、非阻塞 |
| 数据准备阶段 | 应用程序阻塞等待 | 应用程序轮询查询(不阻塞) | 应用程序阻塞在“监听器”(如select),不阻塞单个IO | 应用程序自由,内核就绪后发信号 | 应用程序自由,内核自动处理 |
| 数据拷贝阶段 | 应用程序阻塞 | 应用程序阻塞 | 应用程序阻塞 | 应用程序阻塞 | 内核自动完成,无阻塞 |
| 核心特点 | 简单直观,无需额外操作 | 需频繁轮询,CPU开销高 | 一个线程管理多个IO,效率高 | 无需轮询,依赖信号机制 | 全程无阻塞,内核包办所有 |
| 适用场景 | 连接数少、低并发场景(如简单TCP服务) | 极少单独使用(需配合其他机制) | 高并发、多连接场景(如Nginx、Redis) | 较少用(信号处理复杂) | 对性能要求极高的场景(如高性能服务器) |
| 典型系统调用 | recvfrom(直接阻塞) | recvfrom(非阻塞,立即返回) | select/poll/epoll | sigaction(注册信号)、recvfrom | aio_read/aio_write |
| 分类 | 具体模型 | 特点说明 |
|---|---|---|
| 同步IO | 1. 阻塞IO 2. 非阻塞IO 3. IO多路复用 4. 信号驱动IO |
应用程序需要主动等待或查询“数据是否准备好”,且最终都需要自己发起“数据拷贝”操作。 |
| 异步IO | 1. 异步IO模型 | 内核主动完成“数据准备”和“数据拷贝”,完成后再通知应用程序;应用程序全程无需主动等待或操作。 |
| 阻塞IO | 1. 阻塞IO | 应用程序发起IO请求后,会一直等待(被内核挂起),直到数据准备好并完成拷贝,期间无法执行其他任务。 |
| 非阻塞IO | 1. 非阻塞IO 2. IO多路复用 3. 信号驱动IO 4. 异步IO |
应用程序发起IO请求后,不会被挂起,可立即返回并执行其他任务;数据准备阶段不占用程序的等待时间。 |
什么是 IO ?

IO 即输入输出操作,指计算机系统中数据在不同设备之间的传输过程。IO 操作通常涉及 CPU、内存和外设(如磁盘、网络接口等)之间的数据交换。
- 通常用户进程中的一个完整IO分为两阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网络等)。
- IO 有内存 IO 、网络 IO 和磁盘 IO 三种,通常我们说的 IO 指的是后两者。
- 根据 OS 对 IO 操作的处理方式,或者说 IO 双方的消息通信机制是否一致,IO 可以分为同步 IO 和异步 IO 两种。
- 同步IO:导致请求进程阻塞,直到I/O操作完成。
- 异步IO:不导致请求进程阻塞。
同步 与 异步
同步异步强调的是两个操作之间的顺序关系,两个操作之间是有序的还是无序的。
- 同步指的是在发出一个功能调用时,调用者必须等待这个调用完成并返回结果后,才能继续执行后续的代码。例如,在发送一个请求后,必须等待响应返回才能继续处理其他任务。
- 某个操作 A 必须等待前一个操作 B 完成之后才能开始,也就是说 A 在 B 完成之前不会启动,意味着操作 A 在操作 B 之后按顺序执行,并且 A 必须等待 B 完成后才开始。
- 异步指的是在发出一个功能调用后,调用者不需要等待结果返回,而是可以继续执行其他任务。当结果准备好后,通过回调函数、信号、通知或状态来告知调用者。
- 操作 A 不需要等待前一个操作 B 完成之后才能开始,A 和 B 可以同时进行,或者 A 可以在等待 B 的过程中执行其他操作,意味着操作 A 和操作 B 可以同时执行或 A 不需要等待 B 完成。
同步 IO 与 异步 IO
- 同步 IO 是指在进行输入输出操作时,用户进程需要等待 IO 操作完成后才能继续执行其他任务,用户进程在发出 IO 请求后会被阻塞,直到数据传输完成并返回结果。用户进程和IO设备双方的动作,是经过双方协调的,步调一致的。
- 异步 IO 是指在进行输入输出操作时,用户进程不需要等待 IO 操作完成,即可继续执行其他任务,用户进程在发出 IO 请求后不会被阻塞,而是继续执行其他任务。用户进程和IO设备双方的动作,是不经过双方协调的,都可以随意进行各自的操作。
阻塞 与 非阻塞
阻塞与非阻塞强调的是一个调用发起后调用发起方的行为,是被动等待还是主动获得执行权,进程 IO 调用会不会阻塞进程自己。
- 阻塞调用发出后,调用方会挂起等待,当被调用方执行完成并返回结果后,调用方才会被唤醒并接到结果继续执行之后的操作。
- 在用户进程中调用执行的时候,进程会等待该 IO 操作,而使得其他操作无法执行。
- 非阻塞调用发出后,调用方不会挂起等待,而是立即返回,之后可以选择继续别的操作。被调用方在后台(可能以各种形式实现)处理原本的业务逻辑,处理完成后可以通过回调、信号等机制通知调用方。调用方也会轮询检查操作是否完成。
- 在用户进程中调用执行的时候,无论成功与否,该IO操作会立即返回,之后进程可以进行其他操作。
阻塞 IO 与 非阻塞 IO
- 阻塞 IO , 进程发起一个 IO 调用会一直阻塞下去,直到内核把数据准备好,并将其从内核复制到用户空间,复制完成后才会继续处理数据。
- 非阻塞 IO , 进程发起一个 IO 调用后,不会阻塞进程自己,而是立即返回,可以选择继续别的操作。进程需要不断轮询内核数据是否就绪,在内核数据还未就绪时,应用进程还可以做其他事情。
网络 IO
网络IO是指数据在网络中的传输,包括从客户端发送到服务器端,以及从服务器端发送到客户端。
对于一个输入操作来说,进程 IO 系统调用后,内核会先看缓冲区中有没有相应的缓存数据,有那就复制到进程空间,没有的话再到设备中读取所以,对于一个网络输入操作通常包括两个不同阶段:
- 准备阶段是判断是否能够操作(即等待数据是否可用),在内核进程完成的,等待网络数据到达网卡→读取到内核缓冲区,
- 操作阶段则执行实际的 IO 调用,数据准备好之后会从内核缓冲区复制数据到进程空间。
网络 IO 模型

网络IO模型是指操作系统中处理网络 IO 请求的不同方式。常见的网络 IO 模型有以下五种:
阻塞 IO :进程发起 IO 系统调用后,进程被阻塞,转到内核空间处理,只有等待要操作的数据准备好,并复制到应用进程的缓冲区中才返回;
- 进程阻塞挂起不消耗CPU资源,及时响应每个操作,实现难度低、开发应用较容易,适用并发量小的网络应用开发;不适用并发量大的应用:因为一个请求IO会阻塞进程,所以,得为每请求分配一个处理进程以及时响应,系统开销大。

- 进程阻塞挂起不消耗CPU资源,及时响应每个操作,实现难度低、开发应用较容易,适用并发量小的网络应用开发;不适用并发量大的应用:因为一个请求IO会阻塞进程,所以,得为每请求分配一个处理进程以及时响应,系统开销大。
非阻塞 IO :进程发起 IO 系统调用后,如果内核缓冲区没有数据,需要到 IO 设备中读取,该调用返回一个错误而不会被阻塞,一般情况下,应用进程需要利用轮询的方式来检测某个操作是否就绪。数据就绪后,实际的I/O操作会等待数据复制到应用进程的缓冲区中以后才返回;
- 进程轮询(重复)调用,消耗CPU的资源;实现难度低、开发应用相对阻塞 IO 模式较难;适用并发量较小、且不需要及时响应的网络应用开发;
- 专一进程解决多个进程IO的阻塞问题,性能好;Reactor 模式;适用高并发服务应用开发:一个进程(线程)响应多个请求;

IO复用:多路 IO 共用一个同步阻塞接口,多个的进程的 IO 可以注册到同一个接口上,然后用一个进程调用该接口,这是对阻塞 IO 的改进(主要是 select,poll,epoll ,关键是能实现同时对多个 IO 端口进行监听)。
- 此时阻塞发生在 select/poll/epoll 的系统调用上,而不是阻塞在实际的 IO 系统调用上。IO 多路复用的高级之处在于,它能同时监听多个文件描述符,如果监听的 IO 在内核缓冲区都没有可读数据, select/poll/epoll 调用进程会被阻塞;而当任一 IO 在内核缓冲区中有可数据时, select/poll/epoll 调用就会返回;读取数据完毕后, select/poll/epoll 调用进程可以自己或通知另外的进程(注册进程)来继续监听,再次发起读取 IO,读取内核中准备好的数据。

- 此时阻塞发生在 select/poll/epoll 的系统调用上,而不是阻塞在实际的 IO 系统调用上。IO 多路复用的高级之处在于,它能同时监听多个文件描述符,如果监听的 IO 在内核缓冲区都没有可读数据, select/poll/epoll 调用进程会被阻塞;而当任一 IO 在内核缓冲区中有可数据时, select/poll/epoll 调用就会返回;读取数据完毕后, select/poll/epoll 调用进程可以自己或通知另外的进程(注册进程)来继续监听,再次发起读取 IO,读取内核中准备好的数据。
信号驱动 IO :当进程发起一个 IO 操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个 SIGIO 信号给进程,进程便在信号处理函数中调用 IO 读取数据。
- 回调机制,实现、开发应用难度大;

- 回调机制,实现、开发应用难度大;
异步IO: 应用进程通知内核开始一个异步 IO 操作,然后进程返回(不阻塞),并让内核在整个操作(包含将数据从内核复制到应该进程的缓冲区)完成后通知应用进程。
- 不阻塞,数据一步到位;Proactor模式;需要操作系统的底层支持
网络 IO 模型中的 同步 IO 和异步 IO
避免将它们与网络 IO 模型混淆。在网络 IO 中可以详细扩展成以下两种:同步 IO :用户进程发出IO调用,去获取 IO 设备数据,双方的数据要经过内核缓冲区同步,完全准备好后,再复制返回到用户进程。而复制返回到用户进程会导致请求进程阻塞,直到 IO 操作完成。
异步 IO :用户进程发出IO调用,去获取 IO 设备数据,并不需要同步,内核直接复制到进程,整个过程不导致请求进程阻塞。
阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型在第一阶段即判断是否可操作阶段各不相同,但一旦数据可操作,则切换到同步阻塞模式下执行 IO 操作,所以都算是同步 IO 。
网络 IO 模型中的 阻塞 IO 与 非阻塞 IO
- 阻塞 IO 模型是一个阻塞IO调用,而 非阻塞 IO 模型是多个非阻塞 IO 调用 + 一个阻塞 IO 调用,因为多个 IO 检查会立即返回错误,不会阻塞进程。
- 非阻塞 IO 模型对于阻塞 IO 模型来说区别就是,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。
IO 多路复用
select、poll 以及 epoll 是 Linux 系统的三个系统调用,也是 IO 多路复用模型的具体实现。IO 多路复用就是通过一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作的一种机制。与多进程和多线程技术相比,IO 多路复用技术的最大优势是系统开销小,系统不必创建进程或线程,也不必维护这些进程,从而大大减小了系统的开销。
| 维度 | select | poll | epoll(LT模式) | epoll(ET模式) |
|---|---|---|---|---|
| FD 数量限制 | 有(默认 1024) | 无(受内存限制) | 无(受内存限制) | 无(受内存限制) |
| 用户态到内核态拷贝 | 每次调用拷贝所有 FD 集合 | 每次调用拷贝整个 pollfd 链表 | 仅 epoll_ctl 时拷贝(一次注册) | 仅 epoll_ctl 时拷贝(一次注册) |
| 就绪 FD 查找方式 | 遍历所有 FD(O(n)) | 遍历所有 pollfd(O(n)) | 直接返回就绪列表(O(1)) | 直接返回就绪列表(O(1)) |
| 重复通知机制 | 水平触发(未处理完会重复通知) | 水平触发 | 水平触发 | 边缘触发(仅状态变化时通知一次) |
| 系统调用效率 | 低(随 FD 增加下降明显) | 中(优于 select,仍随 FD 下降) | 高(几乎不随 FD 增加下降) | 高(效率略高于 LT) |
| 跨平台性 | 好(Linux/Windows/BSD 均支持) | 好(同上) | 差(仅 Linux 支持) | 差(仅 Linux 支持) |
- select
1 | int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, |
select 函数监视的文件描述符分三类,分别是 writefds、readfds 和 exceptfds。
select 调用过程:
- 核心逻辑:进程通过 select 系统调用,将需要监听的 文件描述符(FD) 集合(读 / 写 / 异常三类)传递给内核,内核阻塞等待这些 FD 中任意一个就绪(有数据可读 / 可写 / 异常),就绪后返回就绪的 FD 数量,进程再遍历所有 FD 找出就绪的进行处理。
- 用户进程需要监控某些资源 fds,在调用 select 函数后会阻塞,操作系统会将用户线程加入这些资源的等待队列中。
- 直到有文件描述符就绪(有数据可读、可写或有 except)或超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。
- select 函数返回后,中断程序唤起用户线程。用户可以遍历 fds(所有 fd, 包括未就绪的),通过 FD_ISSET 判断具体哪个 fd 收到数据,并做出相应处理。
- 使用位掩码(bitmask)存储 FD 集合(如 fd_set 结构体),本质是固定大小的整数数组(每个位代表一个 FD)。
优点:实现起来简单有效,且几乎所有操作系统都有对应的实现。
缺点: - 每次调用 select 都需要将进程加入到所有监视 fd 的等待队列,每次唤醒都需要从每个队列中移除。
- 每次调用后,就绪的 FD 集会被内核修改(未就绪的位被清空),下次调用前需重新初始化 FD 集合。
- 这里涉及了两次遍历,而且每次都要将整个 fd_set 列表传递给内核,有一定的开销。
- 当函数返回时,系统会将就绪描述符写入 fd_set 中,并将其拷贝到用户空间。进程被唤醒后,用户线程并不知道哪些 fd 收到数据,还需要遍历一次。
- 受 fd_set 的大小限制,32 位系统最多能监听 1024 个 fd,64 位最多监听 2048 个。
- poll
1 | int poll(struct pollfd* fds, int nfds, int timeout); |
poll 函数与 select 原理相似,都需要来回拷贝全部监听的文件描述符:
- 核心逻辑:与 select 类似,也是阻塞等待多个 FD 就绪,但解决了 FD 数量限制问题。进程通过 poll 传递一个 事件结构体链表(pollfd),每个结构体包含 FD 和需要监听的事件(读 / 写 / 异常),内核返回时标记就绪的事件,进程遍历数组处理就绪 FD。
- poll 函数采用链表的方式替代原来 select 中 fd_set 结构,因此可监听文件描述符数量不受限。
- poll 函数返回后,可以通过 pollfd 结构中的内容进行处理就绪文件描述符,相比 select 效率要高。
- 新增水平触发 LT :也就是通知程序 fd 就绪后,这次没有被处理,那么下次 poll 的时候会再次通知同个 fd 已经就绪。
缺点: - 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
- epoll
1 | int epoll_create(int size); |
epoll 使用一个文件描述符管理多个描述符,将用户进程监控的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间只需拷贝一次。
- 核心逻辑:通过 三步操作 实现高效监听:
- 创建 epoll 实例:进程调用 epoll_create 创建一个内核态的 事件表(红黑树),用于存储需要监听的 FD 和事件。
- 注册 / 修改 / 删除事件:通过 epoll_ctl 向事件表中添加、修改或删除 FD 及对应的监听事件(如 EPOLLIN 可读)。
- 等待事件就绪:调用 epoll_wait 阻塞等待事件表中的 FD 就绪,内核直接返回 仅就绪的 FD 列表,进程无需遍历所有 FD。
- 当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为 ep_poll_callback,这个回调函数其实就所把这个事件添加到 rdllist 这个双向链表中。
- 一旦有事件发生,epoll 就会将该事件添加到双向链表中。那么当我们调用 epoll_wait 时,epoll_wait 只需要检查 rdlist 双向链表中是否有存在注册的事件,效率非常可观。
- 内核用红黑树存储所有注册的 FD 和事件(支持高效的插入、删除、查找,时间复杂度 O (log n))。
- 用就绪链表存储就绪的 FD,epoll_wait 直接返回该链表,避免遍历。因为所有就绪 FD 都需要处理,最好的选择便是线性数据结构。
工作模式: - LT模式:也是默认模式,即当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,并且下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
- ET模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
- ET 是一种高速工作方式,很大程度上减少了 epoll 事件被重复触发的次数。epoll 工作在 ET 模式的时候,必须使用非阻塞 socket ,以避免由于一个 socket 因为没有数据而阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
优点: - 内核维护事件表,FD 和事件仅需注册一次(通过 epoll_ctl),无需每次调用都从用户态拷贝到内核态(仅 epoll_ctl 时拷贝,epoll_wait 无大量拷贝)。
- epoll_ctrl 是不太频繁调用的,而 epoll_wait 是非常频繁调用的。而 epoll_wait 却几乎没有入参,这比 select 的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。
- mmap 的引入,将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。
缺点: - 仅 Linux 系统支持(不跨平台,Windows 用 IOCP,BSD 用 kqueue)。
- 编程复杂度高于 select/poll。