常见的 I/O 模型有哪些?(阻塞、非阻塞、多路复用、信号驱动、异步)
这是一个非常经典的计算机系统和网络编程面试题。根据 UNIX 网络编程(Richard Stevens 的经典著作)的分类,I/O 模型主要有 5 种。
为了理解这些模型,首先需要明确一个 I/O 操作(通常是 Read 操作)涉及的两个主要阶段:
- 等待数据准备好 (Waiting for the data to be ready):数据从网络到达网卡,并被复制到内核缓冲区。
- 将数据从内核拷贝到用户空间 (Copying the data from the kernel to the process):内核将数据拷贝到应用程序的内存缓冲区。
以下是这 5 种模型的详细解析及生活类比(假设你在钓鱼):
1. 阻塞 I/O (Blocking I/O)
这是最流行、最简单的模型。默认情况下,所有的 Socket 都是阻塞的。
- 过程:
- 用户进程调用
recvfrom系统调用。 - 阶段 1:如果没有数据,进程进入睡眠状态(阻塞),直到数据到达。
- 阶段 2:数据到达后,内核将其拷贝到用户内存,拷贝完成后,进程被唤醒。
- 用户进程调用
- 特点:进程在两个阶段都被阻塞。
- 钓鱼类比:你抛下鱼竿,眼睛死死盯着浮标,什么别的事都不做,直到鱼上钩并把你拉上来。
2. 非阻塞 I/O (Non-blocking I/O)
可以通过设置 Socket 使其变为非阻塞。
- 过程:
- 用户进程调用
recvfrom。 - 阶段 1:如果数据没准备好,内核立即返回一个错误(
EWOULDBLOCK或EAGAIN),进程不睡眠。 - 进程收到错误后,再次调用
recvfrom(轮询/Polling)。 - 阶段 2:一旦数据准备好,进程再次发起调用时,内核开始拷贝数据。在拷贝数据的过程中,进程是阻塞的。
- 用户进程调用
- 特点:进程通过轮询 (Polling) 方式检查数据,占用 CPU 较高。虽然阶段 1 不阻塞,但阶段 2 依然阻塞。
- 钓鱼类比:你抛下鱼竿,手里拿着手机玩。每隔几秒钟抬头看一眼浮标。没鱼就继续玩手机,有鱼了就放下手机把鱼拉上来。
3. I/O 多路复用 (I/O Multiplexing)
这是目前高并发网络编程的主流模型(如 Nginx, Redis, Netty)。常用的系统调用有 select、poll、epoll。
- 过程:
- 用户进程调用
select(或epoll),传入多个 Socket 描述符。 - 阶段 1:进程在
select调用上阻塞,等待多个 Socket 中的任意一个变为可读。 - 一旦有一个或多个 Socket 数据准备好,
select返回。 - 阶段 2:用户进程调用
recvfrom,将数据从内核拷贝到用户空间(此过程阻塞)。
- 用户进程调用
- 特点:
- 看起来和阻塞 I/O 很像,甚至还多了一次系统调用。但它的核心优势在于一个线程可以同时处理多个连接。
- 如果是处理单个连接,它的性能不如阻塞 I/O。
- 钓鱼类比:你是个鱼塘管理员,同时看着 100 个鱼竿。你坐在监控室盯着报警器(Select),一旦某个鱼竿的报警器响了,你就跑过去把那条鱼拉上来。
4. 信号驱动 I/O (Signal-driven I/O)
- 过程:
- 用户进程开启 Socket 的信号驱动 I/O 功能,并通过
sigaction系统调用安装一个信号处理函数。 - 阶段 1:系统调用立即返回,进程继续工作,不阻塞。
- 当数据准备好时,内核向进程发送
SIGIO信号。 - 阶段 2:进程在信号处理函数中调用
recvfrom,将数据拷贝到用户空间(此过程阻塞)。
- 用户进程开启 Socket 的信号驱动 I/O 功能,并通过
- 特点:阶段 1 是非阻塞的,但阶段 2(拷贝数据)依然是阻塞的。
- 钓鱼类比:你在鱼竿上绑了一个铃铛,然后坐在一旁看书。铃铛响了(信号),你放下书,把鱼拉上来。
5. 异步 I/O (Asynchronous I/O, AIO)
这是真正的异步模型。
- 过程:
- 用户进程发起
aio_read系统调用,传入缓冲区地址等信息。 - 阶段 1 & 阶段 2:系统调用立即返回,进程继续做其他事。内核负责等待数据并将数据拷贝到用户指定的缓冲区。
- 当所有操作完成(数据已经在用户缓冲区了),内核通过信号或回调函数通知用户进程。
- 用户进程发起
- 特点:进程在整个 I/O 过程中(等待 + 拷贝)完全没有被阻塞。
- 钓鱼类比:你雇了一个专业的捕鱼人。你告诉他:“去帮我钓条鱼,直接送到我家厨房的桶里,搞定后发微信告诉我。”然后你就去逛街了。
总结与对比
为了区分这些模型,最关键的是看同步 (Synchronous) 还是 异步 (Asynchronous)。
根据 POSIX 的定义:
- 同步 I/O 操作:导致请求进程阻塞,直到 I/O 操作完成。
- 异步 I/O 操作:不导致请求进程阻塞。
| 模型 | 阶段 1 (等待数据) | 阶段 2 (拷贝数据) | 归类 | 关键点 |
|---|---|---|---|---|
| 阻塞 I/O | 阻塞 | 阻塞 | 同步 | 傻等,啥也不干 |
| 非阻塞 I/O | 非阻塞 (轮询) | 阻塞 | 同步 | 忙轮询,浪费 CPU |
| 多路复用 | 阻塞 (在 select 上) | 阻塞 | 同步 | 一个看门大爷管所有门 |
| 信号驱动 | 非阻塞 (回调) | 阻塞 | 同步 | 也就是等数据时不堵,读数据还得堵 |
| 异步 I/O | 非阻塞 | 非阻塞 | 异步 | 全程托管,真正的甩手掌柜 |
注意:
前四种模型(阻塞、非阻塞、多路复用、信号驱动)实际上都属于 同步 I/O。因为它们在第二阶段(数据从内核拷贝到用户空间)时,进程都是阻塞的,必须等待拷贝完成才能继续执行。
只有 异步 I/O (AIO) 才是真正的异步,因为数据拷贝过程也是由内核完成的,用户进程完全没参与等待。
右滑查看面试常问