type
status
date
slug
summary
tags
category
icon
password
1. The Big Picture——I/O 模型1.1 阻塞 I/O 模型1.2 非阻塞 I/O 模型1.3 I/O 多路复用1.4 信号驱动 I/O(了解)1.5 异步 I/O(了解)1.6 五种 I/O 模型对比2. select()3. 经典案例——聊天室归纳总结参考文章
前面几章我们完成了[基础篇]的学习,学习了 sockets 的基础 API。从这一章开始,我们就正式进入[性能篇]的学习。
网络程序很多时候都是在处理网络 I/O 事件。因此如何感知 I/O 事件,就成了网络编程的重中之重。在 Linux 系统中,我们有五种方式来感知 I/O 事件,也就是五种 I/O 模型。
1. The Big Picture——I/O 模型
这五种 I/O 模型分别是:
- 阻塞 I/O
- 非阻塞 I/O
- I/O 多路复用(
select
,poll
和epoll
)
- 信号驱动 I/O(
SIGIO
)
- 异步 I/O(POSIX
aio_
等函数)
在具体讲这五种 I/O 模型之前,我们先来看一下读数据操作必须经历的两个阶段:
- 等待数据就绪(可能很漫长)
- 讲数据从内核缓冲区拷贝到应用程序的缓冲区。
1.1 阻塞 I/O 模型
阻塞 I/O 是最常见的,默认情况下所有的套接字都是阻塞的。到目前为止,我们用得都是阻塞 I/O。以
recvfrom()
为例,其模型如下图所示:
在等待数据以及拷贝数据的过程中,应用程序都处于阻塞状态。
1.2 非阻塞 I/O 模型
非阻塞 I/O 和阻塞 I/O 的区别是,如果数据没有就绪,非阻塞 I/O 会立即返回(不会陷入阻塞),并返回错误
EAGAIN
或EWOULDBLOCK
。
应用程序不断询问内核数据是否就绪,这种方式我们称之为轮询(polling)。轮询的方式非常消耗 CPU 资源。
1.3 I/O 多路复用
当使用 I/O 多路复用时,我们可能会阻塞在
select
,poll
和epoll
等系统调用,而不会阻塞在真正的 I/O 读写操作上。
I/O 多路复用强大的地方在于,它可以一次性监视很多个文件描述符。只要其中有一个文件描述符就绪,
select
等函数就会返回。接下来的 I/O 读写操作就不会阻塞了。阻塞 I/O + 多线程
阻塞 I/O + 多线程的方式,非常类似 I/O 多路复用。一个线程负责一个文件描述符的读写操作,就算这个线程阻塞了,其它线程依然可以正常工作。不过这种方式会消耗比较多的资源。
1.4 信号驱动 I/O(了解)
我们也可以使用
SIGIO
信号,告诉内核:当 I/O 事件就绪时就通知应用程序,然后由应用程序去处理 I/O 事件。这种模型我们称之为信号驱动 I/O 模型。
信号驱动 I/O 一般用得很少,因为它引入了复杂的信号机制,增加了程序的不确定性。
1.5 异步 I/O(了解)
异步 I/O 和信号驱动 I/O 不同。异步 I/O 会让内核拷贝数据,当数据拷贝完成后再通知应用程序;而信号驱动 I/O,仅仅是让内核通知数据就绪,然后由应用程序自己拷贝数据。

Linux 对异步 I/O 的支持
aio_
系列函数是 POSIX 定义的异步 I/O 接口。遗憾的是,Linux 下的aio_
操作并不是操作系统级别的,它是 GNU libc 在用户空间借由 pthread 库实现的;而且它仅仅支持磁盘 I/O,不支持套接字 I/O。
这也是为什么在 Linux 平台上,我们一般都是使用epoll
+非阻塞 I/O 来构建高性能高并发网络程序的根本原因。1.6 五种 I/O 模型对比
如下图所示,前四种 I/O 模型都需要应用程序自己拷贝数据(同步 I/O),它们的不同之处仅在于等待数据阶段。而异步 I/O 和前四种都不同,等待数据和拷贝数据都由内核完成,应用程序在这两个阶段都不会阻塞。

2. select()
接下来,我们重点学习一下 I/O 多路复用模型。首先,我们来看一下
select()
函数:参数说明
nfds
: 它的值应该设置为三个集合中,最大的文件描述符加 1。(目的是告诉内核,我们监听文件描述符的上限)。readfds
: 读集合。传入的时候,集合中是所有要监视读事件的文件描述符;传出的时候,集合中是所有读事件已经就绪的文件描述符。writefds
: 写集合。传入的时候,集合中是所有要监视写事件的文件描述符;传出的时候,集合中是所有写事件已经就绪的文件描述符。exceptfds
: 异常集合。传入的时候,集合中是所有要监视异常事件的文件描述符;传出的时候,集合中是所有有异常事件发生的文件描述符。timeout
: 超时时间,精度为微秒。传入的时候,表示超时时间;传出的时候,表示还剩多少时间。如果timeout
为NULL
,表示无限期阻塞;如果timeout
的两个字段都为 0,则表示立马返回,不阻塞。fd_set
几乎都是用位图实现的,并且我们提供了四个宏函数用于操作这些集合:好,到这儿
select()
的理论部分,我们就学习完了。接下来,我们来构建一个经典的聊天室案例,来看一下在实际应用中,是如何使用select()
的。3. 经典案例——聊天室
这是一个多人聊天室服务器:
- 准许多人同时进入到聊天室,也允许任何人随时退出聊天室。
- 发送的消息会同时转发给聊天室的其他成员。
归纳总结
这一章,我们讲了以下内容:
- 五种 I/O 模型——重点理解阻塞 I/O,非阻塞 I/O,以及 I/O 多路复用。
select()
——select
是 I/O 多路复用的一种方式,也是本章的重点。几乎各个平台都支持select()
函数。
- 最后,我们构建了经典的聊天室案例。通过这个案列,希望大家掌握
select()
的用法,以及 I/O 多路复用的使用场景。
参考文章
- UNIX Network Programming, Volume 1: The Sockets Networking API, 3rd Edition
欢迎您在底部评论区留言,我们一起交流学习~
- 作者:Thomas He
- 链接:https://notion-next-lovat-ten.vercel.app/article/networkprogramming/6
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章